• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.R;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.os.Build;
27 import android.util.AttributeSet;
28 import android.view.View;
29 import android.view.View.OnFocusChangeListener;
30 import android.view.ViewGroup;
31 import android.view.accessibility.AccessibilityEvent;
32 import android.view.accessibility.AccessibilityNodeInfo;
33 
34 /**
35  *
36  * Displays a list of tab labels representing each page in the parent's tab
37  * collection. The container object for this widget is
38  * {@link android.widget.TabHost TabHost}. When the user selects a tab, this
39  * object sends a message to the parent container, TabHost, to tell it to switch
40  * the displayed page. You typically won't use many methods directly on this
41  * object. The container TabHost is used to add labels, add the callback
42  * handler, and manage callbacks. You might call this object to iterate the list
43  * of tabs, or to tweak the layout of the tab list, but most methods should be
44  * called on the containing TabHost object.
45  *
46  * @attr ref android.R.styleable#TabWidget_divider
47  * @attr ref android.R.styleable#TabWidget_tabStripEnabled
48  * @attr ref android.R.styleable#TabWidget_tabStripLeft
49  * @attr ref android.R.styleable#TabWidget_tabStripRight
50  */
51 public class TabWidget extends LinearLayout implements OnFocusChangeListener {
52     private OnTabSelectionChanged mSelectionChangedListener;
53 
54     // This value will be set to 0 as soon as the first tab is added to TabHost.
55     private int mSelectedTab = -1;
56 
57     private Drawable mLeftStrip;
58     private Drawable mRightStrip;
59 
60     private boolean mDrawBottomStrips = true;
61     private boolean mStripMoved;
62 
63     private final Rect mBounds = new Rect();
64 
65     // When positive, the widths and heights of tabs will be imposed so that they fit in parent
66     private int mImposedTabsHeight = -1;
67     private int[] mImposedTabWidths;
68 
TabWidget(Context context)69     public TabWidget(Context context) {
70         this(context, null);
71     }
72 
TabWidget(Context context, AttributeSet attrs)73     public TabWidget(Context context, AttributeSet attrs) {
74         this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
75     }
76 
TabWidget(Context context, AttributeSet attrs, int defStyle)77     public TabWidget(Context context, AttributeSet attrs, int defStyle) {
78         super(context, attrs, defStyle);
79 
80         final TypedArray a = context.obtainStyledAttributes(
81                 attrs, com.android.internal.R.styleable.TabWidget, defStyle, 0);
82 
83         setStripEnabled(a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true));
84         setLeftStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripLeft));
85         setRightStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripRight));
86 
87         a.recycle();
88 
89         initTabWidget();
90     }
91 
92     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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
getChildDrawingOrder(int childCount, int i)99     protected int getChildDrawingOrder(int childCount, int i) {
100         if (mSelectedTab == -1) {
101             return i;
102         } else {
103             // Always draw the selected tab last, so that drop shadows are drawn
104             // in the correct z-order.
105             if (i == childCount - 1) {
106                 return mSelectedTab;
107             } else if (i >= mSelectedTab) {
108                 return i + 1;
109             } else {
110                 return i;
111             }
112         }
113     }
114 
initTabWidget()115     private void initTabWidget() {
116         setChildrenDrawingOrderEnabled(true);
117 
118         final Context context = mContext;
119         final Resources resources = context.getResources();
120 
121         // Tests the target Sdk version, as set in the Manifest. Could not be set using styles.xml
122         // in a values-v? directory which targets the current platform Sdk version instead.
123         if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
124             // Donut apps get old color scheme
125             if (mLeftStrip == null) {
126                 mLeftStrip = resources.getDrawable(
127                         com.android.internal.R.drawable.tab_bottom_left_v4);
128             }
129             if (mRightStrip == null) {
130                 mRightStrip = resources.getDrawable(
131                         com.android.internal.R.drawable.tab_bottom_right_v4);
132             }
133         } else {
134             // Use modern color scheme for Eclair and beyond
135             if (mLeftStrip == null) {
136                 mLeftStrip = resources.getDrawable(
137                         com.android.internal.R.drawable.tab_bottom_left);
138             }
139             if (mRightStrip == null) {
140                 mRightStrip = resources.getDrawable(
141                         com.android.internal.R.drawable.tab_bottom_right);
142             }
143         }
144 
145         // Deal with focus, as we don't want the focus to go by default
146         // to a tab other than the current tab
147         setFocusable(true);
148         setOnFocusChangeListener(this);
149     }
150 
151     @Override
measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight)152     void measureChildBeforeLayout(View child, int childIndex,
153             int widthMeasureSpec, int totalWidth,
154             int heightMeasureSpec, int totalHeight) {
155         if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) {
156             widthMeasureSpec = MeasureSpec.makeMeasureSpec(
157                     totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY);
158             heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight,
159                     MeasureSpec.EXACTLY);
160         }
161 
162         super.measureChildBeforeLayout(child, childIndex,
163                 widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
164     }
165 
166     @Override
measureHorizontal(int widthMeasureSpec, int heightMeasureSpec)167     void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
168         if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
169             super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
170             return;
171         }
172 
173         // First, measure with no constraint
174         final int unspecifiedWidth = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
175         mImposedTabsHeight = -1;
176         super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
177 
178         int extraWidth = getMeasuredWidth() - MeasureSpec.getSize(widthMeasureSpec);
179         if (extraWidth > 0) {
180             final int count = getChildCount();
181 
182             int childCount = 0;
183             for (int i = 0; i < count; i++) {
184                 final View child = getChildAt(i);
185                 if (child.getVisibility() == GONE) continue;
186                 childCount++;
187             }
188 
189             if (childCount > 0) {
190                 if (mImposedTabWidths == null || mImposedTabWidths.length != count) {
191                     mImposedTabWidths = new int[count];
192                 }
193                 for (int i = 0; i < count; i++) {
194                     final View child = getChildAt(i);
195                     if (child.getVisibility() == GONE) continue;
196                     final int childWidth = child.getMeasuredWidth();
197                     final int delta = extraWidth / childCount;
198                     final int newWidth = Math.max(0, childWidth - delta);
199                     mImposedTabWidths[i] = newWidth;
200                     // Make sure the extra width is evenly distributed, no int division remainder
201                     extraWidth -= childWidth - newWidth; // delta may have been clamped
202                     childCount--;
203                     mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight());
204                 }
205             }
206         }
207 
208         // Measure again, this time with imposed tab widths and respecting initial spec request
209         super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
210     }
211 
212     /**
213      * Returns the tab indicator view at the given index.
214      *
215      * @param index the zero-based index of the tab indicator view to return
216      * @return the tab indicator view at the given index
217      */
getChildTabViewAt(int index)218     public View getChildTabViewAt(int index) {
219         return getChildAt(index);
220     }
221 
222     /**
223      * Returns the number of tab indicator views.
224      * @return the number of tab indicator views.
225      */
getTabCount()226     public int getTabCount() {
227         return getChildCount();
228     }
229 
230     /**
231      * Sets the drawable to use as a divider between the tab indicators.
232      * @param drawable the divider drawable
233      */
234     @Override
setDividerDrawable(Drawable drawable)235     public void setDividerDrawable(Drawable drawable) {
236         super.setDividerDrawable(drawable);
237     }
238 
239     /**
240      * Sets the drawable to use as a divider between the tab indicators.
241      * @param resId the resource identifier of the drawable to use as a
242      * divider.
243      */
setDividerDrawable(int resId)244     public void setDividerDrawable(int resId) {
245         setDividerDrawable(getResources().getDrawable(resId));
246     }
247 
248     /**
249      * Sets the drawable to use as the left part of the strip below the
250      * tab indicators.
251      * @param drawable the left strip drawable
252      */
setLeftStripDrawable(Drawable drawable)253     public void setLeftStripDrawable(Drawable drawable) {
254         mLeftStrip = drawable;
255         requestLayout();
256         invalidate();
257     }
258 
259     /**
260      * Sets the drawable to use as the left part of the strip below the
261      * tab indicators.
262      * @param resId the resource identifier of the drawable to use as the
263      * left strip drawable
264      */
setLeftStripDrawable(int resId)265     public void setLeftStripDrawable(int resId) {
266         setLeftStripDrawable(getResources().getDrawable(resId));
267     }
268 
269     /**
270      * Sets the drawable to use as the right part of the strip below the
271      * tab indicators.
272      * @param drawable the right strip drawable
273      */
setRightStripDrawable(Drawable drawable)274     public void setRightStripDrawable(Drawable drawable) {
275         mRightStrip = drawable;
276         requestLayout();
277         invalidate();
278     }
279 
280     /**
281      * Sets the drawable to use as the right part of the strip below the
282      * tab indicators.
283      * @param resId the resource identifier of the drawable to use as the
284      * right strip drawable
285      */
setRightStripDrawable(int resId)286     public void setRightStripDrawable(int resId) {
287         setRightStripDrawable(getResources().getDrawable(resId));
288     }
289 
290     /**
291      * Controls whether the bottom strips on the tab indicators are drawn or
292      * not.  The default is to draw them.  If the user specifies a custom
293      * view for the tab indicators, then the TabHost class calls this method
294      * to disable drawing of the bottom strips.
295      * @param stripEnabled true if the bottom strips should be drawn.
296      */
setStripEnabled(boolean stripEnabled)297     public void setStripEnabled(boolean stripEnabled) {
298         mDrawBottomStrips = stripEnabled;
299         invalidate();
300     }
301 
302     /**
303      * Indicates whether the bottom strips on the tab indicators are drawn
304      * or not.
305      */
isStripEnabled()306     public boolean isStripEnabled() {
307         return mDrawBottomStrips;
308     }
309 
310     @Override
childDrawableStateChanged(View child)311     public void childDrawableStateChanged(View child) {
312         if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
313             // To make sure that the bottom strip is redrawn
314             invalidate();
315         }
316         super.childDrawableStateChanged(child);
317     }
318 
319     @Override
dispatchDraw(Canvas canvas)320     public void dispatchDraw(Canvas canvas) {
321         super.dispatchDraw(canvas);
322 
323         // Do nothing if there are no tabs.
324         if (getTabCount() == 0) return;
325 
326         // If the user specified a custom view for the tab indicators, then
327         // do not draw the bottom strips.
328         if (!mDrawBottomStrips) {
329             // Skip drawing the bottom strips.
330             return;
331         }
332 
333         final View selectedChild = getChildTabViewAt(mSelectedTab);
334 
335         final Drawable leftStrip = mLeftStrip;
336         final Drawable rightStrip = mRightStrip;
337 
338         leftStrip.setState(selectedChild.getDrawableState());
339         rightStrip.setState(selectedChild.getDrawableState());
340 
341         if (mStripMoved) {
342             final Rect bounds = mBounds;
343             bounds.left = selectedChild.getLeft();
344             bounds.right = selectedChild.getRight();
345             final int myHeight = getHeight();
346             leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
347                     myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
348             rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
349                     Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
350             mStripMoved = false;
351         }
352 
353         leftStrip.draw(canvas);
354         rightStrip.draw(canvas);
355     }
356 
357     /**
358      * Sets the current tab.
359      * This method is used to bring a tab to the front of the Widget,
360      * and is used to post to the rest of the UI that a different tab
361      * has been brought to the foreground.
362      *
363      * Note, this is separate from the traditional "focus" that is
364      * employed from the view logic.
365      *
366      * For instance, if we have a list in a tabbed view, a user may be
367      * navigating up and down the list, moving the UI focus (orange
368      * highlighting) through the list items.  The cursor movement does
369      * not effect the "selected" tab though, because what is being
370      * scrolled through is all on the same tab.  The selected tab only
371      * changes when we navigate between tabs (moving from the list view
372      * to the next tabbed view, in this example).
373      *
374      * To move both the focus AND the selected tab at once, please use
375      * {@link #setCurrentTab}. Normally, the view logic takes care of
376      * adjusting the focus, so unless you're circumventing the UI,
377      * you'll probably just focus your interest here.
378      *
379      *  @param index The tab that you want to indicate as the selected
380      *  tab (tab brought to the front of the widget)
381      *
382      *  @see #focusCurrentTab
383      */
setCurrentTab(int index)384     public void setCurrentTab(int index) {
385         if (index < 0 || index >= getTabCount() || index == mSelectedTab) {
386             return;
387         }
388 
389         if (mSelectedTab != -1) {
390             getChildTabViewAt(mSelectedTab).setSelected(false);
391         }
392         mSelectedTab = index;
393         getChildTabViewAt(mSelectedTab).setSelected(true);
394         mStripMoved = true;
395 
396         if (isShown()) {
397             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
398         }
399     }
400 
401     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)402     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
403         onPopulateAccessibilityEvent(event);
404         // Dispatch only to the selected tab.
405         if (mSelectedTab != -1) {
406             View tabView = getChildTabViewAt(mSelectedTab);
407             if (tabView != null && tabView.getVisibility() == VISIBLE) {
408                 return tabView.dispatchPopulateAccessibilityEvent(event);
409             }
410         }
411         return false;
412     }
413 
414     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)415     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
416         super.onInitializeAccessibilityEvent(event);
417         event.setClassName(TabWidget.class.getName());
418         event.setItemCount(getTabCount());
419         event.setCurrentItemIndex(mSelectedTab);
420     }
421 
422 
423     @Override
sendAccessibilityEventUnchecked(AccessibilityEvent event)424     public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
425         // this class fires events only when tabs are focused or selected
426         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) {
427             event.recycle();
428             return;
429         }
430         super.sendAccessibilityEventUnchecked(event);
431     }
432 
433     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)434     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
435         super.onInitializeAccessibilityNodeInfo(info);
436         info.setClassName(TabWidget.class.getName());
437     }
438 
439     /**
440      * Sets the current tab and focuses the UI on it.
441      * This method makes sure that the focused tab matches the selected
442      * tab, normally at {@link #setCurrentTab}.  Normally this would not
443      * be an issue if we go through the UI, since the UI is responsible
444      * for calling TabWidget.onFocusChanged(), but in the case where we
445      * are selecting the tab programmatically, we'll need to make sure
446      * focus keeps up.
447      *
448      *  @param index The tab that you want focused (highlighted in orange)
449      *  and selected (tab brought to the front of the widget)
450      *
451      *  @see #setCurrentTab
452      */
focusCurrentTab(int index)453     public void focusCurrentTab(int index) {
454         final int oldTab = mSelectedTab;
455 
456         // set the tab
457         setCurrentTab(index);
458 
459         // change the focus if applicable.
460         if (oldTab != index) {
461             getChildTabViewAt(index).requestFocus();
462         }
463     }
464 
465     @Override
setEnabled(boolean enabled)466     public void setEnabled(boolean enabled) {
467         super.setEnabled(enabled);
468 
469         final int count = getTabCount();
470         for (int i = 0; i < count; i++) {
471             View child = getChildTabViewAt(i);
472             child.setEnabled(enabled);
473         }
474     }
475 
476     @Override
addView(View child)477     public void addView(View child) {
478         if (child.getLayoutParams() == null) {
479             final LinearLayout.LayoutParams lp = new LayoutParams(
480                     0,
481                     ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
482             lp.setMargins(0, 0, 0, 0);
483             child.setLayoutParams(lp);
484         }
485 
486         // Ensure you can navigate to the tab with the keyboard, and you can touch it
487         child.setFocusable(true);
488         child.setClickable(true);
489 
490         super.addView(child);
491 
492         // TODO: detect this via geometry with a tabwidget listener rather
493         // than potentially interfere with the view's listener
494         child.setOnClickListener(new TabClickListener(getTabCount() - 1));
495         child.setOnFocusChangeListener(this);
496     }
497 
498     @Override
removeAllViews()499     public void removeAllViews() {
500         super.removeAllViews();
501         mSelectedTab = -1;
502     }
503 
504     /**
505      * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator.
506      */
setTabSelectionListener(OnTabSelectionChanged listener)507     void setTabSelectionListener(OnTabSelectionChanged listener) {
508         mSelectionChangedListener = listener;
509     }
510 
511     /** {@inheritDoc} */
onFocusChange(View v, boolean hasFocus)512     public void onFocusChange(View v, boolean hasFocus) {
513         if (v == this && hasFocus && getTabCount() > 0) {
514             getChildTabViewAt(mSelectedTab).requestFocus();
515             return;
516         }
517 
518         if (hasFocus) {
519             int i = 0;
520             int numTabs = getTabCount();
521             while (i < numTabs) {
522                 if (getChildTabViewAt(i) == v) {
523                     setCurrentTab(i);
524                     mSelectionChangedListener.onTabSelectionChanged(i, false);
525                     if (isShown()) {
526                         // a tab is focused so send an event to announce the tab widget state
527                         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
528                     }
529                     break;
530                 }
531                 i++;
532             }
533         }
534     }
535 
536     // registered with each tab indicator so we can notify tab host
537     private class TabClickListener implements OnClickListener {
538 
539         private final int mTabIndex;
540 
TabClickListener(int tabIndex)541         private TabClickListener(int tabIndex) {
542             mTabIndex = tabIndex;
543         }
544 
onClick(View v)545         public void onClick(View v) {
546             mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
547         }
548     }
549 
550     /**
551      * Let {@link TabHost} know that the user clicked on a tab indicator.
552      */
553     static interface OnTabSelectionChanged {
554         /**
555          * Informs the TabHost which tab was selected. It also indicates
556          * if the tab was clicked/pressed or just focused into.
557          *
558          * @param tabIndex index of the tab that was selected
559          * @param clicked whether the selection changed due to a touch/click
560          * or due to focus entering the tab through navigation. Pass true
561          * if it was due to a press/click and false otherwise.
562          */
onTabSelectionChanged(int tabIndex, boolean clicked)563         void onTabSelectionChanged(int tabIndex, boolean clicked);
564     }
565 }
566