TabWidget.java revision 5d5cd178e3afe4a0069d392834cdebcc5c35cc08
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.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.util.AttributeSet;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.View.OnFocusChangeListener;
29
30
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 */
44public class TabWidget extends LinearLayout implements OnFocusChangeListener {
45
46
47    private OnTabSelectionChanged mSelectionChangedListener;
48    private int mSelectedTab = 0;
49    private Drawable mBottomLeftStrip;
50    private Drawable mBottomRightStrip;
51    private boolean mStripMoved;
52    private Drawable mDividerDrawable;
53    private boolean mDrawBottomStrips = true;
54
55    public TabWidget(Context context) {
56        this(context, null);
57    }
58
59    public TabWidget(Context context, AttributeSet attrs) {
60        this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
61    }
62
63    public TabWidget(Context context, AttributeSet attrs, int defStyle) {
64        super(context, attrs);
65        initTabWidget();
66
67        TypedArray a =
68            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget,
69                    defStyle, 0);
70
71        a.recycle();
72    }
73
74    @Override
75    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
76        mStripMoved = true;
77        super.onSizeChanged(w, h, oldw, oldh);
78    }
79
80    private void initTabWidget() {
81        setOrientation(LinearLayout.HORIZONTAL);
82        mBottomLeftStrip = mContext.getResources().getDrawable(
83                com.android.internal.R.drawable.tab_bottom_left);
84        mBottomRightStrip = mContext.getResources().getDrawable(
85                com.android.internal.R.drawable.tab_bottom_right);
86        // Deal with focus, as we don't want the focus to go by default
87        // to a tab other than the current tab
88        setFocusable(true);
89        setOnFocusChangeListener(this);
90    }
91
92    /**
93     * Returns the tab indicator view at the given index.
94     *
95     * @param index the zero-based index of the tab indicator view to return
96     * @return the tab indicator view at the given index
97     */
98    public View getChildTabViewAt(int index) {
99        // If we are using dividers, then instead of tab views at 0, 1, 2, ...
100        // we have tab views at 0, 2, 4, ...
101        if (mDividerDrawable != null) {
102            index *= 2;
103        }
104        return getChildAt(index);
105    }
106
107    /**
108     * Returns the number of tab indicator views.
109     * @return the number of tab indicator views.
110     */
111    public int getTabCount() {
112        int children = getChildCount();
113
114        // If we have dividers, then we will always have an odd number of
115        // children: 1, 3, 5, ... and we want to convert that sequence to
116        // this: 1, 2, 3, ...
117        if (mDividerDrawable != null) {
118            children = (children + 1) / 2;
119        }
120        return children;
121    }
122
123    /**
124     * Sets the drawable to use as a divider between the tab indicators.
125     * @param drawable the divider drawable
126     */
127    public void setDividerDrawable(Drawable drawable) {
128        mDividerDrawable = drawable;
129    }
130
131    /**
132     * Sets the drawable to use as a divider between the tab indicators.
133     * @param resId the resource identifier of the drawable to use as a
134     * divider.
135     */
136    public void setDividerDrawable(int resId) {
137        mDividerDrawable = mContext.getResources().getDrawable(resId);
138    }
139
140    /**
141     * Controls whether the bottom strips on the tab indicators are drawn or
142     * not.  The default is to draw them.  If the user specifies a custom
143     * view for the tab indicators, then the TabHost class calls this method
144     * to disable drawing of the bottom strips.
145     * @param drawBottomStrips true if the bottom strips should be drawn.
146     */
147    void setDrawBottomStrips(boolean drawBottomStrips) {
148        mDrawBottomStrips = drawBottomStrips;
149    }
150
151    @Override
152    public void childDrawableStateChanged(View child) {
153        if (child == getChildTabViewAt(mSelectedTab)) {
154            // To make sure that the bottom strip is redrawn
155            invalidate();
156        }
157        super.childDrawableStateChanged(child);
158    }
159
160    @Override
161    public void dispatchDraw(Canvas canvas) {
162        super.dispatchDraw(canvas);
163
164        // If the user specified a custom view for the tab indicators, then
165        // do not draw the bottom strips.
166        if (!mDrawBottomStrips) {
167            // Skip drawing the bottom strips.
168            return;
169        }
170
171        View selectedChild = getChildTabViewAt(mSelectedTab);
172
173        mBottomLeftStrip.setState(selectedChild.getDrawableState());
174        mBottomRightStrip.setState(selectedChild.getDrawableState());
175
176        if (mStripMoved) {
177            Rect selBounds = new Rect(); // Bounds of the selected tab indicator
178            selBounds.left = selectedChild.getLeft();
179            selBounds.right = selectedChild.getRight();
180            final int myHeight = getHeight();
181            mBottomLeftStrip.setBounds(
182                    Math.min(0, selBounds.left
183                                 - mBottomLeftStrip.getIntrinsicWidth()),
184                    myHeight - mBottomLeftStrip.getIntrinsicHeight(),
185                    selBounds.left,
186                    getHeight());
187            mBottomRightStrip.setBounds(
188                    selBounds.right,
189                    myHeight - mBottomRightStrip.getIntrinsicHeight(),
190                    Math.max(getWidth(),
191                            selBounds.right + mBottomRightStrip.getIntrinsicWidth()),
192                    myHeight);
193            mStripMoved = false;
194        }
195
196        mBottomLeftStrip.draw(canvas);
197        mBottomRightStrip.draw(canvas);
198    }
199
200    /**
201     * Sets the current tab.
202     * This method is used to bring a tab to the front of the Widget,
203     * and is used to post to the rest of the UI that a different tab
204     * has been brought to the foreground.
205     *
206     * Note, this is separate from the traditional "focus" that is
207     * employed from the view logic.
208     *
209     * For instance, if we have a list in a tabbed view, a user may be
210     * navigating up and down the list, moving the UI focus (orange
211     * highlighting) through the list items.  The cursor movement does
212     * not effect the "selected" tab though, because what is being
213     * scrolled through is all on the same tab.  The selected tab only
214     * changes when we navigate between tabs (moving from the list view
215     * to the next tabbed view, in this example).
216     *
217     * To move both the focus AND the selected tab at once, please use
218     * {@link #setCurrentTab}. Normally, the view logic takes care of
219     * adjusting the focus, so unless you're circumventing the UI,
220     * you'll probably just focus your interest here.
221     *
222     *  @param index The tab that you want to indicate as the selected
223     *  tab (tab brought to the front of the widget)
224     *
225     *  @see #focusCurrentTab
226     */
227    public void setCurrentTab(int index) {
228        if (index < 0 || index >= getTabCount()) {
229            return;
230        }
231
232        getChildTabViewAt(mSelectedTab).setSelected(false);
233        mSelectedTab = index;
234        getChildTabViewAt(mSelectedTab).setSelected(true);
235        mStripMoved = true;
236    }
237
238    /**
239     * Sets the current tab and focuses the UI on it.
240     * This method makes sure that the focused tab matches the selected
241     * tab, normally at {@link #setCurrentTab}.  Normally this would not
242     * be an issue if we go through the UI, since the UI is responsible
243     * for calling TabWidget.onFocusChanged(), but in the case where we
244     * are selecting the tab programmatically, we'll need to make sure
245     * focus keeps up.
246     *
247     *  @param index The tab that you want focused (highlighted in orange)
248     *  and selected (tab brought to the front of the widget)
249     *
250     *  @see #setCurrentTab
251     */
252    public void focusCurrentTab(int index) {
253        final int oldTab = mSelectedTab;
254
255        // set the tab
256        setCurrentTab(index);
257
258        // change the focus if applicable.
259        if (oldTab != index) {
260            getChildTabViewAt(index).requestFocus();
261        }
262    }
263
264    @Override
265    public void setEnabled(boolean enabled) {
266        super.setEnabled(enabled);
267        int count = getTabCount();
268
269        for (int i = 0; i < count; i++) {
270            View child = getChildTabViewAt(i);
271            child.setEnabled(enabled);
272        }
273    }
274
275    @Override
276    public void addView(View child) {
277        if (child.getLayoutParams() == null) {
278            final LinearLayout.LayoutParams lp = new LayoutParams(
279                    0,
280                    ViewGroup.LayoutParams.FILL_PARENT, 1.0f);
281            lp.setMargins(0, 0, 0, 0);
282            child.setLayoutParams(lp);
283        }
284
285        // Ensure you can navigate to the tab with the keyboard, and you can touch it
286        child.setFocusable(true);
287        child.setClickable(true);
288
289        // If we have dividers between the tabs and we already have at least one
290        // tab, then add a divider before adding the next tab.
291        if (mDividerDrawable != null && getTabCount() > 0) {
292            ImageView divider = new ImageView(mContext);
293            final LinearLayout.LayoutParams lp = new LayoutParams(
294                    mDividerDrawable.getIntrinsicWidth(),
295                    LayoutParams.FILL_PARENT);
296            lp.setMargins(0, 0, 0, 0);
297            divider.setLayoutParams(lp);
298            divider.setBackgroundDrawable(mDividerDrawable);
299            super.addView(divider);
300        }
301        super.addView(child);
302
303        // TODO: detect this via geometry with a tabwidget listener rather
304        // than potentially interfere with the view's listener
305        child.setOnClickListener(new TabClickListener(getTabCount() - 1));
306        child.setOnFocusChangeListener(this);
307    }
308
309    /**
310     * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
311     */
312    void setTabSelectionListener(OnTabSelectionChanged listener) {
313        mSelectionChangedListener = listener;
314    }
315
316    public void onFocusChange(View v, boolean hasFocus) {
317        if (v == this && hasFocus) {
318            getChildTabViewAt(mSelectedTab).requestFocus();
319            return;
320        }
321
322        if (hasFocus) {
323            int i = 0;
324            int numTabs = getTabCount();
325            while (i < numTabs) {
326                if (getChildTabViewAt(i) == v) {
327                    setCurrentTab(i);
328                    mSelectionChangedListener.onTabSelectionChanged(i, false);
329                    break;
330                }
331                i++;
332            }
333        }
334    }
335
336    // registered with each tab indicator so we can notify tab host
337    private class TabClickListener implements OnClickListener {
338
339        private final int mTabIndex;
340
341        private TabClickListener(int tabIndex) {
342            mTabIndex = tabIndex;
343        }
344
345        public void onClick(View v) {
346            mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
347        }
348    }
349
350    /**
351     * Let {@link TabHost} know that the user clicked on a tab indicator.
352     */
353    static interface OnTabSelectionChanged {
354        /**
355         * Informs the TabHost which tab was selected. It also indicates
356         * if the tab was clicked/pressed or just focused into.
357         *
358         * @param tabIndex index of the tab that was selected
359         * @param clicked whether the selection changed due to a touch/click
360         * or due to focus entering the tab through navigation. Pass true
361         * if it was due to a press/click and false otherwise.
362         */
363        void onTabSelectionChanged(int tabIndex, boolean clicked);
364    }
365
366}
367
368