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