TabWidget.java revision 52a5e6588395c9cea128d245a2572b7d69bbb12c
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 event.setItemCount(getTabCount()); 431 event.setCurrentItemIndex(mSelectedTab); 432 if (mSelectedTab != -1) { 433 getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); 434 } 435 return true; 436 } 437 438 /** 439 * Sets the current tab and focuses the UI on it. 440 * This method makes sure that the focused tab matches the selected 441 * tab, normally at {@link #setCurrentTab}. Normally this would not 442 * be an issue if we go through the UI, since the UI is responsible 443 * for calling TabWidget.onFocusChanged(), but in the case where we 444 * are selecting the tab programmatically, we'll need to make sure 445 * focus keeps up. 446 * 447 * @param index The tab that you want focused (highlighted in orange) 448 * and selected (tab brought to the front of the widget) 449 * 450 * @see #setCurrentTab 451 */ 452 public void focusCurrentTab(int index) { 453 final int oldTab = mSelectedTab; 454 455 // set the tab 456 setCurrentTab(index); 457 458 // change the focus if applicable. 459 if (oldTab != index) { 460 getChildTabViewAt(index).requestFocus(); 461 } 462 } 463 464 @Override 465 public void setEnabled(boolean enabled) { 466 super.setEnabled(enabled); 467 int count = getTabCount(); 468 469 for (int i = 0; i < count; i++) { 470 View child = getChildTabViewAt(i); 471 child.setEnabled(enabled); 472 } 473 } 474 475 @Override 476 public void addView(View child) { 477 if (child.getLayoutParams() == null) { 478 final LinearLayout.LayoutParams lp = new LayoutParams( 479 0, 480 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); 481 lp.setMargins(0, 0, 0, 0); 482 child.setLayoutParams(lp); 483 } 484 485 // Ensure you can navigate to the tab with the keyboard, and you can touch it 486 child.setFocusable(true); 487 child.setClickable(true); 488 489 // If we have dividers between the tabs and we already have at least one 490 // tab, then add a divider before adding the next tab. 491 if (mDividerDrawable != null && getTabCount() > 0) { 492 ImageView divider = new ImageView(mContext); 493 final LinearLayout.LayoutParams lp = new LayoutParams( 494 mDividerDrawable.getIntrinsicWidth(), 495 LayoutParams.MATCH_PARENT); 496 lp.setMargins(0, 0, 0, 0); 497 divider.setLayoutParams(lp); 498 divider.setBackgroundDrawable(mDividerDrawable); 499 super.addView(divider); 500 } 501 super.addView(child); 502 503 // TODO: detect this via geometry with a tabwidget listener rather 504 // than potentially interfere with the view's listener 505 child.setOnClickListener(new TabClickListener(getTabCount() - 1)); 506 child.setOnFocusChangeListener(this); 507 } 508 509 @Override 510 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { 511 // this class fires events only when tabs are focused or selected 512 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { 513 return; 514 } 515 super.sendAccessibilityEventUnchecked(event); 516 } 517 518 /** 519 * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. 520 */ 521 void setTabSelectionListener(OnTabSelectionChanged listener) { 522 mSelectionChangedListener = listener; 523 } 524 525 public void onFocusChange(View v, boolean hasFocus) { 526 if (v == this && hasFocus && getTabCount() > 0) { 527 getChildTabViewAt(mSelectedTab).requestFocus(); 528 return; 529 } 530 531 if (hasFocus) { 532 int i = 0; 533 int numTabs = getTabCount(); 534 while (i < numTabs) { 535 if (getChildTabViewAt(i) == v) { 536 setCurrentTab(i); 537 mSelectionChangedListener.onTabSelectionChanged(i, false); 538 if (isShown()) { 539 // a tab is focused so send an event to announce the tab widget state 540 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 541 } 542 break; 543 } 544 i++; 545 } 546 } 547 } 548 549 // registered with each tab indicator so we can notify tab host 550 private class TabClickListener implements OnClickListener { 551 552 private final int mTabIndex; 553 554 private TabClickListener(int tabIndex) { 555 mTabIndex = tabIndex; 556 } 557 558 public void onClick(View v) { 559 mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); 560 } 561 } 562 563 /** 564 * Let {@link TabHost} know that the user clicked on a tab indicator. 565 */ 566 static interface OnTabSelectionChanged { 567 /** 568 * Informs the TabHost which tab was selected. It also indicates 569 * if the tab was clicked/pressed or just focused into. 570 * 571 * @param tabIndex index of the tab that was selected 572 * @param clicked whether the selection changed due to a touch/click 573 * or due to focus entering the tab through navigation. Pass true 574 * if it was due to a press/click and false otherwise. 575 */ 576 void onTabSelectionChanged(int tabIndex, boolean clicked); 577 } 578 579} 580 581