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