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