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