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