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.ViewGroup;
     30 import android.view.View.OnFocusChangeListener;
     31 
     32 /**
     33  *
     34  * Displays a list of tab labels representing each page in the parent's tab
     35  * collection. The container object for this widget is
     36  * {@link android.widget.TabHost TabHost}. When the user selects a tab, this
     37  * object sends a message to the parent container, TabHost, to tell it to switch
     38  * the displayed page. You typically won't use many methods directly on this
     39  * object. The container TabHost is used to add labels, add the callback
     40  * handler, and manage callbacks. You might call this object to iterate the list
     41  * of tabs, or to tweak the layout of the tab list, but most methods should be
     42  * called on the containing TabHost object.
     43  *
     44  * @attr ref android.R.styleable#TabWidget_divider
     45  * @attr ref android.R.styleable#TabWidget_tabStripEnabled
     46  * @attr ref android.R.styleable#TabWidget_tabStripLeft
     47  * @attr ref android.R.styleable#TabWidget_tabStripRight
     48  */
     49 public class TabWidget extends LinearLayout implements OnFocusChangeListener {
     50     private OnTabSelectionChanged mSelectionChangedListener;
     51 
     52     private int mSelectedTab = 0;
     53 
     54     private Drawable mLeftStrip;
     55     private Drawable mRightStrip;
     56 
     57     private boolean mDrawBottomStrips = true;
     58     private boolean mStripMoved;
     59 
     60     private Drawable mDividerDrawable;
     61 
     62     private final Rect mBounds = new Rect();
     63 
     64     public TabWidget(Context context) {
     65         this(context, null);
     66     }
     67 
     68     public TabWidget(Context context, AttributeSet attrs) {
     69         this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
     70     }
     71 
     72     public TabWidget(Context context, AttributeSet attrs, int defStyle) {
     73         super(context, attrs);
     74 
     75         TypedArray a =
     76             context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget,
     77                     defStyle, 0);
     78 
     79         mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true);
     80         mDividerDrawable = a.getDrawable(R.styleable.TabWidget_divider);
     81         mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
     82         mRightStrip = a.getDrawable(R.styleable.TabWidget_tabStripRight);
     83 
     84         a.recycle();
     85 
     86         initTabWidget();
     87     }
     88 
     89     @Override
     90     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
     91         mStripMoved = true;
     92         super.onSizeChanged(w, h, oldw, oldh);
     93     }
     94 
     95     @Override
     96     protected int getChildDrawingOrder(int childCount, int i) {
     97         // Always draw the selected tab last, so that drop shadows are drawn
     98         // in the correct z-order.
     99         if (i == childCount - 1) {
    100             return mSelectedTab;
    101         } else if (i >= mSelectedTab) {
    102             return i + 1;
    103         } else {
    104             return i;
    105         }
    106     }
    107 
    108     private void initTabWidget() {
    109         setOrientation(LinearLayout.HORIZONTAL);
    110         mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
    111 
    112         final Context context = mContext;
    113         final Resources resources = context.getResources();
    114 
    115         if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
    116             // Donut apps get old color scheme
    117             if (mLeftStrip == null) {
    118                 mLeftStrip = resources.getDrawable(
    119                         com.android.internal.R.drawable.tab_bottom_left_v4);
    120             }
    121             if (mRightStrip == null) {
    122                 mRightStrip = resources.getDrawable(
    123                         com.android.internal.R.drawable.tab_bottom_right_v4);
    124             }
    125         } else {
    126             // Use modern color scheme for Eclair and beyond
    127             if (mLeftStrip == null) {
    128                 mLeftStrip = resources.getDrawable(
    129                         com.android.internal.R.drawable.tab_bottom_left);
    130             }
    131             if (mRightStrip == null) {
    132                 mRightStrip = resources.getDrawable(
    133                         com.android.internal.R.drawable.tab_bottom_right);
    134             }
    135         }
    136 
    137         // Deal with focus, as we don't want the focus to go by default
    138         // to a tab other than the current tab
    139         setFocusable(true);
    140         setOnFocusChangeListener(this);
    141     }
    142 
    143     /**
    144      * Returns the tab indicator view at the given index.
    145      *
    146      * @param index the zero-based index of the tab indicator view to return
    147      * @return the tab indicator view at the given index
    148      */
    149     public View getChildTabViewAt(int index) {
    150         // If we are using dividers, then instead of tab views at 0, 1, 2, ...
    151         // we have tab views at 0, 2, 4, ...
    152         if (mDividerDrawable != null) {
    153             index *= 2;
    154         }
    155         return getChildAt(index);
    156     }
    157 
    158     /**
    159      * Returns the number of tab indicator views.
    160      * @return the number of tab indicator views.
    161      */
    162     public int getTabCount() {
    163         int children = getChildCount();
    164 
    165         // If we have dividers, then we will always have an odd number of
    166         // children: 1, 3, 5, ... and we want to convert that sequence to
    167         // this: 1, 2, 3, ...
    168         if (mDividerDrawable != null) {
    169             children = (children + 1) / 2;
    170         }
    171         return children;
    172     }
    173 
    174     /**
    175      * Sets the drawable to use as a divider between the tab indicators.
    176      * @param drawable the divider drawable
    177      */
    178     public void setDividerDrawable(Drawable drawable) {
    179         mDividerDrawable = drawable;
    180         requestLayout();
    181         invalidate();
    182     }
    183 
    184     /**
    185      * Sets the drawable to use as a divider between the tab indicators.
    186      * @param resId the resource identifier of the drawable to use as a
    187      * divider.
    188      */
    189     public void setDividerDrawable(int resId) {
    190         mDividerDrawable = mContext.getResources().getDrawable(resId);
    191         requestLayout();
    192         invalidate();
    193     }
    194 
    195     /**
    196      * Sets the drawable to use as the left part of the strip below the
    197      * tab indicators.
    198      * @param drawable the left strip drawable
    199      */
    200     public void setLeftStripDrawable(Drawable drawable) {
    201         mLeftStrip = drawable;
    202         requestLayout();
    203         invalidate();
    204     }
    205 
    206     /**
    207      * Sets the drawable to use as the left part of the strip below the
    208      * tab indicators.
    209      * @param resId the resource identifier of the drawable to use as the
    210      * left strip drawable
    211      */
    212     public void setLeftStripDrawable(int resId) {
    213         mLeftStrip = mContext.getResources().getDrawable(resId);
    214         requestLayout();
    215         invalidate();
    216     }
    217 
    218     /**
    219      * Sets the drawable to use as the right part of the strip below the
    220      * tab indicators.
    221      * @param drawable the right strip drawable
    222      */
    223     public void setRightStripDrawable(Drawable drawable) {
    224         mRightStrip = drawable;
    225         requestLayout();
    226         invalidate();    }
    227 
    228     /**
    229      * Sets the drawable to use as the right part of the strip below the
    230      * tab indicators.
    231      * @param resId the resource identifier of the drawable to use as the
    232      * right strip drawable
    233      */
    234     public void setRightStripDrawable(int resId) {
    235         mRightStrip = mContext.getResources().getDrawable(resId);
    236         requestLayout();
    237         invalidate();
    238     }
    239 
    240     /**
    241      * Controls whether the bottom strips on the tab indicators are drawn or
    242      * not.  The default is to draw them.  If the user specifies a custom
    243      * view for the tab indicators, then the TabHost class calls this method
    244      * to disable drawing of the bottom strips.
    245      * @param stripEnabled true if the bottom strips should be drawn.
    246      */
    247     public void setStripEnabled(boolean stripEnabled) {
    248         mDrawBottomStrips = stripEnabled;
    249         invalidate();
    250     }
    251 
    252     /**
    253      * Indicates whether the bottom strips on the tab indicators are drawn
    254      * or not.
    255      */
    256     public boolean isStripEnabled() {
    257         return mDrawBottomStrips;
    258     }
    259 
    260     @Override
    261     public void childDrawableStateChanged(View child) {
    262         if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
    263             // To make sure that the bottom strip is redrawn
    264             invalidate();
    265         }
    266         super.childDrawableStateChanged(child);
    267     }
    268 
    269     @Override
    270     public void dispatchDraw(Canvas canvas) {
    271         super.dispatchDraw(canvas);
    272 
    273         // Do nothing if there are no tabs.
    274         if (getTabCount() == 0) return;
    275 
    276         // If the user specified a custom view for the tab indicators, then
    277         // do not draw the bottom strips.
    278         if (!mDrawBottomStrips) {
    279             // Skip drawing the bottom strips.
    280             return;
    281         }
    282 
    283         final View selectedChild = getChildTabViewAt(mSelectedTab);
    284 
    285         final Drawable leftStrip = mLeftStrip;
    286         final Drawable rightStrip = mRightStrip;
    287 
    288         leftStrip.setState(selectedChild.getDrawableState());
    289         rightStrip.setState(selectedChild.getDrawableState());
    290 
    291         if (mStripMoved) {
    292             final Rect bounds = mBounds;
    293             bounds.left = selectedChild.getLeft();
    294             bounds.right = selectedChild.getRight();
    295             final int myHeight = getHeight();
    296             leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
    297                     myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
    298             rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
    299                     Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
    300             mStripMoved = false;
    301         }
    302 
    303         leftStrip.draw(canvas);
    304         rightStrip.draw(canvas);
    305     }
    306 
    307     /**
    308      * Sets the current tab.
    309      * This method is used to bring a tab to the front of the Widget,
    310      * and is used to post to the rest of the UI that a different tab
    311      * has been brought to the foreground.
    312      *
    313      * Note, this is separate from the traditional "focus" that is
    314      * employed from the view logic.
    315      *
    316      * For instance, if we have a list in a tabbed view, a user may be
    317      * navigating up and down the list, moving the UI focus (orange
    318      * highlighting) through the list items.  The cursor movement does
    319      * not effect the "selected" tab though, because what is being
    320      * scrolled through is all on the same tab.  The selected tab only
    321      * changes when we navigate between tabs (moving from the list view
    322      * to the next tabbed view, in this example).
    323      *
    324      * To move both the focus AND the selected tab at once, please use
    325      * {@link #setCurrentTab}. Normally, the view logic takes care of
    326      * adjusting the focus, so unless you're circumventing the UI,
    327      * you'll probably just focus your interest here.
    328      *
    329      *  @param index The tab that you want to indicate as the selected
    330      *  tab (tab brought to the front of the widget)
    331      *
    332      *  @see #focusCurrentTab
    333      */
    334     public void setCurrentTab(int index) {
    335         if (index < 0 || index >= getTabCount()) {
    336             return;
    337         }
    338 
    339         getChildTabViewAt(mSelectedTab).setSelected(false);
    340         mSelectedTab = index;
    341         getChildTabViewAt(mSelectedTab).setSelected(true);
    342         mStripMoved = true;
    343     }
    344 
    345     /**
    346      * Sets the current tab and focuses the UI on it.
    347      * This method makes sure that the focused tab matches the selected
    348      * tab, normally at {@link #setCurrentTab}.  Normally this would not
    349      * be an issue if we go through the UI, since the UI is responsible
    350      * for calling TabWidget.onFocusChanged(), but in the case where we
    351      * are selecting the tab programmatically, we'll need to make sure
    352      * focus keeps up.
    353      *
    354      *  @param index The tab that you want focused (highlighted in orange)
    355      *  and selected (tab brought to the front of the widget)
    356      *
    357      *  @see #setCurrentTab
    358      */
    359     public void focusCurrentTab(int index) {
    360         final int oldTab = mSelectedTab;
    361 
    362         // set the tab
    363         setCurrentTab(index);
    364 
    365         // change the focus if applicable.
    366         if (oldTab != index) {
    367             getChildTabViewAt(index).requestFocus();
    368         }
    369     }
    370 
    371     @Override
    372     public void setEnabled(boolean enabled) {
    373         super.setEnabled(enabled);
    374         int count = getTabCount();
    375 
    376         for (int i = 0; i < count; i++) {
    377             View child = getChildTabViewAt(i);
    378             child.setEnabled(enabled);
    379         }
    380     }
    381 
    382     @Override
    383     public void addView(View child) {
    384         if (child.getLayoutParams() == null) {
    385             final LinearLayout.LayoutParams lp = new LayoutParams(
    386                     0,
    387                     ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
    388             lp.setMargins(0, 0, 0, 0);
    389             child.setLayoutParams(lp);
    390         }
    391 
    392         // Ensure you can navigate to the tab with the keyboard, and you can touch it
    393         child.setFocusable(true);
    394         child.setClickable(true);
    395 
    396         // If we have dividers between the tabs and we already have at least one
    397         // tab, then add a divider before adding the next tab.
    398         if (mDividerDrawable != null && getTabCount() > 0) {
    399             ImageView divider = new ImageView(mContext);
    400             final LinearLayout.LayoutParams lp = new LayoutParams(
    401                     mDividerDrawable.getIntrinsicWidth(),
    402                     LayoutParams.MATCH_PARENT);
    403             lp.setMargins(0, 0, 0, 0);
    404             divider.setLayoutParams(lp);
    405             divider.setBackgroundDrawable(mDividerDrawable);
    406             super.addView(divider);
    407         }
    408         super.addView(child);
    409 
    410         // TODO: detect this via geometry with a tabwidget listener rather
    411         // than potentially interfere with the view's listener
    412         child.setOnClickListener(new TabClickListener(getTabCount() - 1));
    413         child.setOnFocusChangeListener(this);
    414     }
    415 
    416     /**
    417      * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
    418      */
    419     void setTabSelectionListener(OnTabSelectionChanged listener) {
    420         mSelectionChangedListener = listener;
    421     }
    422 
    423     public void onFocusChange(View v, boolean hasFocus) {
    424         if (v == this && hasFocus && getTabCount() > 0) {
    425             getChildTabViewAt(mSelectedTab).requestFocus();
    426             return;
    427         }
    428 
    429         if (hasFocus) {
    430             int i = 0;
    431             int numTabs = getTabCount();
    432             while (i < numTabs) {
    433                 if (getChildTabViewAt(i) == v) {
    434                     setCurrentTab(i);
    435                     mSelectionChangedListener.onTabSelectionChanged(i, false);
    436                     break;
    437                 }
    438                 i++;
    439             }
    440         }
    441     }
    442 
    443     // registered with each tab indicator so we can notify tab host
    444     private class TabClickListener implements OnClickListener {
    445 
    446         private final int mTabIndex;
    447 
    448         private TabClickListener(int tabIndex) {
    449             mTabIndex = tabIndex;
    450         }
    451 
    452         public void onClick(View v) {
    453             mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
    454         }
    455     }
    456 
    457     /**
    458      * Let {@link TabHost} know that the user clicked on a tab indicator.
    459      */
    460     static interface OnTabSelectionChanged {
    461         /**
    462          * Informs the TabHost which tab was selected. It also indicates
    463          * if the tab was clicked/pressed or just focused into.
    464          *
    465          * @param tabIndex index of the tab that was selected
    466          * @param clicked whether the selection changed due to a touch/click
    467          * or due to focus entering the tab through navigation. Pass true
    468          * if it was due to a press/click and false otherwise.
    469          */
    470         void onTabSelectionChanged(int tabIndex, boolean clicked);
    471     }
    472 
    473 }
    474 
    475