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 public 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 TabWidget(Context context)81 public TabWidget(Context context) { 82 this(context, null); 83 } 84 TabWidget(Context context, AttributeSet attrs)85 public TabWidget(Context context, AttributeSet attrs) { 86 this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); 87 } 88 TabWidget(Context context, AttributeSet attrs, int defStyleAttr)89 public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) { 90 this(context, attrs, defStyleAttr, 0); 91 } 92 TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 onSizeChanged(int w, int h, int oldw, int oldh)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 getChildDrawingOrder(int childCount, int i)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 measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight)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 measureHorizontal(int widthMeasureSpec, int heightMeasureSpec)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 */ getChildTabViewAt(int index)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 */ getTabCount()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 setDividerDrawable(@ullable Drawable drawable)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 */ setDividerDrawable(@rawableRes int resId)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 */ setLeftStripDrawable(@ullable Drawable drawable)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 */ setLeftStripDrawable(@rawableRes int resId)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 getLeftStripDrawable()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 */ setRightStripDrawable(@ullable Drawable drawable)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 */ setRightStripDrawable(@rawableRes int resId)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 getRightStripDrawable()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 */ setStripEnabled(boolean stripEnabled)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 */ isStripEnabled()353 public boolean isStripEnabled() { 354 return mDrawBottomStrips; 355 } 356 357 @Override childDrawableStateChanged(View child)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 dispatchDraw(Canvas canvas)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 #focusCurrentTab}. 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 */ setCurrentTab(int index)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 getAccessibilityClassName()458 public CharSequence getAccessibilityClassName() { 459 return TabWidget.class.getName(); 460 } 461 462 /** @hide */ 463 @Override onInitializeAccessibilityEventInternal(AccessibilityEvent event)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 */ focusCurrentTab(int index)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 setEnabled(boolean enabled)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 addView(View child)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 removeAllViews()532 public void removeAllViews() { 533 super.removeAllViews(); 534 mSelectedTab = -1; 535 } 536 537 @Override onResolvePointerIcon(MotionEvent event, int pointerIndex)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 setTabSelectionListener(OnTabSelectionChanged listener)550 void setTabSelectionListener(OnTabSelectionChanged listener) { 551 mSelectionChangedListener = listener; 552 } 553 554 @Override onFocusChange(View v, boolean hasFocus)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 TabClickListener(int tabIndex)563 private TabClickListener(int tabIndex) { 564 mTabIndex = tabIndex; 565 } 566 onClick(View v)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 */ onTabSelectionChanged(int tabIndex, boolean clicked)586 void onTabSelectionChanged(int tabIndex, boolean clicked); 587 } 588 } 589