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