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