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