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