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