Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.annotation.DrawableRes;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Rect;
     25 import android.graphics.drawable.Drawable;
     26 import android.os.Build;
     27 import android.util.AttributeSet;
     28 import android.view.MotionEvent;
     29 import android.view.PointerIcon;
     30 import android.view.View;
     31 import android.view.View.OnFocusChangeListener;
     32 import android.view.ViewGroup;
     33 import android.view.accessibility.AccessibilityEvent;
     34 
     35 import com.android.internal.R;
     36 
     37 /**
     38  *
     39  * Displays a list of tab labels representing each page in the parent's tab
     40  * collection.
     41  * <p>
     42  * The container object for this widget is {@link android.widget.TabHost TabHost}.
     43  * When the user selects a tab, this object sends a message to the parent
     44  * container, TabHost, to tell it to switch the displayed page. You typically
     45  * won't use many methods directly on this object. The container TabHost is
     46  * used to add labels, add the callback handler, and manage callbacks. You
     47  * might call this object to iterate the list of tabs, or to tweak the layout
     48  * of the tab list, but most methods should be called on the containing TabHost
     49  * object.
     50  *
     51  * @attr ref android.R.styleable#TabWidget_divider
     52  * @attr ref android.R.styleable#TabWidget_tabStripEnabled
     53  * @attr ref android.R.styleable#TabWidget_tabStripLeft
     54  * @attr ref android.R.styleable#TabWidget_tabStripRight
     55  */
     56 public class TabWidget extends LinearLayout implements OnFocusChangeListener {
     57     private final Rect mBounds = new Rect();
     58 
     59     private OnTabSelectionChanged mSelectionChangedListener;
     60 
     61     // This value will be set to 0 as soon as the first tab is added to TabHost.
     62     private int mSelectedTab = -1;
     63 
     64     private Drawable mLeftStrip;
     65     private Drawable mRightStrip;
     66 
     67     private boolean mDrawBottomStrips = true;
     68     private boolean mStripMoved;
     69 
     70     // When positive, the widths and heights of tabs will be imposed so that
     71     // they fit in parent.
     72     private int mImposedTabsHeight = -1;
     73     private int[] mImposedTabWidths;
     74 
     75     public TabWidget(Context context) {
     76         this(context, null);
     77     }
     78 
     79     public TabWidget(Context context, AttributeSet attrs) {
     80         this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
     81     }
     82 
     83     public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) {
     84         this(context, attrs, defStyleAttr, 0);
     85     }
     86 
     87     public TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     88         super(context, attrs, defStyleAttr, defStyleRes);
     89 
     90         final TypedArray a = context.obtainStyledAttributes(
     91                 attrs, R.styleable.TabWidget, defStyleAttr, defStyleRes);
     92 
     93         mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, mDrawBottomStrips);
     94 
     95         // Tests the target SDK version, as set in the Manifest. Could not be
     96         // set using styles.xml in a values-v? directory which targets the
     97         // current platform SDK version instead.
     98         final boolean isTargetSdkDonutOrLower =
     99                 context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT;
    100 
    101         final boolean hasExplicitLeft = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripLeft);
    102         if (hasExplicitLeft) {
    103             mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
    104         } else if (isTargetSdkDonutOrLower) {
    105             mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left_v4);
    106         } else {
    107             mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left);
    108         }
    109 
    110         final boolean hasExplicitRight = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripRight);
    111         if (hasExplicitRight) {
    112             mRightStrip = a.getDrawable(R.styleable.TabWidget_tabStripRight);
    113         } else if (isTargetSdkDonutOrLower) {
    114             mRightStrip = context.getDrawable(R.drawable.tab_bottom_right_v4);
    115         } else {
    116             mRightStrip = context.getDrawable(R.drawable.tab_bottom_right);
    117         }
    118 
    119         a.recycle();
    120 
    121         setChildrenDrawingOrderEnabled(true);
    122     }
    123 
    124     @Override
    125     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    126         mStripMoved = true;
    127 
    128         super.onSizeChanged(w, h, oldw, oldh);
    129     }
    130 
    131     @Override
    132     protected int getChildDrawingOrder(int childCount, int i) {
    133         if (mSelectedTab == -1) {
    134             return i;
    135         } else {
    136             // Always draw the selected tab last, so that drop shadows are drawn
    137             // in the correct z-order.
    138             if (i == childCount - 1) {
    139                 return mSelectedTab;
    140             } else if (i >= mSelectedTab) {
    141                 return i + 1;
    142             } else {
    143                 return i;
    144             }
    145         }
    146     }
    147 
    148     @Override
    149     void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth,
    150             int heightMeasureSpec, int totalHeight) {
    151         if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) {
    152             widthMeasureSpec = MeasureSpec.makeMeasureSpec(
    153                     totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY);
    154             heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight,
    155                     MeasureSpec.EXACTLY);
    156         }
    157 
    158         super.measureChildBeforeLayout(child, childIndex,
    159                 widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
    160     }
    161 
    162     @Override
    163     void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
    164         if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
    165             super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    166             return;
    167         }
    168 
    169         // First, measure with no constraint
    170         final int width = MeasureSpec.getSize(widthMeasureSpec);
    171         final int unspecifiedWidth = MeasureSpec.makeSafeMeasureSpec(width,
    172                 MeasureSpec.UNSPECIFIED);
    173         mImposedTabsHeight = -1;
    174         super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
    175 
    176         int extraWidth = getMeasuredWidth() - width;
    177         if (extraWidth > 0) {
    178             final int count = getChildCount();
    179 
    180             int childCount = 0;
    181             for (int i = 0; i < count; i++) {
    182                 final View child = getChildAt(i);
    183                 if (child.getVisibility() == GONE) continue;
    184                 childCount++;
    185             }
    186 
    187             if (childCount > 0) {
    188                 if (mImposedTabWidths == null || mImposedTabWidths.length != count) {
    189                     mImposedTabWidths = new int[count];
    190                 }
    191                 for (int i = 0; i < count; i++) {
    192                     final View child = getChildAt(i);
    193                     if (child.getVisibility() == GONE) continue;
    194                     final int childWidth = child.getMeasuredWidth();
    195                     final int delta = extraWidth / childCount;
    196                     final int newWidth = Math.max(0, childWidth - delta);
    197                     mImposedTabWidths[i] = newWidth;
    198                     // Make sure the extra width is evenly distributed, no int division remainder
    199                     extraWidth -= childWidth - newWidth; // delta may have been clamped
    200                     childCount--;
    201                     mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight());
    202                 }
    203             }
    204         }
    205 
    206         // Measure again, this time with imposed tab widths and respecting
    207         // initial spec request.
    208         super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    209     }
    210 
    211     /**
    212      * Returns the tab indicator view at the given index.
    213      *
    214      * @param index the zero-based index of the tab indicator view to return
    215      * @return the tab indicator view at the given index
    216      */
    217     public View getChildTabViewAt(int index) {
    218         return getChildAt(index);
    219     }
    220 
    221     /**
    222      * Returns the number of tab indicator views.
    223      *
    224      * @return the number of tab indicator views
    225      */
    226     public int getTabCount() {
    227         return getChildCount();
    228     }
    229 
    230     /**
    231      * Sets the drawable to use as a divider between the tab indicators.
    232      *
    233      * @param drawable the divider drawable
    234      * @attr ref android.R.styleable#TabWidget_divider
    235      */
    236     @Override
    237     public void setDividerDrawable(@Nullable Drawable drawable) {
    238         super.setDividerDrawable(drawable);
    239     }
    240 
    241     /**
    242      * Sets the drawable to use as a divider between the tab indicators.
    243      *
    244      * @param resId the resource identifier of the drawable to use as a divider
    245      * @attr ref android.R.styleable#TabWidget_divider
    246      */
    247     public void setDividerDrawable(@DrawableRes int resId) {
    248         setDividerDrawable(mContext.getDrawable(resId));
    249     }
    250 
    251     /**
    252      * Sets the drawable to use as the left part of the strip below the tab
    253      * indicators.
    254      *
    255      * @param drawable the left strip drawable
    256      * @see #getLeftStripDrawable()
    257      * @attr ref android.R.styleable#TabWidget_tabStripLeft
    258      */
    259     public void setLeftStripDrawable(@Nullable Drawable drawable) {
    260         mLeftStrip = drawable;
    261         requestLayout();
    262         invalidate();
    263     }
    264 
    265     /**
    266      * Sets the drawable to use as the left part of the strip below the tab
    267      * indicators.
    268      *
    269      * @param resId the resource identifier of the drawable to use as the left
    270      *              strip drawable
    271      * @see #getLeftStripDrawable()
    272      * @attr ref android.R.styleable#TabWidget_tabStripLeft
    273      */
    274     public void setLeftStripDrawable(@DrawableRes int resId) {
    275         setLeftStripDrawable(mContext.getDrawable(resId));
    276     }
    277 
    278     /**
    279      * @return the drawable used as the left part of the strip below the tab
    280      *         indicators, may be {@code null}
    281      * @see #setLeftStripDrawable(int)
    282      * @see #setLeftStripDrawable(Drawable)
    283      * @attr ref android.R.styleable#TabWidget_tabStripLeft
    284      */
    285     @Nullable
    286     public Drawable getLeftStripDrawable() {
    287         return mLeftStrip;
    288     }
    289 
    290     /**
    291      * Sets the drawable to use as the right part of the strip below the tab
    292      * indicators.
    293      *
    294      * @param drawable the right strip drawable
    295      * @see #getRightStripDrawable()
    296      * @attr ref android.R.styleable#TabWidget_tabStripRight
    297      */
    298     public void setRightStripDrawable(@Nullable Drawable drawable) {
    299         mRightStrip = drawable;
    300         requestLayout();
    301         invalidate();
    302     }
    303 
    304     /**
    305      * Sets the drawable to use as the right part of the strip below the tab
    306      * indicators.
    307      *
    308      * @param resId the resource identifier of the drawable to use as the right
    309      *              strip drawable
    310      * @see #getRightStripDrawable()
    311      * @attr ref android.R.styleable#TabWidget_tabStripRight
    312      */
    313     public void setRightStripDrawable(@DrawableRes int resId) {
    314         setRightStripDrawable(mContext.getDrawable(resId));
    315     }
    316 
    317     /**
    318      * @return the drawable used as the right part of the strip below the tab
    319      *         indicators, may be {@code null}
    320      * @see #setRightStripDrawable(int)
    321      * @see #setRightStripDrawable(Drawable)
    322      * @attr ref android.R.styleable#TabWidget_tabStripRight
    323      */
    324     @Nullable
    325     public Drawable getRightStripDrawable() {
    326         return mRightStrip;
    327     }
    328 
    329     /**
    330      * Controls whether the bottom strips on the tab indicators are drawn or
    331      * not.  The default is to draw them.  If the user specifies a custom
    332      * view for the tab indicators, then the TabHost class calls this method
    333      * to disable drawing of the bottom strips.
    334      * @param stripEnabled true if the bottom strips should be drawn.
    335      */
    336     public void setStripEnabled(boolean stripEnabled) {
    337         mDrawBottomStrips = stripEnabled;
    338         invalidate();
    339     }
    340 
    341     /**
    342      * Indicates whether the bottom strips on the tab indicators are drawn
    343      * or not.
    344      */
    345     public boolean isStripEnabled() {
    346         return mDrawBottomStrips;
    347     }
    348 
    349     @Override
    350     public void childDrawableStateChanged(View child) {
    351         if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
    352             // To make sure that the bottom strip is redrawn
    353             invalidate();
    354         }
    355         super.childDrawableStateChanged(child);
    356     }
    357 
    358     @Override
    359     public void dispatchDraw(Canvas canvas) {
    360         super.dispatchDraw(canvas);
    361 
    362         // Do nothing if there are no tabs.
    363         if (getTabCount() == 0) return;
    364 
    365         // If the user specified a custom view for the tab indicators, then
    366         // do not draw the bottom strips.
    367         if (!mDrawBottomStrips) {
    368             // Skip drawing the bottom strips.
    369             return;
    370         }
    371 
    372         final View selectedChild = getChildTabViewAt(mSelectedTab);
    373 
    374         final Drawable leftStrip = mLeftStrip;
    375         final Drawable rightStrip = mRightStrip;
    376 
    377         leftStrip.setState(selectedChild.getDrawableState());
    378         rightStrip.setState(selectedChild.getDrawableState());
    379 
    380         if (mStripMoved) {
    381             final Rect bounds = mBounds;
    382             bounds.left = selectedChild.getLeft();
    383             bounds.right = selectedChild.getRight();
    384             final int myHeight = getHeight();
    385             leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
    386                     myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
    387             rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
    388                     Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
    389             mStripMoved = false;
    390         }
    391 
    392         leftStrip.draw(canvas);
    393         rightStrip.draw(canvas);
    394     }
    395 
    396     /**
    397      * Sets the current tab.
    398      * <p>
    399      * This method is used to bring a tab to the front of the Widget,
    400      * and is used to post to the rest of the UI that a different tab
    401      * has been brought to the foreground.
    402      * <p>
    403      * Note, this is separate from the traditional "focus" that is
    404      * employed from the view logic.
    405      * <p>
    406      * For instance, if we have a list in a tabbed view, a user may be
    407      * navigating up and down the list, moving the UI focus (orange
    408      * highlighting) through the list items.  The cursor movement does
    409      * not effect the "selected" tab though, because what is being
    410      * scrolled through is all on the same tab.  The selected tab only
    411      * changes when we navigate between tabs (moving from the list view
    412      * to the next tabbed view, in this example).
    413      * <p>
    414      * To move both the focus AND the selected tab at once, please use
    415      * {@link #setCurrentTab}. Normally, the view logic takes care of
    416      * adjusting the focus, so unless you're circumventing the UI,
    417      * you'll probably just focus your interest here.
    418      *
    419      * @param index the index of the tab that you want to indicate as the
    420      *              selected tab (tab brought to the front of the widget)
    421      * @see #focusCurrentTab
    422      */
    423     public void setCurrentTab(int index) {
    424         if (index < 0 || index >= getTabCount() || index == mSelectedTab) {
    425             return;
    426         }
    427 
    428         if (mSelectedTab != -1) {
    429             getChildTabViewAt(mSelectedTab).setSelected(false);
    430         }
    431         mSelectedTab = index;
    432         getChildTabViewAt(mSelectedTab).setSelected(true);
    433         mStripMoved = true;
    434     }
    435 
    436     @Override
    437     public CharSequence getAccessibilityClassName() {
    438         return TabWidget.class.getName();
    439     }
    440 
    441     /** @hide */
    442     @Override
    443     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
    444         super.onInitializeAccessibilityEventInternal(event);
    445         event.setItemCount(getTabCount());
    446         event.setCurrentItemIndex(mSelectedTab);
    447     }
    448 
    449     /**
    450      * Sets the current tab and focuses the UI on it.
    451      * This method makes sure that the focused tab matches the selected
    452      * tab, normally at {@link #setCurrentTab}.  Normally this would not
    453      * be an issue if we go through the UI, since the UI is responsible
    454      * for calling TabWidget.onFocusChanged(), but in the case where we
    455      * are selecting the tab programmatically, we'll need to make sure
    456      * focus keeps up.
    457      *
    458      *  @param index The tab that you want focused (highlighted in orange)
    459      *  and selected (tab brought to the front of the widget)
    460      *
    461      *  @see #setCurrentTab
    462      */
    463     public void focusCurrentTab(int index) {
    464         final int oldTab = mSelectedTab;
    465 
    466         // set the tab
    467         setCurrentTab(index);
    468 
    469         // change the focus if applicable.
    470         if (oldTab != index) {
    471             getChildTabViewAt(index).requestFocus();
    472         }
    473     }
    474 
    475     @Override
    476     public void setEnabled(boolean enabled) {
    477         super.setEnabled(enabled);
    478 
    479         final int count = getTabCount();
    480         for (int i = 0; i < count; i++) {
    481             final View child = getChildTabViewAt(i);
    482             child.setEnabled(enabled);
    483         }
    484     }
    485 
    486     @Override
    487     public void addView(View child) {
    488         if (child.getLayoutParams() == null) {
    489             final LinearLayout.LayoutParams lp = new LayoutParams(
    490                     0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
    491             lp.setMargins(0, 0, 0, 0);
    492             child.setLayoutParams(lp);
    493         }
    494 
    495         // Ensure you can navigate to the tab with the keyboard, and you can touch it
    496         child.setFocusable(true);
    497         child.setClickable(true);
    498 
    499         if (child.getPointerIcon() == null) {
    500             child.setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND));
    501         }
    502 
    503         super.addView(child);
    504 
    505         // TODO: detect this via geometry with a tabwidget listener rather
    506         // than potentially interfere with the view's listener
    507         child.setOnClickListener(new TabClickListener(getTabCount() - 1));
    508     }
    509 
    510     @Override
    511     public void removeAllViews() {
    512         super.removeAllViews();
    513         mSelectedTab = -1;
    514     }
    515 
    516     @Override
    517     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
    518         if (!isEnabled()) {
    519             return null;
    520         }
    521         return super.onResolvePointerIcon(event, pointerIndex);
    522     }
    523 
    524     /**
    525      * Provides a way for {@link TabHost} to be notified that the user clicked
    526      * on a tab indicator.
    527      */
    528     void setTabSelectionListener(OnTabSelectionChanged listener) {
    529         mSelectionChangedListener = listener;
    530     }
    531 
    532     @Override
    533     public void onFocusChange(View v, boolean hasFocus) {
    534         // No-op. Tab selection is separate from keyboard focus.
    535     }
    536 
    537     // registered with each tab indicator so we can notify tab host
    538     private class TabClickListener implements OnClickListener {
    539         private final int mTabIndex;
    540 
    541         private TabClickListener(int tabIndex) {
    542             mTabIndex = tabIndex;
    543         }
    544 
    545         public void onClick(View v) {
    546             mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
    547         }
    548     }
    549 
    550     /**
    551      * Lets {@link TabHost} know that the user clicked on a tab indicator.
    552      */
    553     interface OnTabSelectionChanged {
    554         /**
    555          * Informs the TabHost which tab was selected. It also indicates
    556          * if the tab was clicked/pressed or just focused into.
    557          *
    558          * @param tabIndex index of the tab that was selected
    559          * @param clicked whether the selection changed due to a touch/click or
    560          *                due to focus entering the tab through navigation.
    561          *                {@code true} if it was due to a press/click and
    562          *                {@code false} otherwise.
    563          */
    564         void onTabSelectionChanged(int tabIndex, boolean clicked);
    565     }
    566 }
    567