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