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