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