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