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