TabWidget.java revision 6741c941a76d17c257c396d86c7561aa43005b1a
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
17package android.widget;
18
19import android.R;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.os.Build;
27import android.util.AttributeSet;
28import android.view.View;
29import android.view.View.OnFocusChangeListener;
30import android.view.ViewGroup;
31import 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 */
53public 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 Drawable mDividerDrawable;
66
67    private final Rect mBounds = new Rect();
68
69    public TabWidget(Context context) {
70        this(context, null);
71    }
72
73    public TabWidget(Context context, AttributeSet attrs) {
74        this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
75    }
76
77    public TabWidget(Context context, AttributeSet attrs, int defStyle) {
78        super(context, attrs);
79
80        TypedArray a =
81            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget,
82                    defStyle, 0);
83
84        mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true);
85        mDividerDrawable = a.getDrawable(R.styleable.TabWidget_divider);
86        mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
87        mRightStrip = 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        mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
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        }
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     * {@inheritDoc}
145     */
146    @Override
147    void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
148        // First measure with no constraint
149        final int unspecifiedWidth = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
150        super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
151
152        final int count = getChildCount();
153        int totalWidth = 0;
154        int totalCount = 0;
155        for (int i = 0; i < count; i++) {
156            final View child = getChildAt(i);
157            if (child.getVisibility() == GONE) {
158                continue;
159            }
160            final int childWidth = child.getMeasuredWidth();
161            totalWidth += childWidth;
162            totalCount++;
163        }
164
165        final int width = MeasureSpec.getSize(widthMeasureSpec);
166        if (totalWidth > width && totalCount > 0) {
167            int extraWidth = totalWidth - width;
168            for (int i = 0; i < count; i++) {
169                final View child = getChildAt(i);
170                if (child.getVisibility() == GONE) {
171                    continue;
172                }
173                final int childWidth = child.getMeasuredWidth();
174                final int delta = extraWidth / totalCount;
175                final int tabWidth = Math.max(0, childWidth - delta);
176
177                final int tabWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
178                        tabWidth, MeasureSpec.EXACTLY);
179                final int tabHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
180                        child.getMeasuredHeight(), MeasureSpec.EXACTLY);
181
182                child.measure(tabWidthMeasureSpec, tabHeightMeasureSpec);
183
184                // Make sure the extra width is evenly distributed, avoiding int division remainder
185                extraWidth -= delta;
186                totalCount--;
187            }
188            setMeasuredDimension(width, getMeasuredHeight());
189        }
190    }
191
192    /**
193     * Returns the tab indicator view at the given index.
194     *
195     * @param index the zero-based index of the tab indicator view to return
196     * @return the tab indicator view at the given index
197     */
198    public View getChildTabViewAt(int index) {
199        // If we are using dividers, then instead of tab views at 0, 1, 2, ...
200        // we have tab views at 0, 2, 4, ...
201        if (mDividerDrawable != null) {
202            index *= 2;
203        }
204        return getChildAt(index);
205    }
206
207    /**
208     * Returns the number of tab indicator views.
209     * @return the number of tab indicator views.
210     */
211    public int getTabCount() {
212        int children = getChildCount();
213
214        // If we have dividers, then we will always have an odd number of
215        // children: 1, 3, 5, ... and we want to convert that sequence to
216        // this: 1, 2, 3, ...
217        if (mDividerDrawable != null) {
218            children = (children + 1) / 2;
219        }
220        return children;
221    }
222
223    /**
224     * Sets the drawable to use as a divider between the tab indicators.
225     * @param drawable the divider drawable
226     */
227    @Override
228    public void setDividerDrawable(Drawable drawable) {
229        mDividerDrawable = drawable;
230        requestLayout();
231        invalidate();
232    }
233
234    /**
235     * Sets the drawable to use as a divider between the tab indicators.
236     * @param resId the resource identifier of the drawable to use as a
237     * divider.
238     */
239    public void setDividerDrawable(int resId) {
240        mDividerDrawable = mContext.getResources().getDrawable(resId);
241        requestLayout();
242        invalidate();
243    }
244
245    /**
246     * Sets the drawable to use as the left part of the strip below the
247     * tab indicators.
248     * @param drawable the left strip drawable
249     */
250    public void setLeftStripDrawable(Drawable drawable) {
251        mLeftStrip = drawable;
252        requestLayout();
253        invalidate();
254    }
255
256    /**
257     * Sets the drawable to use as the left part of the strip below the
258     * tab indicators.
259     * @param resId the resource identifier of the drawable to use as the
260     * left strip drawable
261     */
262    public void setLeftStripDrawable(int resId) {
263        mLeftStrip = mContext.getResources().getDrawable(resId);
264        requestLayout();
265        invalidate();
266    }
267
268    /**
269     * Sets the drawable to use as the right part of the strip below the
270     * tab indicators.
271     * @param drawable the right strip drawable
272     */
273    public void setRightStripDrawable(Drawable drawable) {
274        mRightStrip = drawable;
275        requestLayout();
276        invalidate();    }
277
278    /**
279     * Sets the drawable to use as the right part of the strip below the
280     * tab indicators.
281     * @param resId the resource identifier of the drawable to use as the
282     * right strip drawable
283     */
284    public void setRightStripDrawable(int resId) {
285        mRightStrip = mContext.getResources().getDrawable(resId);
286        requestLayout();
287        invalidate();
288    }
289
290    /**
291     * Controls whether the bottom strips on the tab indicators are drawn or
292     * not.  The default is to draw them.  If the user specifies a custom
293     * view for the tab indicators, then the TabHost class calls this method
294     * to disable drawing of the bottom strips.
295     * @param stripEnabled true if the bottom strips should be drawn.
296     */
297    public void setStripEnabled(boolean stripEnabled) {
298        mDrawBottomStrips = stripEnabled;
299        invalidate();
300    }
301
302    /**
303     * Indicates whether the bottom strips on the tab indicators are drawn
304     * or not.
305     */
306    public boolean isStripEnabled() {
307        return mDrawBottomStrips;
308    }
309
310    @Override
311    public void childDrawableStateChanged(View child) {
312        if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
313            // To make sure that the bottom strip is redrawn
314            invalidate();
315        }
316        super.childDrawableStateChanged(child);
317    }
318
319    @Override
320    public void dispatchDraw(Canvas canvas) {
321        super.dispatchDraw(canvas);
322
323        // Do nothing if there are no tabs.
324        if (getTabCount() == 0) return;
325
326        // If the user specified a custom view for the tab indicators, then
327        // do not draw the bottom strips.
328        if (!mDrawBottomStrips) {
329            // Skip drawing the bottom strips.
330            return;
331        }
332
333        final View selectedChild = getChildTabViewAt(mSelectedTab);
334
335        final Drawable leftStrip = mLeftStrip;
336        final Drawable rightStrip = mRightStrip;
337
338        leftStrip.setState(selectedChild.getDrawableState());
339        rightStrip.setState(selectedChild.getDrawableState());
340
341        if (mStripMoved) {
342            final Rect bounds = mBounds;
343            bounds.left = selectedChild.getLeft();
344            bounds.right = selectedChild.getRight();
345            final int myHeight = getHeight();
346            leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
347                    myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
348            rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
349                    Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
350            mStripMoved = false;
351        }
352
353        leftStrip.draw(canvas);
354        rightStrip.draw(canvas);
355    }
356
357    /**
358     * Sets the current tab.
359     * This method is used to bring a tab to the front of the Widget,
360     * and is used to post to the rest of the UI that a different tab
361     * has been brought to the foreground.
362     *
363     * Note, this is separate from the traditional "focus" that is
364     * employed from the view logic.
365     *
366     * For instance, if we have a list in a tabbed view, a user may be
367     * navigating up and down the list, moving the UI focus (orange
368     * highlighting) through the list items.  The cursor movement does
369     * not effect the "selected" tab though, because what is being
370     * scrolled through is all on the same tab.  The selected tab only
371     * changes when we navigate between tabs (moving from the list view
372     * to the next tabbed view, in this example).
373     *
374     * To move both the focus AND the selected tab at once, please use
375     * {@link #setCurrentTab}. Normally, the view logic takes care of
376     * adjusting the focus, so unless you're circumventing the UI,
377     * you'll probably just focus your interest here.
378     *
379     *  @param index The tab that you want to indicate as the selected
380     *  tab (tab brought to the front of the widget)
381     *
382     *  @see #focusCurrentTab
383     */
384    public void setCurrentTab(int index) {
385        if (index < 0 || index >= getTabCount() || index == mSelectedTab) {
386            return;
387        }
388
389        if (mSelectedTab != -1) {
390            getChildTabViewAt(mSelectedTab).setSelected(false);
391        }
392        mSelectedTab = index;
393        getChildTabViewAt(mSelectedTab).setSelected(true);
394        mStripMoved = true;
395
396        if (isShown()) {
397            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
398        }
399    }
400
401    @Override
402    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
403        event.setItemCount(getTabCount());
404        event.setCurrentItemIndex(mSelectedTab);
405        if (mSelectedTab != -1) {
406            getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event);
407        }
408        return true;
409    }
410
411    /**
412     * Sets the current tab and focuses the UI on it.
413     * This method makes sure that the focused tab matches the selected
414     * tab, normally at {@link #setCurrentTab}.  Normally this would not
415     * be an issue if we go through the UI, since the UI is responsible
416     * for calling TabWidget.onFocusChanged(), but in the case where we
417     * are selecting the tab programmatically, we'll need to make sure
418     * focus keeps up.
419     *
420     *  @param index The tab that you want focused (highlighted in orange)
421     *  and selected (tab brought to the front of the widget)
422     *
423     *  @see #setCurrentTab
424     */
425    public void focusCurrentTab(int index) {
426        final int oldTab = mSelectedTab;
427
428        // set the tab
429        setCurrentTab(index);
430
431        // change the focus if applicable.
432        if (oldTab != index) {
433            getChildTabViewAt(index).requestFocus();
434        }
435    }
436
437    @Override
438    public void setEnabled(boolean enabled) {
439        super.setEnabled(enabled);
440        int count = getTabCount();
441
442        for (int i = 0; i < count; i++) {
443            View child = getChildTabViewAt(i);
444            child.setEnabled(enabled);
445        }
446    }
447
448    @Override
449    public void addView(View child) {
450        if (child.getLayoutParams() == null) {
451            final LinearLayout.LayoutParams lp = new LayoutParams(
452                    0,
453                    ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
454            lp.setMargins(0, 0, 0, 0);
455            child.setLayoutParams(lp);
456        }
457
458        // Ensure you can navigate to the tab with the keyboard, and you can touch it
459        child.setFocusable(true);
460        child.setClickable(true);
461
462        // If we have dividers between the tabs and we already have at least one
463        // tab, then add a divider before adding the next tab.
464        if (mDividerDrawable != null && getTabCount() > 0) {
465            ImageView divider = new ImageView(mContext);
466            final LinearLayout.LayoutParams lp = new LayoutParams(
467                    mDividerDrawable.getIntrinsicWidth(),
468                    LayoutParams.MATCH_PARENT);
469            lp.setMargins(0, 0, 0, 0);
470            divider.setLayoutParams(lp);
471            divider.setBackgroundDrawable(mDividerDrawable);
472            super.addView(divider);
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 sendAccessibilityEventUnchecked(AccessibilityEvent event) {
484        // this class fires events only when tabs are focused or selected
485        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) {
486            return;
487        }
488        super.sendAccessibilityEventUnchecked(event);
489    }
490
491    /**
492     * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
493     */
494    void setTabSelectionListener(OnTabSelectionChanged listener) {
495        mSelectionChangedListener = listener;
496    }
497
498    public void onFocusChange(View v, boolean hasFocus) {
499        if (v == this && hasFocus && getTabCount() > 0) {
500            getChildTabViewAt(mSelectedTab).requestFocus();
501            return;
502        }
503
504        if (hasFocus) {
505            int i = 0;
506            int numTabs = getTabCount();
507            while (i < numTabs) {
508                if (getChildTabViewAt(i) == v) {
509                    setCurrentTab(i);
510                    mSelectionChangedListener.onTabSelectionChanged(i, false);
511                    if (isShown()) {
512                        // a tab is focused so send an event to announce the tab widget state
513                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
514                    }
515                    break;
516                }
517                i++;
518            }
519        }
520    }
521
522    // registered with each tab indicator so we can notify tab host
523    private class TabClickListener implements OnClickListener {
524
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     * Let {@link TabHost} know that the user clicked on a tab indicator.
538     */
539    static 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
546         * or due to focus entering the tab through navigation. Pass true
547         * if it was due to a press/click and false otherwise.
548         */
549        void onTabSelectionChanged(int tabIndex, boolean clicked);
550    }
551
552}
553
554