TabWidget.java revision 41c738849c1fdf29357048ec9f5e48143dbb026a
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.ViewGroup;
30import android.view.View.OnFocusChangeListener;
31
32/**
33 *
34 * Displays a list of tab labels representing each page in the parent's tab
35 * collection. The container object for this widget is
36 * {@link android.widget.TabHost TabHost}. When the user selects a tab, this
37 * object sends a message to the parent container, TabHost, to tell it to switch
38 * the displayed page. You typically won't use many methods directly on this
39 * object. The container TabHost is used to add labels, add the callback
40 * handler, and manage callbacks. You might call this object to iterate the list
41 * of tabs, or to tweak the layout of the tab list, but most methods should be
42 * called on the containing TabHost object.
43 *
44 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-tabwidget.html">Tab Layout
45 * tutorial</a>.</p>
46 *
47 * @attr ref android.R.styleable#TabWidget_divider
48 * @attr ref android.R.styleable#TabWidget_tabStripEnabled
49 * @attr ref android.R.styleable#TabWidget_tabStripLeft
50 * @attr ref android.R.styleable#TabWidget_tabStripRight
51 */
52public class TabWidget extends LinearLayout implements OnFocusChangeListener {
53    private OnTabSelectionChanged mSelectionChangedListener;
54
55    private int mSelectedTab = 0;
56
57    private Drawable mLeftStrip;
58    private Drawable mRightStrip;
59
60    private boolean mDrawBottomStrips = true;
61    private boolean mStripMoved;
62
63    private Drawable mDividerDrawable;
64
65    private final Rect mBounds = new Rect();
66
67    public TabWidget(Context context) {
68        this(context, null);
69    }
70
71    public TabWidget(Context context, AttributeSet attrs) {
72        this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
73    }
74
75    public TabWidget(Context context, AttributeSet attrs, int defStyle) {
76        super(context, attrs);
77
78        TypedArray a =
79            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget,
80                    defStyle, 0);
81
82        mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true);
83        mDividerDrawable = a.getDrawable(R.styleable.TabWidget_divider);
84        mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
85        mRightStrip = 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        // Always draw the selected tab last, so that drop shadows are drawn
101        // in the correct z-order.
102        if (i == childCount - 1) {
103            return mSelectedTab;
104        } else if (i >= mSelectedTab) {
105            return i + 1;
106        } else {
107            return i;
108        }
109    }
110
111    private void initTabWidget() {
112        mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
113
114        final Context context = mContext;
115        final Resources resources = context.getResources();
116
117        if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
118            // Donut apps get old color scheme
119            if (mLeftStrip == null) {
120                mLeftStrip = resources.getDrawable(
121                        com.android.internal.R.drawable.tab_bottom_left_v4);
122            }
123            if (mRightStrip == null) {
124                mRightStrip = resources.getDrawable(
125                        com.android.internal.R.drawable.tab_bottom_right_v4);
126            }
127        } else {
128            // Use modern color scheme for Eclair and beyond
129            if (mLeftStrip == null) {
130                mLeftStrip = resources.getDrawable(
131                        com.android.internal.R.drawable.tab_bottom_left);
132            }
133            if (mRightStrip == null) {
134                mRightStrip = resources.getDrawable(
135                        com.android.internal.R.drawable.tab_bottom_right);
136            }
137        }
138
139        // Deal with focus, as we don't want the focus to go by default
140        // to a tab other than the current tab
141        setFocusable(true);
142        setOnFocusChangeListener(this);
143    }
144
145    /**
146     * Returns the tab indicator view at the given index.
147     *
148     * @param index the zero-based index of the tab indicator view to return
149     * @return the tab indicator view at the given index
150     */
151    public View getChildTabViewAt(int index) {
152        // If we are using dividers, then instead of tab views at 0, 1, 2, ...
153        // we have tab views at 0, 2, 4, ...
154        if (mDividerDrawable != null) {
155            index *= 2;
156        }
157        return getChildAt(index);
158    }
159
160    /**
161     * Returns the number of tab indicator views.
162     * @return the number of tab indicator views.
163     */
164    public int getTabCount() {
165        int children = getChildCount();
166
167        // If we have dividers, then we will always have an odd number of
168        // children: 1, 3, 5, ... and we want to convert that sequence to
169        // this: 1, 2, 3, ...
170        if (mDividerDrawable != null) {
171            children = (children + 1) / 2;
172        }
173        return children;
174    }
175
176    /**
177     * Sets the drawable to use as a divider between the tab indicators.
178     * @param drawable the divider drawable
179     */
180    public void setDividerDrawable(Drawable drawable) {
181        mDividerDrawable = drawable;
182        requestLayout();
183        invalidate();
184    }
185
186    /**
187     * Sets the drawable to use as a divider between the tab indicators.
188     * @param resId the resource identifier of the drawable to use as a
189     * divider.
190     */
191    public void setDividerDrawable(int resId) {
192        mDividerDrawable = mContext.getResources().getDrawable(resId);
193        requestLayout();
194        invalidate();
195    }
196
197    /**
198     * Sets the drawable to use as the left part of the strip below the
199     * tab indicators.
200     * @param drawable the left strip drawable
201     */
202    public void setLeftStripDrawable(Drawable drawable) {
203        mLeftStrip = drawable;
204        requestLayout();
205        invalidate();
206    }
207
208    /**
209     * Sets the drawable to use as the left part of the strip below the
210     * tab indicators.
211     * @param resId the resource identifier of the drawable to use as the
212     * left strip drawable
213     */
214    public void setLeftStripDrawable(int resId) {
215        mLeftStrip = mContext.getResources().getDrawable(resId);
216        requestLayout();
217        invalidate();
218    }
219
220    /**
221     * Sets the drawable to use as the right part of the strip below the
222     * tab indicators.
223     * @param drawable the right strip drawable
224     */
225    public void setRightStripDrawable(Drawable drawable) {
226        mRightStrip = drawable;
227        requestLayout();
228        invalidate();    }
229
230    /**
231     * Sets the drawable to use as the right part of the strip below the
232     * tab indicators.
233     * @param resId the resource identifier of the drawable to use as the
234     * right strip drawable
235     */
236    public void setRightStripDrawable(int resId) {
237        mRightStrip = mContext.getResources().getDrawable(resId);
238        requestLayout();
239        invalidate();
240    }
241
242    /**
243     * Controls whether the bottom strips on the tab indicators are drawn or
244     * not.  The default is to draw them.  If the user specifies a custom
245     * view for the tab indicators, then the TabHost class calls this method
246     * to disable drawing of the bottom strips.
247     * @param stripEnabled true if the bottom strips should be drawn.
248     */
249    public void setStripEnabled(boolean stripEnabled) {
250        mDrawBottomStrips = stripEnabled;
251        invalidate();
252    }
253
254    /**
255     * Indicates whether the bottom strips on the tab indicators are drawn
256     * or not.
257     */
258    public boolean isStripEnabled() {
259        return mDrawBottomStrips;
260    }
261
262    @Override
263    public void childDrawableStateChanged(View child) {
264        if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
265            // To make sure that the bottom strip is redrawn
266            invalidate();
267        }
268        super.childDrawableStateChanged(child);
269    }
270
271    @Override
272    public void dispatchDraw(Canvas canvas) {
273        super.dispatchDraw(canvas);
274
275        // Do nothing if there are no tabs.
276        if (getTabCount() == 0) return;
277
278        // If the user specified a custom view for the tab indicators, then
279        // do not draw the bottom strips.
280        if (!mDrawBottomStrips) {
281            // Skip drawing the bottom strips.
282            return;
283        }
284
285        final View selectedChild = getChildTabViewAt(mSelectedTab);
286
287        final Drawable leftStrip = mLeftStrip;
288        final Drawable rightStrip = mRightStrip;
289
290        leftStrip.setState(selectedChild.getDrawableState());
291        rightStrip.setState(selectedChild.getDrawableState());
292
293        if (mStripMoved) {
294            final Rect bounds = mBounds;
295            bounds.left = selectedChild.getLeft();
296            bounds.right = selectedChild.getRight();
297            final int myHeight = getHeight();
298            leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
299                    myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
300            rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
301                    Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
302            mStripMoved = false;
303        }
304
305        leftStrip.draw(canvas);
306        rightStrip.draw(canvas);
307    }
308
309    /**
310     * Sets the current tab.
311     * This method is used to bring a tab to the front of the Widget,
312     * and is used to post to the rest of the UI that a different tab
313     * has been brought to the foreground.
314     *
315     * Note, this is separate from the traditional "focus" that is
316     * employed from the view logic.
317     *
318     * For instance, if we have a list in a tabbed view, a user may be
319     * navigating up and down the list, moving the UI focus (orange
320     * highlighting) through the list items.  The cursor movement does
321     * not effect the "selected" tab though, because what is being
322     * scrolled through is all on the same tab.  The selected tab only
323     * changes when we navigate between tabs (moving from the list view
324     * to the next tabbed view, in this example).
325     *
326     * To move both the focus AND the selected tab at once, please use
327     * {@link #setCurrentTab}. Normally, the view logic takes care of
328     * adjusting the focus, so unless you're circumventing the UI,
329     * you'll probably just focus your interest here.
330     *
331     *  @param index The tab that you want to indicate as the selected
332     *  tab (tab brought to the front of the widget)
333     *
334     *  @see #focusCurrentTab
335     */
336    public void setCurrentTab(int index) {
337        if (index < 0 || index >= getTabCount()) {
338            return;
339        }
340
341        getChildTabViewAt(mSelectedTab).setSelected(false);
342        mSelectedTab = index;
343        getChildTabViewAt(mSelectedTab).setSelected(true);
344        mStripMoved = true;
345    }
346
347    /**
348     * Sets the current tab and focuses the UI on it.
349     * This method makes sure that the focused tab matches the selected
350     * tab, normally at {@link #setCurrentTab}.  Normally this would not
351     * be an issue if we go through the UI, since the UI is responsible
352     * for calling TabWidget.onFocusChanged(), but in the case where we
353     * are selecting the tab programmatically, we'll need to make sure
354     * focus keeps up.
355     *
356     *  @param index The tab that you want focused (highlighted in orange)
357     *  and selected (tab brought to the front of the widget)
358     *
359     *  @see #setCurrentTab
360     */
361    public void focusCurrentTab(int index) {
362        final int oldTab = mSelectedTab;
363
364        // set the tab
365        setCurrentTab(index);
366
367        // change the focus if applicable.
368        if (oldTab != index) {
369            getChildTabViewAt(index).requestFocus();
370        }
371    }
372
373    @Override
374    public void setEnabled(boolean enabled) {
375        super.setEnabled(enabled);
376        int count = getTabCount();
377
378        for (int i = 0; i < count; i++) {
379            View child = getChildTabViewAt(i);
380            child.setEnabled(enabled);
381        }
382    }
383
384    @Override
385    public void addView(View child) {
386        if (child.getLayoutParams() == null) {
387            final LinearLayout.LayoutParams lp = new LayoutParams(
388                    0,
389                    ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
390            lp.setMargins(0, 0, 0, 0);
391            child.setLayoutParams(lp);
392        }
393
394        // Ensure you can navigate to the tab with the keyboard, and you can touch it
395        child.setFocusable(true);
396        child.setClickable(true);
397
398        // If we have dividers between the tabs and we already have at least one
399        // tab, then add a divider before adding the next tab.
400        if (mDividerDrawable != null && getTabCount() > 0) {
401            ImageView divider = new ImageView(mContext);
402            final LinearLayout.LayoutParams lp = new LayoutParams(
403                    mDividerDrawable.getIntrinsicWidth(),
404                    LayoutParams.MATCH_PARENT);
405            lp.setMargins(0, 0, 0, 0);
406            divider.setLayoutParams(lp);
407            divider.setBackgroundDrawable(mDividerDrawable);
408            super.addView(divider);
409        }
410        super.addView(child);
411
412        // TODO: detect this via geometry with a tabwidget listener rather
413        // than potentially interfere with the view's listener
414        child.setOnClickListener(new TabClickListener(getTabCount() - 1));
415        child.setOnFocusChangeListener(this);
416    }
417
418    /**
419     * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
420     */
421    void setTabSelectionListener(OnTabSelectionChanged listener) {
422        mSelectionChangedListener = listener;
423    }
424
425    public void onFocusChange(View v, boolean hasFocus) {
426        if (v == this && hasFocus && getTabCount() > 0) {
427            getChildTabViewAt(mSelectedTab).requestFocus();
428            return;
429        }
430
431        if (hasFocus) {
432            int i = 0;
433            int numTabs = getTabCount();
434            while (i < numTabs) {
435                if (getChildTabViewAt(i) == v) {
436                    setCurrentTab(i);
437                    mSelectionChangedListener.onTabSelectionChanged(i, false);
438                    break;
439                }
440                i++;
441            }
442        }
443    }
444
445    // registered with each tab indicator so we can notify tab host
446    private class TabClickListener implements OnClickListener {
447
448        private final int mTabIndex;
449
450        private TabClickListener(int tabIndex) {
451            mTabIndex = tabIndex;
452        }
453
454        public void onClick(View v) {
455            mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
456        }
457    }
458
459    /**
460     * Let {@link TabHost} know that the user clicked on a tab indicator.
461     */
462    static interface OnTabSelectionChanged {
463        /**
464         * Informs the TabHost which tab was selected. It also indicates
465         * if the tab was clicked/pressed or just focused into.
466         *
467         * @param tabIndex index of the tab that was selected
468         * @param clicked whether the selection changed due to a touch/click
469         * or due to focus entering the tab through navigation. Pass true
470         * if it was due to a press/click and false otherwise.
471         */
472        void onTabSelectionChanged(int tabIndex, boolean clicked);
473    }
474
475}
476
477