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