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