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