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