blob: 49a0f39b3bad23db0356c18b7c29ba233e71888e [file] [log] [blame]
Rahul Ravikumar05336002019-10-14 15:04:32 -07001/*
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
17package android.widget;
18
19import android.annotation.DrawableRes;
20import android.annotation.Nullable;
21import android.annotation.UnsupportedAppUsage;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.os.Build;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
30import android.view.PointerIcon;
31import android.view.View;
32import android.view.View.OnFocusChangeListener;
33import android.view.ViewGroup;
34import android.view.accessibility.AccessibilityEvent;
35
36import 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 */
57public 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
81 public TabWidget(Context context) {
82 this(context, null);
83 }
84
85 public TabWidget(Context context, AttributeSet attrs) {
86 this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
87 }
88
89 public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) {
90 this(context, attrs, defStyleAttr, 0);
91 }
92
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
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
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
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
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 */
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 */
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
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 */
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 */
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 */
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
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 */
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 */
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
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 */
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 */
353 public boolean isStripEnabled() {
354 return mDrawBottomStrips;
355 }
356
357 @Override
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
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 #setCurrentTab}. 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 */
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
458 public CharSequence getAccessibilityClassName() {
459 return TabWidget.class.getName();
460 }
461
462 /** @hide */
463 @Override
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 */
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
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
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
532 public void removeAllViews() {
533 super.removeAllViews();
534 mSelectedTab = -1;
535 }
536
537 @Override
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
550 void setTabSelectionListener(OnTabSelectionChanged listener) {
551 mSelectionChangedListener = listener;
552 }
553
554 @Override
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
563 private TabClickListener(int tabIndex) {
564 mTabIndex = tabIndex;
565 }
566
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 */
586 void onTabSelectionChanged(int tabIndex, boolean clicked);
587 }
588}