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