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;
32import android.view.accessibility.AccessibilityNodeInfo;
33
34/**
35 *
36 * Displays a list of tab labels representing each page in the parent's tab
37 * collection. The container object for this widget is
38 * {@link android.widget.TabHost TabHost}. When the user selects a tab, this
39 * object sends a message to the parent container, TabHost, to tell it to switch
40 * the displayed page. You typically won't use many methods directly on this
41 * object. The container TabHost is used to add labels, add the callback
42 * handler, and manage callbacks. You might call this object to iterate the list
43 * of tabs, or to tweak the layout of the tab list, but most methods should be
44 * called on the containing TabHost object.
45 *
46 * @attr ref android.R.styleable#TabWidget_divider
47 * @attr ref android.R.styleable#TabWidget_tabStripEnabled
48 * @attr ref android.R.styleable#TabWidget_tabStripLeft
49 * @attr ref android.R.styleable#TabWidget_tabStripRight
50 */
51public class TabWidget extends LinearLayout implements OnFocusChangeListener {
52    private OnTabSelectionChanged mSelectionChangedListener;
53
54    // This value will be set to 0 as soon as the first tab is added to TabHost.
55    private int mSelectedTab = -1;
56
57    private Drawable mLeftStrip;
58    private Drawable mRightStrip;
59
60    private boolean mDrawBottomStrips = true;
61    private boolean mStripMoved;
62
63    private final Rect mBounds = new Rect();
64
65    // When positive, the widths and heights of tabs will be imposed so that they fit in parent
66    private int mImposedTabsHeight = -1;
67    private int[] mImposedTabWidths;
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, defStyle);
79
80        final TypedArray a = context.obtainStyledAttributes(
81                attrs, com.android.internal.R.styleable.TabWidget, defStyle, 0);
82
83        setStripEnabled(a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true));
84        setLeftStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripLeft));
85        setRightStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripRight));
86
87        a.recycle();
88
89        initTabWidget();
90    }
91
92    @Override
93    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
94        mStripMoved = true;
95        super.onSizeChanged(w, h, oldw, oldh);
96    }
97
98    @Override
99    protected int getChildDrawingOrder(int childCount, int i) {
100        if (mSelectedTab == -1) {
101            return i;
102        } else {
103            // Always draw the selected tab last, so that drop shadows are drawn
104            // in the correct z-order.
105            if (i == childCount - 1) {
106                return mSelectedTab;
107            } else if (i >= mSelectedTab) {
108                return i + 1;
109            } else {
110                return i;
111            }
112        }
113    }
114
115    private void initTabWidget() {
116        setChildrenDrawingOrderEnabled(true);
117
118        final Context context = mContext;
119        final Resources resources = context.getResources();
120
121        // Tests the target Sdk version, as set in the Manifest. Could not be set using styles.xml
122        // in a values-v? directory which targets the current platform Sdk version instead.
123        if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
124            // Donut apps get old color scheme
125            if (mLeftStrip == null) {
126                mLeftStrip = resources.getDrawable(
127                        com.android.internal.R.drawable.tab_bottom_left_v4);
128            }
129            if (mRightStrip == null) {
130                mRightStrip = resources.getDrawable(
131                        com.android.internal.R.drawable.tab_bottom_right_v4);
132            }
133        } else {
134            // Use modern color scheme for Eclair and beyond
135            if (mLeftStrip == null) {
136                mLeftStrip = resources.getDrawable(
137                        com.android.internal.R.drawable.tab_bottom_left);
138            }
139            if (mRightStrip == null) {
140                mRightStrip = resources.getDrawable(
141                        com.android.internal.R.drawable.tab_bottom_right);
142            }
143        }
144
145        // Deal with focus, as we don't want the focus to go by default
146        // to a tab other than the current tab
147        setFocusable(true);
148        setOnFocusChangeListener(this);
149    }
150
151    @Override
152    void measureChildBeforeLayout(View child, int childIndex,
153            int widthMeasureSpec, int totalWidth,
154            int heightMeasureSpec, int totalHeight) {
155        if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) {
156            widthMeasureSpec = MeasureSpec.makeMeasureSpec(
157                    totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY);
158            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight,
159                    MeasureSpec.EXACTLY);
160        }
161
162        super.measureChildBeforeLayout(child, childIndex,
163                widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
164    }
165
166    @Override
167    void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
168        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
169            super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
170            return;
171        }
172
173        // First, measure with no constraint
174        final int unspecifiedWidth = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
175        mImposedTabsHeight = -1;
176        super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
177
178        int extraWidth = getMeasuredWidth() - MeasureSpec.getSize(widthMeasureSpec);
179        if (extraWidth > 0) {
180            final int count = getChildCount();
181
182            int childCount = 0;
183            for (int i = 0; i < count; i++) {
184                final View child = getChildAt(i);
185                if (child.getVisibility() == GONE) continue;
186                childCount++;
187            }
188
189            if (childCount > 0) {
190                if (mImposedTabWidths == null || mImposedTabWidths.length != count) {
191                    mImposedTabWidths = new int[count];
192                }
193                for (int i = 0; i < count; i++) {
194                    final View child = getChildAt(i);
195                    if (child.getVisibility() == GONE) continue;
196                    final int childWidth = child.getMeasuredWidth();
197                    final int delta = extraWidth / childCount;
198                    final int newWidth = Math.max(0, childWidth - delta);
199                    mImposedTabWidths[i] = newWidth;
200                    // Make sure the extra width is evenly distributed, no int division remainder
201                    extraWidth -= childWidth - newWidth; // delta may have been clamped
202                    childCount--;
203                    mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight());
204                }
205            }
206        }
207
208        // Measure again, this time with imposed tab widths and respecting initial spec request
209        super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
210    }
211
212    /**
213     * Returns the tab indicator view at the given index.
214     *
215     * @param index the zero-based index of the tab indicator view to return
216     * @return the tab indicator view at the given index
217     */
218    public View getChildTabViewAt(int index) {
219        return getChildAt(index);
220    }
221
222    /**
223     * Returns the number of tab indicator views.
224     * @return the number of tab indicator views.
225     */
226    public int getTabCount() {
227        return getChildCount();
228    }
229
230    /**
231     * Sets the drawable to use as a divider between the tab indicators.
232     * @param drawable the divider drawable
233     */
234    @Override
235    public void setDividerDrawable(Drawable drawable) {
236        super.setDividerDrawable(drawable);
237    }
238
239    /**
240     * Sets the drawable to use as a divider between the tab indicators.
241     * @param resId the resource identifier of the drawable to use as a
242     * divider.
243     */
244    public void setDividerDrawable(int resId) {
245        setDividerDrawable(getResources().getDrawable(resId));
246    }
247
248    /**
249     * Sets the drawable to use as the left part of the strip below the
250     * tab indicators.
251     * @param drawable the left strip drawable
252     */
253    public void setLeftStripDrawable(Drawable drawable) {
254        mLeftStrip = drawable;
255        requestLayout();
256        invalidate();
257    }
258
259    /**
260     * Sets the drawable to use as the left part of the strip below the
261     * tab indicators.
262     * @param resId the resource identifier of the drawable to use as the
263     * left strip drawable
264     */
265    public void setLeftStripDrawable(int resId) {
266        setLeftStripDrawable(getResources().getDrawable(resId));
267    }
268
269    /**
270     * Sets the drawable to use as the right part of the strip below the
271     * tab indicators.
272     * @param drawable the right strip drawable
273     */
274    public void setRightStripDrawable(Drawable drawable) {
275        mRightStrip = drawable;
276        requestLayout();
277        invalidate();
278    }
279
280    /**
281     * Sets the drawable to use as the right part of the strip below the
282     * tab indicators.
283     * @param resId the resource identifier of the drawable to use as the
284     * right strip drawable
285     */
286    public void setRightStripDrawable(int resId) {
287        setRightStripDrawable(getResources().getDrawable(resId));
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        onPopulateAccessibilityEvent(event);
404        // Dispatch only to the selected tab.
405        if (mSelectedTab != -1) {
406            View tabView = getChildTabViewAt(mSelectedTab);
407            if (tabView != null && tabView.getVisibility() == VISIBLE) {
408                return tabView.dispatchPopulateAccessibilityEvent(event);
409            }
410        }
411        return false;
412    }
413
414    @Override
415    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
416        super.onInitializeAccessibilityEvent(event);
417        event.setClassName(TabWidget.class.getName());
418        event.setItemCount(getTabCount());
419        event.setCurrentItemIndex(mSelectedTab);
420    }
421
422
423    @Override
424    public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
425        // this class fires events only when tabs are focused or selected
426        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) {
427            event.recycle();
428            return;
429        }
430        super.sendAccessibilityEventUnchecked(event);
431    }
432
433    @Override
434    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
435        super.onInitializeAccessibilityNodeInfo(info);
436        info.setClassName(TabWidget.class.getName());
437    }
438
439    /**
440     * Sets the current tab and focuses the UI on it.
441     * This method makes sure that the focused tab matches the selected
442     * tab, normally at {@link #setCurrentTab}.  Normally this would not
443     * be an issue if we go through the UI, since the UI is responsible
444     * for calling TabWidget.onFocusChanged(), but in the case where we
445     * are selecting the tab programmatically, we'll need to make sure
446     * focus keeps up.
447     *
448     *  @param index The tab that you want focused (highlighted in orange)
449     *  and selected (tab brought to the front of the widget)
450     *
451     *  @see #setCurrentTab
452     */
453    public void focusCurrentTab(int index) {
454        final int oldTab = mSelectedTab;
455
456        // set the tab
457        setCurrentTab(index);
458
459        // change the focus if applicable.
460        if (oldTab != index) {
461            getChildTabViewAt(index).requestFocus();
462        }
463    }
464
465    @Override
466    public void setEnabled(boolean enabled) {
467        super.setEnabled(enabled);
468
469        final int count = getTabCount();
470        for (int i = 0; i < count; i++) {
471            View child = getChildTabViewAt(i);
472            child.setEnabled(enabled);
473        }
474    }
475
476    @Override
477    public void addView(View child) {
478        if (child.getLayoutParams() == null) {
479            final LinearLayout.LayoutParams lp = new LayoutParams(
480                    0,
481                    ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
482            lp.setMargins(0, 0, 0, 0);
483            child.setLayoutParams(lp);
484        }
485
486        // Ensure you can navigate to the tab with the keyboard, and you can touch it
487        child.setFocusable(true);
488        child.setClickable(true);
489
490        super.addView(child);
491
492        // TODO: detect this via geometry with a tabwidget listener rather
493        // than potentially interfere with the view's listener
494        child.setOnClickListener(new TabClickListener(getTabCount() - 1));
495        child.setOnFocusChangeListener(this);
496    }
497
498    @Override
499    public void removeAllViews() {
500        super.removeAllViews();
501        mSelectedTab = -1;
502    }
503
504    /**
505     * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
506     */
507    void setTabSelectionListener(OnTabSelectionChanged listener) {
508        mSelectionChangedListener = listener;
509    }
510
511    /** {@inheritDoc} */
512    public void onFocusChange(View v, boolean hasFocus) {
513        if (v == this && hasFocus && getTabCount() > 0) {
514            getChildTabViewAt(mSelectedTab).requestFocus();
515            return;
516        }
517
518        if (hasFocus) {
519            int i = 0;
520            int numTabs = getTabCount();
521            while (i < numTabs) {
522                if (getChildTabViewAt(i) == v) {
523                    setCurrentTab(i);
524                    mSelectionChangedListener.onTabSelectionChanged(i, false);
525                    if (isShown()) {
526                        // a tab is focused so send an event to announce the tab widget state
527                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
528                    }
529                    break;
530                }
531                i++;
532            }
533        }
534    }
535
536    // registered with each tab indicator so we can notify tab host
537    private class TabClickListener implements OnClickListener {
538
539        private final int mTabIndex;
540
541        private TabClickListener(int tabIndex) {
542            mTabIndex = tabIndex;
543        }
544
545        public void onClick(View v) {
546            mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
547        }
548    }
549
550    /**
551     * Let {@link TabHost} know that the user clicked on a tab indicator.
552     */
553    static interface OnTabSelectionChanged {
554        /**
555         * Informs the TabHost which tab was selected. It also indicates
556         * if the tab was clicked/pressed or just focused into.
557         *
558         * @param tabIndex index of the tab that was selected
559         * @param clicked whether the selection changed due to a touch/click
560         * or due to focus entering the tab through navigation. Pass true
561         * if it was due to a press/click and false otherwise.
562         */
563        void onTabSelectionChanged(int tabIndex, boolean clicked);
564    }
565}
566