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.content.Context; 21 import android.content.res.Resources; 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 * <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 */ 53 public 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 final Rect mBounds = new Rect(); 66 67 // When positive, the widths and heights of tabs will be imposed so that they fit in parent 68 private int mImposedTabsHeight = -1; 69 private int[] mImposedTabWidths; 70 71 public TabWidget(Context context) { 72 this(context, null); 73 } 74 75 public TabWidget(Context context, AttributeSet attrs) { 76 this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); 77 } 78 79 public TabWidget(Context context, AttributeSet attrs, int defStyle) { 80 super(context, attrs, defStyle); 81 82 final TypedArray a = context.obtainStyledAttributes( 83 attrs, com.android.internal.R.styleable.TabWidget, defStyle, 0); 84 85 setStripEnabled(a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true)); 86 setLeftStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripLeft)); 87 setRightStripDrawable(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 setChildrenDrawingOrderEnabled(true); 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 } else { 136 // Use modern color scheme for Eclair and beyond 137 if (mLeftStrip == null) { 138 mLeftStrip = resources.getDrawable( 139 com.android.internal.R.drawable.tab_bottom_left); 140 } 141 if (mRightStrip == null) { 142 mRightStrip = resources.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 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 super.measureHorizontal(widthMeasureSpec, heightMeasureSpec); 212 } 213 214 /** 215 * Returns the tab indicator view at the given index. 216 * 217 * @param index the zero-based index of the tab indicator view to return 218 * @return the tab indicator view at the given index 219 */ 220 public View getChildTabViewAt(int index) { 221 return getChildAt(index); 222 } 223 224 /** 225 * Returns the number of tab indicator views. 226 * @return the number of tab indicator views. 227 */ 228 public int getTabCount() { 229 return getChildCount(); 230 } 231 232 /** 233 * Sets the drawable to use as a divider between the tab indicators. 234 * @param drawable the divider drawable 235 */ 236 @Override 237 public void setDividerDrawable(Drawable drawable) { 238 super.setDividerDrawable(drawable); 239 } 240 241 /** 242 * Sets the drawable to use as a divider between the tab indicators. 243 * @param resId the resource identifier of the drawable to use as a 244 * divider. 245 */ 246 public void setDividerDrawable(int resId) { 247 setDividerDrawable(getResources().getDrawable(resId)); 248 } 249 250 /** 251 * Sets the drawable to use as the left part of the strip below the 252 * tab indicators. 253 * @param drawable the left strip drawable 254 */ 255 public void setLeftStripDrawable(Drawable drawable) { 256 mLeftStrip = drawable; 257 requestLayout(); 258 invalidate(); 259 } 260 261 /** 262 * Sets the drawable to use as the left part of the strip below the 263 * tab indicators. 264 * @param resId the resource identifier of the drawable to use as the 265 * left strip drawable 266 */ 267 public void setLeftStripDrawable(int resId) { 268 setLeftStripDrawable(getResources().getDrawable(resId)); 269 } 270 271 /** 272 * Sets the drawable to use as the right part of the strip below the 273 * tab indicators. 274 * @param drawable the right strip drawable 275 */ 276 public void setRightStripDrawable(Drawable drawable) { 277 mRightStrip = drawable; 278 requestLayout(); 279 invalidate(); 280 } 281 282 /** 283 * Sets the drawable to use as the right part of the strip below the 284 * tab indicators. 285 * @param resId the resource identifier of the drawable to use as the 286 * right strip drawable 287 */ 288 public void setRightStripDrawable(int resId) { 289 setRightStripDrawable(getResources().getDrawable(resId)); 290 } 291 292 /** 293 * Controls whether the bottom strips on the tab indicators are drawn or 294 * not. The default is to draw them. If the user specifies a custom 295 * view for the tab indicators, then the TabHost class calls this method 296 * to disable drawing of the bottom strips. 297 * @param stripEnabled true if the bottom strips should be drawn. 298 */ 299 public void setStripEnabled(boolean stripEnabled) { 300 mDrawBottomStrips = stripEnabled; 301 invalidate(); 302 } 303 304 /** 305 * Indicates whether the bottom strips on the tab indicators are drawn 306 * or not. 307 */ 308 public boolean isStripEnabled() { 309 return mDrawBottomStrips; 310 } 311 312 @Override 313 public void childDrawableStateChanged(View child) { 314 if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) { 315 // To make sure that the bottom strip is redrawn 316 invalidate(); 317 } 318 super.childDrawableStateChanged(child); 319 } 320 321 @Override 322 public void dispatchDraw(Canvas canvas) { 323 super.dispatchDraw(canvas); 324 325 // Do nothing if there are no tabs. 326 if (getTabCount() == 0) return; 327 328 // If the user specified a custom view for the tab indicators, then 329 // do not draw the bottom strips. 330 if (!mDrawBottomStrips) { 331 // Skip drawing the bottom strips. 332 return; 333 } 334 335 final View selectedChild = getChildTabViewAt(mSelectedTab); 336 337 final Drawable leftStrip = mLeftStrip; 338 final Drawable rightStrip = mRightStrip; 339 340 leftStrip.setState(selectedChild.getDrawableState()); 341 rightStrip.setState(selectedChild.getDrawableState()); 342 343 if (mStripMoved) { 344 final Rect bounds = mBounds; 345 bounds.left = selectedChild.getLeft(); 346 bounds.right = selectedChild.getRight(); 347 final int myHeight = getHeight(); 348 leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()), 349 myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight); 350 rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(), 351 Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight); 352 mStripMoved = false; 353 } 354 355 leftStrip.draw(canvas); 356 rightStrip.draw(canvas); 357 } 358 359 /** 360 * Sets the current tab. 361 * This method is used to bring a tab to the front of the Widget, 362 * and is used to post to the rest of the UI that a different tab 363 * has been brought to the foreground. 364 * 365 * Note, this is separate from the traditional "focus" that is 366 * employed from the view logic. 367 * 368 * For instance, if we have a list in a tabbed view, a user may be 369 * navigating up and down the list, moving the UI focus (orange 370 * highlighting) through the list items. The cursor movement does 371 * not effect the "selected" tab though, because what is being 372 * scrolled through is all on the same tab. The selected tab only 373 * changes when we navigate between tabs (moving from the list view 374 * to the next tabbed view, in this example). 375 * 376 * To move both the focus AND the selected tab at once, please use 377 * {@link #setCurrentTab}. Normally, the view logic takes care of 378 * adjusting the focus, so unless you're circumventing the UI, 379 * you'll probably just focus your interest here. 380 * 381 * @param index The tab that you want to indicate as the selected 382 * tab (tab brought to the front of the widget) 383 * 384 * @see #focusCurrentTab 385 */ 386 public void setCurrentTab(int index) { 387 if (index < 0 || index >= getTabCount() || index == mSelectedTab) { 388 return; 389 } 390 391 if (mSelectedTab != -1) { 392 getChildTabViewAt(mSelectedTab).setSelected(false); 393 } 394 mSelectedTab = index; 395 getChildTabViewAt(mSelectedTab).setSelected(true); 396 mStripMoved = true; 397 398 if (isShown()) { 399 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 400 } 401 } 402 403 @Override 404 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 405 onPopulateAccessibilityEvent(event); 406 // Dispatch only to the selected tab. 407 if (mSelectedTab != -1) { 408 View tabView = getChildTabViewAt(mSelectedTab); 409 if (tabView != null && tabView.getVisibility() == VISIBLE) { 410 return tabView.dispatchPopulateAccessibilityEvent(event); 411 } 412 } 413 return false; 414 } 415 416 @Override 417 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 418 super.onInitializeAccessibilityEvent(event); 419 event.setItemCount(getTabCount()); 420 event.setCurrentItemIndex(mSelectedTab); 421 } 422 423 /** 424 * Sets the current tab and focuses the UI on it. 425 * This method makes sure that the focused tab matches the selected 426 * tab, normally at {@link #setCurrentTab}. Normally this would not 427 * be an issue if we go through the UI, since the UI is responsible 428 * for calling TabWidget.onFocusChanged(), but in the case where we 429 * are selecting the tab programmatically, we'll need to make sure 430 * focus keeps up. 431 * 432 * @param index The tab that you want focused (highlighted in orange) 433 * and selected (tab brought to the front of the widget) 434 * 435 * @see #setCurrentTab 436 */ 437 public void focusCurrentTab(int index) { 438 final int oldTab = mSelectedTab; 439 440 // set the tab 441 setCurrentTab(index); 442 443 // change the focus if applicable. 444 if (oldTab != index) { 445 getChildTabViewAt(index).requestFocus(); 446 } 447 } 448 449 @Override 450 public void setEnabled(boolean enabled) { 451 super.setEnabled(enabled); 452 453 final int count = getTabCount(); 454 for (int i = 0; i < count; i++) { 455 View child = getChildTabViewAt(i); 456 child.setEnabled(enabled); 457 } 458 } 459 460 @Override 461 public void addView(View child) { 462 if (child.getLayoutParams() == null) { 463 final LinearLayout.LayoutParams lp = new LayoutParams( 464 0, 465 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); 466 lp.setMargins(0, 0, 0, 0); 467 child.setLayoutParams(lp); 468 } 469 470 // Ensure you can navigate to the tab with the keyboard, and you can touch it 471 child.setFocusable(true); 472 child.setClickable(true); 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 removeAllViews() { 484 super.removeAllViews(); 485 mSelectedTab = -1; 486 } 487 488 @Override 489 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { 490 // this class fires events only when tabs are focused or selected 491 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { 492 event.recycle(); 493 return; 494 } 495 super.sendAccessibilityEventUnchecked(event); 496 } 497 498 /** 499 * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. 500 */ 501 void setTabSelectionListener(OnTabSelectionChanged listener) { 502 mSelectionChangedListener = listener; 503 } 504 505 /** {@inheritDoc} */ 506 public void onFocusChange(View v, boolean hasFocus) { 507 if (v == this && hasFocus && getTabCount() > 0) { 508 getChildTabViewAt(mSelectedTab).requestFocus(); 509 return; 510 } 511 512 if (hasFocus) { 513 int i = 0; 514 int numTabs = getTabCount(); 515 while (i < numTabs) { 516 if (getChildTabViewAt(i) == v) { 517 setCurrentTab(i); 518 mSelectionChangedListener.onTabSelectionChanged(i, false); 519 if (isShown()) { 520 // a tab is focused so send an event to announce the tab widget state 521 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 522 } 523 break; 524 } 525 i++; 526 } 527 } 528 } 529 530 // registered with each tab indicator so we can notify tab host 531 private class TabClickListener implements OnClickListener { 532 533 private final int mTabIndex; 534 535 private TabClickListener(int tabIndex) { 536 mTabIndex = tabIndex; 537 } 538 539 public void onClick(View v) { 540 mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); 541 } 542 } 543 544 /** 545 * Let {@link TabHost} know that the user clicked on a tab indicator. 546 */ 547 static interface OnTabSelectionChanged { 548 /** 549 * Informs the TabHost which tab was selected. It also indicates 550 * if the tab was clicked/pressed or just focused into. 551 * 552 * @param tabIndex index of the tab that was selected 553 * @param clicked whether the selection changed due to a touch/click 554 * or due to focus entering the tab through navigation. Pass true 555 * if it was due to a press/click and false otherwise. 556 */ 557 void onTabSelectionChanged(int tabIndex, boolean clicked); 558 } 559 } 560