Add SDK 29 sources.
Test: N/A
Change-Id: Iedb7a31029e003928eb16f7e69ed147e72bb6235
diff --git a/android/widget/TabWidget.java b/android/widget/TabWidget.java
new file mode 100644
index 0000000..49a0f39
--- /dev/null
+++ b/android/widget/TabWidget.java
@@ -0,0 +1,588 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.R;
+
+/**
+ *
+ * Displays a list of tab labels representing each page in the parent's tab
+ * collection.
+ * <p>
+ * The container object for this widget is {@link android.widget.TabHost TabHost}.
+ * When the user selects a tab, this object sends a message to the parent
+ * container, TabHost, to tell it to switch the displayed page. You typically
+ * won't use many methods directly on this object. The container TabHost is
+ * used to add labels, add the callback handler, and manage callbacks. You
+ * might call this object to iterate the list of tabs, or to tweak the layout
+ * of the tab list, but most methods should be called on the containing TabHost
+ * object.
+ *
+ * @attr ref android.R.styleable#TabWidget_divider
+ * @attr ref android.R.styleable#TabWidget_tabStripEnabled
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+public class TabWidget extends LinearLayout implements OnFocusChangeListener {
+ private final Rect mBounds = new Rect();
+
+ private OnTabSelectionChanged mSelectionChangedListener;
+
+ // This value will be set to 0 as soon as the first tab is added to TabHost.
+ @UnsupportedAppUsage
+ private int mSelectedTab = -1;
+
+ @Nullable
+ private Drawable mLeftStrip;
+
+ @Nullable
+ private Drawable mRightStrip;
+
+ @UnsupportedAppUsage
+ private boolean mDrawBottomStrips = true;
+ private boolean mStripMoved;
+
+ // When positive, the widths and heights of tabs will be imposed so that
+ // they fit in parent.
+ private int mImposedTabsHeight = -1;
+ private int[] mImposedTabWidths;
+
+ public TabWidget(Context context) {
+ this(context, null);
+ }
+
+ public TabWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
+ }
+
+ public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.TabWidget, defStyleAttr, defStyleRes);
+ saveAttributeDataForStyleable(context, R.styleable.TabWidget,
+ attrs, a, defStyleAttr, defStyleRes);
+
+ mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, mDrawBottomStrips);
+
+ // Tests the target SDK version, as set in the Manifest. Could not be
+ // set using styles.xml in a values-v? directory which targets the
+ // current platform SDK version instead.
+ final boolean isTargetSdkDonutOrLower =
+ context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT;
+
+ final boolean hasExplicitLeft = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripLeft);
+ if (hasExplicitLeft) {
+ mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
+ } else if (isTargetSdkDonutOrLower) {
+ mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left_v4);
+ } else {
+ mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left);
+ }
+
+ final boolean hasExplicitRight = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripRight);
+ if (hasExplicitRight) {
+ mRightStrip = a.getDrawable(R.styleable.TabWidget_tabStripRight);
+ } else if (isTargetSdkDonutOrLower) {
+ mRightStrip = context.getDrawable(R.drawable.tab_bottom_right_v4);
+ } else {
+ mRightStrip = context.getDrawable(R.drawable.tab_bottom_right);
+ }
+
+ a.recycle();
+
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mStripMoved = true;
+
+ super.onSizeChanged(w, h, oldw, oldh);
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ if (mSelectedTab == -1) {
+ return i;
+ } else {
+ // Always draw the selected tab last, so that drop shadows are drawn
+ // in the correct z-order.
+ if (i == childCount - 1) {
+ return mSelectedTab;
+ } else if (i >= mSelectedTab) {
+ return i + 1;
+ } else {
+ return i;
+ }
+ }
+ }
+
+ @Override
+ void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth,
+ int heightMeasureSpec, int totalHeight) {
+ if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) {
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight,
+ MeasureSpec.EXACTLY);
+ }
+
+ super.measureChildBeforeLayout(child, childIndex,
+ widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
+ }
+
+ @Override
+ void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
+ if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+ super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+
+ // First, measure with no constraint
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int unspecifiedWidth = MeasureSpec.makeSafeMeasureSpec(width,
+ MeasureSpec.UNSPECIFIED);
+ mImposedTabsHeight = -1;
+ super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
+
+ int extraWidth = getMeasuredWidth() - width;
+ if (extraWidth > 0) {
+ final int count = getChildCount();
+
+ int childCount = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+ childCount++;
+ }
+
+ if (childCount > 0) {
+ if (mImposedTabWidths == null || mImposedTabWidths.length != count) {
+ mImposedTabWidths = new int[count];
+ }
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+ final int childWidth = child.getMeasuredWidth();
+ final int delta = extraWidth / childCount;
+ final int newWidth = Math.max(0, childWidth - delta);
+ mImposedTabWidths[i] = newWidth;
+ // Make sure the extra width is evenly distributed, no int division remainder
+ extraWidth -= childWidth - newWidth; // delta may have been clamped
+ childCount--;
+ mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight());
+ }
+ }
+ }
+
+ // Measure again, this time with imposed tab widths and respecting
+ // initial spec request.
+ super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * Returns the tab indicator view at the given index.
+ *
+ * @param index the zero-based index of the tab indicator view to return
+ * @return the tab indicator view at the given index
+ */
+ public View getChildTabViewAt(int index) {
+ return getChildAt(index);
+ }
+
+ /**
+ * Returns the number of tab indicator views.
+ *
+ * @return the number of tab indicator views
+ */
+ public int getTabCount() {
+ return getChildCount();
+ }
+
+ /**
+ * Sets the drawable to use as a divider between the tab indicators.
+ *
+ * @param drawable the divider drawable
+ * @attr ref android.R.styleable#TabWidget_divider
+ */
+ @Override
+ public void setDividerDrawable(@Nullable Drawable drawable) {
+ super.setDividerDrawable(drawable);
+ }
+
+ /**
+ * Sets the drawable to use as a divider between the tab indicators.
+ *
+ * @param resId the resource identifier of the drawable to use as a divider
+ * @attr ref android.R.styleable#TabWidget_divider
+ */
+ public void setDividerDrawable(@DrawableRes int resId) {
+ setDividerDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * Sets the drawable to use as the left part of the strip below the tab
+ * indicators.
+ *
+ * @param drawable the left strip drawable
+ * @see #getLeftStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ */
+ public void setLeftStripDrawable(@Nullable Drawable drawable) {
+ mLeftStrip = drawable;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the drawable to use as the left part of the strip below the tab
+ * indicators.
+ *
+ * @param resId the resource identifier of the drawable to use as the left
+ * strip drawable
+ * @see #getLeftStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ */
+ public void setLeftStripDrawable(@DrawableRes int resId) {
+ setLeftStripDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * @return the drawable used as the left part of the strip below the tab
+ * indicators, may be {@code null}
+ * @see #setLeftStripDrawable(int)
+ * @see #setLeftStripDrawable(Drawable)
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ */
+ @Nullable
+ public Drawable getLeftStripDrawable() {
+ return mLeftStrip;
+ }
+
+ /**
+ * Sets the drawable to use as the right part of the strip below the tab
+ * indicators.
+ *
+ * @param drawable the right strip drawable
+ * @see #getRightStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+ public void setRightStripDrawable(@Nullable Drawable drawable) {
+ mRightStrip = drawable;
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Sets the drawable to use as the right part of the strip below the tab
+ * indicators.
+ *
+ * @param resId the resource identifier of the drawable to use as the right
+ * strip drawable
+ * @see #getRightStripDrawable()
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+ public void setRightStripDrawable(@DrawableRes int resId) {
+ setRightStripDrawable(mContext.getDrawable(resId));
+ }
+
+ /**
+ * @return the drawable used as the right part of the strip below the tab
+ * indicators, may be {@code null}
+ * @see #setRightStripDrawable(int)
+ * @see #setRightStripDrawable(Drawable)
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+ @Nullable
+ public Drawable getRightStripDrawable() {
+ return mRightStrip;
+ }
+
+ /**
+ * Controls whether the bottom strips on the tab indicators are drawn or
+ * not. The default is to draw them. If the user specifies a custom
+ * view for the tab indicators, then the TabHost class calls this method
+ * to disable drawing of the bottom strips.
+ * @param stripEnabled true if the bottom strips should be drawn.
+ */
+ public void setStripEnabled(boolean stripEnabled) {
+ mDrawBottomStrips = stripEnabled;
+ invalidate();
+ }
+
+ /**
+ * Indicates whether the bottom strips on the tab indicators are drawn
+ * or not.
+ */
+ public boolean isStripEnabled() {
+ return mDrawBottomStrips;
+ }
+
+ @Override
+ public void childDrawableStateChanged(View child) {
+ if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
+ // To make sure that the bottom strip is redrawn
+ invalidate();
+ }
+ super.childDrawableStateChanged(child);
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ // Do nothing if there are no tabs.
+ if (getTabCount() == 0) return;
+
+ // If the user specified a custom view for the tab indicators, then
+ // do not draw the bottom strips.
+ if (!mDrawBottomStrips) {
+ // Skip drawing the bottom strips.
+ return;
+ }
+
+ final View selectedChild = getChildTabViewAt(mSelectedTab);
+
+ final Drawable leftStrip = mLeftStrip;
+ final Drawable rightStrip = mRightStrip;
+
+ if (leftStrip != null) {
+ leftStrip.setState(selectedChild.getDrawableState());
+ }
+ if (rightStrip != null) {
+ rightStrip.setState(selectedChild.getDrawableState());
+ }
+
+ if (mStripMoved) {
+ final Rect bounds = mBounds;
+ bounds.left = selectedChild.getLeft();
+ bounds.right = selectedChild.getRight();
+ final int myHeight = getHeight();
+ if (leftStrip != null) {
+ leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
+ myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
+ }
+ if (rightStrip != null) {
+ rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
+ Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()),
+ myHeight);
+ }
+ mStripMoved = false;
+ }
+
+ if (leftStrip != null) {
+ leftStrip.draw(canvas);
+ }
+ if (rightStrip != null) {
+ rightStrip.draw(canvas);
+ }
+ }
+
+ /**
+ * Sets the current tab.
+ * <p>
+ * This method is used to bring a tab to the front of the Widget,
+ * and is used to post to the rest of the UI that a different tab
+ * has been brought to the foreground.
+ * <p>
+ * Note, this is separate from the traditional "focus" that is
+ * employed from the view logic.
+ * <p>
+ * For instance, if we have a list in a tabbed view, a user may be
+ * navigating up and down the list, moving the UI focus (orange
+ * highlighting) through the list items. The cursor movement does
+ * not effect the "selected" tab though, because what is being
+ * scrolled through is all on the same tab. The selected tab only
+ * changes when we navigate between tabs (moving from the list view
+ * to the next tabbed view, in this example).
+ * <p>
+ * To move both the focus AND the selected tab at once, please use
+ * {@link #setCurrentTab}. Normally, the view logic takes care of
+ * adjusting the focus, so unless you're circumventing the UI,
+ * you'll probably just focus your interest here.
+ *
+ * @param index the index of the tab that you want to indicate as the
+ * selected tab (tab brought to the front of the widget)
+ * @see #focusCurrentTab
+ */
+ public void setCurrentTab(int index) {
+ if (index < 0 || index >= getTabCount() || index == mSelectedTab) {
+ return;
+ }
+
+ if (mSelectedTab != -1) {
+ getChildTabViewAt(mSelectedTab).setSelected(false);
+ }
+ mSelectedTab = index;
+ getChildTabViewAt(mSelectedTab).setSelected(true);
+ mStripMoved = true;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return TabWidget.class.getName();
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ event.setItemCount(getTabCount());
+ event.setCurrentItemIndex(mSelectedTab);
+ }
+
+ /**
+ * Sets the current tab and focuses the UI on it.
+ * This method makes sure that the focused tab matches the selected
+ * tab, normally at {@link #setCurrentTab}. Normally this would not
+ * be an issue if we go through the UI, since the UI is responsible
+ * for calling TabWidget.onFocusChanged(), but in the case where we
+ * are selecting the tab programmatically, we'll need to make sure
+ * focus keeps up.
+ *
+ * @param index The tab that you want focused (highlighted in orange)
+ * and selected (tab brought to the front of the widget)
+ *
+ * @see #setCurrentTab
+ */
+ public void focusCurrentTab(int index) {
+ final int oldTab = mSelectedTab;
+
+ // set the tab
+ setCurrentTab(index);
+
+ // change the focus if applicable.
+ if (oldTab != index) {
+ getChildTabViewAt(index).requestFocus();
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+
+ final int count = getTabCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildTabViewAt(i);
+ child.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void addView(View child) {
+ if (child.getLayoutParams() == null) {
+ final LinearLayout.LayoutParams lp = new LayoutParams(
+ 0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
+ lp.setMargins(0, 0, 0, 0);
+ child.setLayoutParams(lp);
+ }
+
+ // Ensure you can navigate to the tab with the keyboard, and you can touch it
+ child.setFocusable(true);
+ child.setClickable(true);
+
+ if (child.getPointerIcon() == null) {
+ child.setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND));
+ }
+
+ super.addView(child);
+
+ // TODO: detect this via geometry with a tabwidget listener rather
+ // than potentially interfere with the view's listener
+ child.setOnClickListener(new TabClickListener(getTabCount() - 1));
+ }
+
+ @Override
+ public void removeAllViews() {
+ super.removeAllViews();
+ mSelectedTab = -1;
+ }
+
+ @Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ if (!isEnabled()) {
+ return null;
+ }
+ return super.onResolvePointerIcon(event, pointerIndex);
+ }
+
+ /**
+ * Provides a way for {@link TabHost} to be notified that the user clicked
+ * on a tab indicator.
+ */
+ @UnsupportedAppUsage
+ void setTabSelectionListener(OnTabSelectionChanged listener) {
+ mSelectionChangedListener = listener;
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ // No-op. Tab selection is separate from keyboard focus.
+ }
+
+ // registered with each tab indicator so we can notify tab host
+ private class TabClickListener implements OnClickListener {
+ private final int mTabIndex;
+
+ private TabClickListener(int tabIndex) {
+ mTabIndex = tabIndex;
+ }
+
+ public void onClick(View v) {
+ mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
+ }
+ }
+
+ /**
+ * Lets {@link TabHost} know that the user clicked on a tab indicator.
+ */
+ interface OnTabSelectionChanged {
+ /**
+ * Informs the TabHost which tab was selected. It also indicates
+ * if the tab was clicked/pressed or just focused into.
+ *
+ * @param tabIndex index of the tab that was selected
+ * @param clicked whether the selection changed due to a touch/click or
+ * due to focus entering the tab through navigation.
+ * {@code true} if it was due to a press/click and
+ * {@code false} otherwise.
+ */
+ void onTabSelectionChanged(int tabIndex, boolean clicked);
+ }
+}