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