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