TabWidget.java revision 11f4a48c54f3006778c874662ff04a4d9d157f25
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 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            return getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event);
409        }
410        return false;
411    }
412
413    @Override
414    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
415        super.onInitializeAccessibilityEvent(event);
416        event.setItemCount(getTabCount());
417        event.setCurrentItemIndex(mSelectedTab);
418    }
419
420    /**
421     * Sets the current tab and focuses the UI on it.
422     * This method makes sure that the focused tab matches the selected
423     * tab, normally at {@link #setCurrentTab}.  Normally this would not
424     * be an issue if we go through the UI, since the UI is responsible
425     * for calling TabWidget.onFocusChanged(), but in the case where we
426     * are selecting the tab programmatically, we'll need to make sure
427     * focus keeps up.
428     *
429     *  @param index The tab that you want focused (highlighted in orange)
430     *  and selected (tab brought to the front of the widget)
431     *
432     *  @see #setCurrentTab
433     */
434    public void focusCurrentTab(int index) {
435        final int oldTab = mSelectedTab;
436
437        // set the tab
438        setCurrentTab(index);
439
440        // change the focus if applicable.
441        if (oldTab != index) {
442            getChildTabViewAt(index).requestFocus();
443        }
444    }
445
446    @Override
447    public void setEnabled(boolean enabled) {
448        super.setEnabled(enabled);
449
450        final int count = getTabCount();
451        for (int i = 0; i < count; i++) {
452            View child = getChildTabViewAt(i);
453            child.setEnabled(enabled);
454        }
455    }
456
457    @Override
458    public void addView(View child) {
459        if (child.getLayoutParams() == null) {
460            final LinearLayout.LayoutParams lp = new LayoutParams(
461                    0,
462                    ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
463            lp.setMargins(0, 0, 0, 0);
464            child.setLayoutParams(lp);
465        }
466
467        // Ensure you can navigate to the tab with the keyboard, and you can touch it
468        child.setFocusable(true);
469        child.setClickable(true);
470
471        super.addView(child);
472
473        // TODO: detect this via geometry with a tabwidget listener rather
474        // than potentially interfere with the view's listener
475        child.setOnClickListener(new TabClickListener(getTabCount() - 1));
476        child.setOnFocusChangeListener(this);
477    }
478
479    @Override
480    public void removeAllViews() {
481        super.removeAllViews();
482        mSelectedTab = -1;
483    }
484
485    @Override
486    public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
487        // this class fires events only when tabs are focused or selected
488        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) {
489            return;
490        }
491        super.sendAccessibilityEventUnchecked(event);
492    }
493
494    /**
495     * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
496     */
497    void setTabSelectionListener(OnTabSelectionChanged listener) {
498        mSelectionChangedListener = listener;
499    }
500
501    /** {@inheritDoc} */
502    public void onFocusChange(View v, boolean hasFocus) {
503        if (v == this && hasFocus && getTabCount() > 0) {
504            getChildTabViewAt(mSelectedTab).requestFocus();
505            return;
506        }
507
508        if (hasFocus) {
509            int i = 0;
510            int numTabs = getTabCount();
511            while (i < numTabs) {
512                if (getChildTabViewAt(i) == v) {
513                    setCurrentTab(i);
514                    mSelectionChangedListener.onTabSelectionChanged(i, false);
515                    if (isShown()) {
516                        // a tab is focused so send an event to announce the tab widget state
517                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
518                    }
519                    break;
520                }
521                i++;
522            }
523        }
524    }
525
526    // registered with each tab indicator so we can notify tab host
527    private class TabClickListener implements OnClickListener {
528
529        private final int mTabIndex;
530
531        private TabClickListener(int tabIndex) {
532            mTabIndex = tabIndex;
533        }
534
535        public void onClick(View v) {
536            mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
537        }
538    }
539
540    /**
541     * Let {@link TabHost} know that the user clicked on a tab indicator.
542     */
543    static interface OnTabSelectionChanged {
544        /**
545         * Informs the TabHost which tab was selected. It also indicates
546         * if the tab was clicked/pressed or just focused into.
547         *
548         * @param tabIndex index of the tab that was selected
549         * @param clicked whether the selection changed due to a touch/click
550         * or due to focus entering the tab through navigation. Pass true
551         * if it was due to a press/click and false otherwise.
552         */
553        void onTabSelectionChanged(int tabIndex, boolean clicked);
554    }
555}
556