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