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