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