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