TabWidget.java revision cd59febcea2b75ebe657786133dcfa81fc3d5eb1
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 // First, measure with no constraint 176 final int unspecifiedWidth = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 177 mImposedTabsHeight = -1; 178 super.measureHorizontal(unspecifiedWidth, heightMeasureSpec); 179 180 int extraWidth = getMeasuredWidth() - MeasureSpec.getSize(widthMeasureSpec); 181 if (extraWidth > 0) { 182 final int count = getChildCount(); 183 184 int childCount = 0; 185 for (int i = 0; i < count; i++) { 186 final View child = getChildAt(i); 187 if (child.getVisibility() == GONE) continue; 188 childCount++; 189 } 190 191 if (childCount > 0) { 192 if (mImposedTabWidths == null || mImposedTabWidths.length != count) { 193 mImposedTabWidths = new int[count]; 194 } 195 for (int i = 0; i < count; i++) { 196 final View child = getChildAt(i); 197 if (child.getVisibility() == GONE) continue; 198 final int childWidth = child.getMeasuredWidth(); 199 final int delta = extraWidth / childCount; 200 final int newWidth = Math.max(0, childWidth - delta); 201 mImposedTabWidths[i] = newWidth; 202 // Make sure the extra width is evenly distributed, no int division remainder 203 extraWidth -= childWidth - newWidth; // delta may have been clamped 204 childCount--; 205 mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight()); 206 } 207 } 208 } 209 210 // Measure again, this time with imposed tab widths and respecting initial spec request 211 if (mImposedTabsHeight >= 0 || unspecifiedWidth != widthMeasureSpec) { 212 super.measureHorizontal(widthMeasureSpec, heightMeasureSpec); 213 } 214 } 215 216 /** 217 * Returns the tab indicator view at the given index. 218 * 219 * @param index the zero-based index of the tab indicator view to return 220 * @return the tab indicator view at the given index 221 */ 222 public View getChildTabViewAt(int index) { 223 // If we are using dividers, then instead of tab views at 0, 1, 2, ... 224 // we have tab views at 0, 2, 4, ... 225 if (mDividerDrawable != null) { 226 index *= 2; 227 } 228 return getChildAt(index); 229 } 230 231 /** 232 * Returns the number of tab indicator views. 233 * @return the number of tab indicator views. 234 */ 235 public int getTabCount() { 236 int children = getChildCount(); 237 238 // If we have dividers, then we will always have an odd number of 239 // children: 1, 3, 5, ... and we want to convert that sequence to 240 // this: 1, 2, 3, ... 241 if (mDividerDrawable != null) { 242 children = (children + 1) / 2; 243 } 244 return children; 245 } 246 247 /** 248 * Sets the drawable to use as a divider between the tab indicators. 249 * @param drawable the divider drawable 250 */ 251 @Override 252 public void setDividerDrawable(Drawable drawable) { 253 mDividerDrawable = drawable; 254 requestLayout(); 255 invalidate(); 256 } 257 258 /** 259 * Sets the drawable to use as a divider between the tab indicators. 260 * @param resId the resource identifier of the drawable to use as a 261 * divider. 262 */ 263 public void setDividerDrawable(int resId) { 264 mDividerDrawable = mContext.getResources().getDrawable(resId); 265 requestLayout(); 266 invalidate(); 267 } 268 269 /** 270 * Sets the drawable to use as the left part of the strip below the 271 * tab indicators. 272 * @param drawable the left strip drawable 273 */ 274 public void setLeftStripDrawable(Drawable drawable) { 275 mLeftStrip = drawable; 276 requestLayout(); 277 invalidate(); 278 } 279 280 /** 281 * Sets the drawable to use as the left part of the strip below the 282 * tab indicators. 283 * @param resId the resource identifier of the drawable to use as the 284 * left strip drawable 285 */ 286 public void setLeftStripDrawable(int resId) { 287 mLeftStrip = mContext.getResources().getDrawable(resId); 288 requestLayout(); 289 invalidate(); 290 } 291 292 /** 293 * Sets the drawable to use as the right part of the strip below the 294 * tab indicators. 295 * @param drawable the right strip drawable 296 */ 297 public void setRightStripDrawable(Drawable drawable) { 298 mRightStrip = drawable; 299 requestLayout(); 300 invalidate(); } 301 302 /** 303 * Sets the drawable to use as the right part of the strip below the 304 * tab indicators. 305 * @param resId the resource identifier of the drawable to use as the 306 * right strip drawable 307 */ 308 public void setRightStripDrawable(int resId) { 309 mRightStrip = mContext.getResources().getDrawable(resId); 310 requestLayout(); 311 invalidate(); 312 } 313 314 /** 315 * Controls whether the bottom strips on the tab indicators are drawn or 316 * not. The default is to draw them. If the user specifies a custom 317 * view for the tab indicators, then the TabHost class calls this method 318 * to disable drawing of the bottom strips. 319 * @param stripEnabled true if the bottom strips should be drawn. 320 */ 321 public void setStripEnabled(boolean stripEnabled) { 322 mDrawBottomStrips = stripEnabled; 323 invalidate(); 324 } 325 326 /** 327 * Indicates whether the bottom strips on the tab indicators are drawn 328 * or not. 329 */ 330 public boolean isStripEnabled() { 331 return mDrawBottomStrips; 332 } 333 334 @Override 335 public void childDrawableStateChanged(View child) { 336 if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) { 337 // To make sure that the bottom strip is redrawn 338 invalidate(); 339 } 340 super.childDrawableStateChanged(child); 341 } 342 343 @Override 344 public void dispatchDraw(Canvas canvas) { 345 super.dispatchDraw(canvas); 346 347 // Do nothing if there are no tabs. 348 if (getTabCount() == 0) return; 349 350 // If the user specified a custom view for the tab indicators, then 351 // do not draw the bottom strips. 352 if (!mDrawBottomStrips) { 353 // Skip drawing the bottom strips. 354 return; 355 } 356 357 final View selectedChild = getChildTabViewAt(mSelectedTab); 358 359 final Drawable leftStrip = mLeftStrip; 360 final Drawable rightStrip = mRightStrip; 361 362 leftStrip.setState(selectedChild.getDrawableState()); 363 rightStrip.setState(selectedChild.getDrawableState()); 364 365 if (mStripMoved) { 366 final Rect bounds = mBounds; 367 bounds.left = selectedChild.getLeft(); 368 bounds.right = selectedChild.getRight(); 369 final int myHeight = getHeight(); 370 leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()), 371 myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight); 372 rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(), 373 Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight); 374 mStripMoved = false; 375 } 376 377 leftStrip.draw(canvas); 378 rightStrip.draw(canvas); 379 } 380 381 /** 382 * Sets the current tab. 383 * This method is used to bring a tab to the front of the Widget, 384 * and is used to post to the rest of the UI that a different tab 385 * has been brought to the foreground. 386 * 387 * Note, this is separate from the traditional "focus" that is 388 * employed from the view logic. 389 * 390 * For instance, if we have a list in a tabbed view, a user may be 391 * navigating up and down the list, moving the UI focus (orange 392 * highlighting) through the list items. The cursor movement does 393 * not effect the "selected" tab though, because what is being 394 * scrolled through is all on the same tab. The selected tab only 395 * changes when we navigate between tabs (moving from the list view 396 * to the next tabbed view, in this example). 397 * 398 * To move both the focus AND the selected tab at once, please use 399 * {@link #setCurrentTab}. Normally, the view logic takes care of 400 * adjusting the focus, so unless you're circumventing the UI, 401 * you'll probably just focus your interest here. 402 * 403 * @param index The tab that you want to indicate as the selected 404 * tab (tab brought to the front of the widget) 405 * 406 * @see #focusCurrentTab 407 */ 408 public void setCurrentTab(int index) { 409 if (index < 0 || index >= getTabCount() || index == mSelectedTab) { 410 return; 411 } 412 413 if (mSelectedTab != -1) { 414 getChildTabViewAt(mSelectedTab).setSelected(false); 415 } 416 mSelectedTab = index; 417 getChildTabViewAt(mSelectedTab).setSelected(true); 418 mStripMoved = true; 419 420 if (isShown()) { 421 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 422 } 423 } 424 425 @Override 426 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 427 event.setItemCount(getTabCount()); 428 event.setCurrentItemIndex(mSelectedTab); 429 if (mSelectedTab != -1) { 430 getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); 431 } 432 return true; 433 } 434 435 /** 436 * Sets the current tab and focuses the UI on it. 437 * This method makes sure that the focused tab matches the selected 438 * tab, normally at {@link #setCurrentTab}. Normally this would not 439 * be an issue if we go through the UI, since the UI is responsible 440 * for calling TabWidget.onFocusChanged(), but in the case where we 441 * are selecting the tab programmatically, we'll need to make sure 442 * focus keeps up. 443 * 444 * @param index The tab that you want focused (highlighted in orange) 445 * and selected (tab brought to the front of the widget) 446 * 447 * @see #setCurrentTab 448 */ 449 public void focusCurrentTab(int index) { 450 final int oldTab = mSelectedTab; 451 452 // set the tab 453 setCurrentTab(index); 454 455 // change the focus if applicable. 456 if (oldTab != index) { 457 getChildTabViewAt(index).requestFocus(); 458 } 459 } 460 461 @Override 462 public void setEnabled(boolean enabled) { 463 super.setEnabled(enabled); 464 int count = getTabCount(); 465 466 for (int i = 0; i < count; i++) { 467 View child = getChildTabViewAt(i); 468 child.setEnabled(enabled); 469 } 470 } 471 472 @Override 473 public void addView(View child) { 474 if (child.getLayoutParams() == null) { 475 final LinearLayout.LayoutParams lp = new LayoutParams( 476 0, 477 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); 478 lp.setMargins(0, 0, 0, 0); 479 child.setLayoutParams(lp); 480 } 481 482 // Ensure you can navigate to the tab with the keyboard, and you can touch it 483 child.setFocusable(true); 484 child.setClickable(true); 485 486 // If we have dividers between the tabs and we already have at least one 487 // tab, then add a divider before adding the next tab. 488 if (mDividerDrawable != null && getTabCount() > 0) { 489 ImageView divider = new ImageView(mContext); 490 final LinearLayout.LayoutParams lp = new LayoutParams( 491 mDividerDrawable.getIntrinsicWidth(), 492 LayoutParams.MATCH_PARENT); 493 lp.setMargins(0, 0, 0, 0); 494 divider.setLayoutParams(lp); 495 divider.setBackgroundDrawable(mDividerDrawable); 496 super.addView(divider); 497 } 498 super.addView(child); 499 500 // TODO: detect this via geometry with a tabwidget listener rather 501 // than potentially interfere with the view's listener 502 child.setOnClickListener(new TabClickListener(getTabCount() - 1)); 503 child.setOnFocusChangeListener(this); 504 } 505 506 @Override 507 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { 508 // this class fires events only when tabs are focused or selected 509 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { 510 return; 511 } 512 super.sendAccessibilityEventUnchecked(event); 513 } 514 515 /** 516 * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. 517 */ 518 void setTabSelectionListener(OnTabSelectionChanged listener) { 519 mSelectionChangedListener = listener; 520 } 521 522 public void onFocusChange(View v, boolean hasFocus) { 523 if (v == this && hasFocus && getTabCount() > 0) { 524 getChildTabViewAt(mSelectedTab).requestFocus(); 525 return; 526 } 527 528 if (hasFocus) { 529 int i = 0; 530 int numTabs = getTabCount(); 531 while (i < numTabs) { 532 if (getChildTabViewAt(i) == v) { 533 setCurrentTab(i); 534 mSelectionChangedListener.onTabSelectionChanged(i, false); 535 if (isShown()) { 536 // a tab is focused so send an event to announce the tab widget state 537 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 538 } 539 break; 540 } 541 i++; 542 } 543 } 544 } 545 546 // registered with each tab indicator so we can notify tab host 547 private class TabClickListener implements OnClickListener { 548 549 private final int mTabIndex; 550 551 private TabClickListener(int tabIndex) { 552 mTabIndex = tabIndex; 553 } 554 555 public void onClick(View v) { 556 mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); 557 } 558 } 559 560 /** 561 * Let {@link TabHost} know that the user clicked on a tab indicator. 562 */ 563 static interface OnTabSelectionChanged { 564 /** 565 * Informs the TabHost which tab was selected. It also indicates 566 * if the tab was clicked/pressed or just focused into. 567 * 568 * @param tabIndex index of the tab that was selected 569 * @param clicked whether the selection changed due to a touch/click 570 * or due to focus entering the tab through navigation. Pass true 571 * if it was due to a press/click and false otherwise. 572 */ 573 void onTabSelectionChanged(int tabIndex, boolean clicked); 574 } 575 576} 577 578