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