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 import android.view.accessibility.AccessibilityNodeInfo; 33 34 /** 35 * 36 * Displays a list of tab labels representing each page in the parent's tab 37 * collection. The container object for this widget is 38 * {@link android.widget.TabHost TabHost}. When the user selects a tab, this 39 * object sends a message to the parent container, TabHost, to tell it to switch 40 * the displayed page. You typically won't use many methods directly on this 41 * object. The container TabHost is used to add labels, add the callback 42 * handler, and manage callbacks. You might call this object to iterate the list 43 * of tabs, or to tweak the layout of the tab list, but most methods should be 44 * called on the containing TabHost object. 45 * 46 * @attr ref android.R.styleable#TabWidget_divider 47 * @attr ref android.R.styleable#TabWidget_tabStripEnabled 48 * @attr ref android.R.styleable#TabWidget_tabStripLeft 49 * @attr ref android.R.styleable#TabWidget_tabStripRight 50 */ 51 public class TabWidget extends LinearLayout implements OnFocusChangeListener { 52 private OnTabSelectionChanged mSelectionChangedListener; 53 54 // This value will be set to 0 as soon as the first tab is added to TabHost. 55 private int mSelectedTab = -1; 56 57 private Drawable mLeftStrip; 58 private Drawable mRightStrip; 59 60 private boolean mDrawBottomStrips = true; 61 private boolean mStripMoved; 62 63 private final Rect mBounds = new Rect(); 64 65 // When positive, the widths and heights of tabs will be imposed so that they fit in parent 66 private int mImposedTabsHeight = -1; 67 private int[] mImposedTabWidths; 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, defStyle); 79 80 final TypedArray a = context.obtainStyledAttributes( 81 attrs, com.android.internal.R.styleable.TabWidget, defStyle, 0); 82 83 setStripEnabled(a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true)); 84 setLeftStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripLeft)); 85 setRightStripDrawable(a.getDrawable(R.styleable.TabWidget_tabStripRight)); 86 87 a.recycle(); 88 89 initTabWidget(); 90 } 91 92 @Override 93 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 94 mStripMoved = true; 95 super.onSizeChanged(w, h, oldw, oldh); 96 } 97 98 @Override 99 protected int getChildDrawingOrder(int childCount, int i) { 100 if (mSelectedTab == -1) { 101 return i; 102 } else { 103 // Always draw the selected tab last, so that drop shadows are drawn 104 // in the correct z-order. 105 if (i == childCount - 1) { 106 return mSelectedTab; 107 } else if (i >= mSelectedTab) { 108 return i + 1; 109 } else { 110 return i; 111 } 112 } 113 } 114 115 private void initTabWidget() { 116 setChildrenDrawingOrderEnabled(true); 117 118 final Context context = mContext; 119 final Resources resources = context.getResources(); 120 121 // Tests the target Sdk version, as set in the Manifest. Could not be set using styles.xml 122 // in a values-v? directory which targets the current platform Sdk version instead. 123 if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) { 124 // Donut apps get old color scheme 125 if (mLeftStrip == null) { 126 mLeftStrip = resources.getDrawable( 127 com.android.internal.R.drawable.tab_bottom_left_v4); 128 } 129 if (mRightStrip == null) { 130 mRightStrip = resources.getDrawable( 131 com.android.internal.R.drawable.tab_bottom_right_v4); 132 } 133 } else { 134 // Use modern color scheme for Eclair and beyond 135 if (mLeftStrip == null) { 136 mLeftStrip = resources.getDrawable( 137 com.android.internal.R.drawable.tab_bottom_left); 138 } 139 if (mRightStrip == null) { 140 mRightStrip = resources.getDrawable( 141 com.android.internal.R.drawable.tab_bottom_right); 142 } 143 } 144 145 // Deal with focus, as we don't want the focus to go by default 146 // to a tab other than the current tab 147 setFocusable(true); 148 setOnFocusChangeListener(this); 149 } 150 151 @Override 152 void measureChildBeforeLayout(View child, int childIndex, 153 int widthMeasureSpec, int totalWidth, 154 int heightMeasureSpec, int totalHeight) { 155 if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) { 156 widthMeasureSpec = MeasureSpec.makeMeasureSpec( 157 totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY); 158 heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight, 159 MeasureSpec.EXACTLY); 160 } 161 162 super.measureChildBeforeLayout(child, childIndex, 163 widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); 164 } 165 166 @Override 167 void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) { 168 if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) { 169 super.measureHorizontal(widthMeasureSpec, heightMeasureSpec); 170 return; 171 } 172 173 // First, measure with no constraint 174 final int unspecifiedWidth = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 175 mImposedTabsHeight = -1; 176 super.measureHorizontal(unspecifiedWidth, heightMeasureSpec); 177 178 int extraWidth = getMeasuredWidth() - MeasureSpec.getSize(widthMeasureSpec); 179 if (extraWidth > 0) { 180 final int count = getChildCount(); 181 182 int childCount = 0; 183 for (int i = 0; i < count; i++) { 184 final View child = getChildAt(i); 185 if (child.getVisibility() == GONE) continue; 186 childCount++; 187 } 188 189 if (childCount > 0) { 190 if (mImposedTabWidths == null || mImposedTabWidths.length != count) { 191 mImposedTabWidths = new int[count]; 192 } 193 for (int i = 0; i < count; i++) { 194 final View child = getChildAt(i); 195 if (child.getVisibility() == GONE) continue; 196 final int childWidth = child.getMeasuredWidth(); 197 final int delta = extraWidth / childCount; 198 final int newWidth = Math.max(0, childWidth - delta); 199 mImposedTabWidths[i] = newWidth; 200 // Make sure the extra width is evenly distributed, no int division remainder 201 extraWidth -= childWidth - newWidth; // delta may have been clamped 202 childCount--; 203 mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight()); 204 } 205 } 206 } 207 208 // Measure again, this time with imposed tab widths and respecting initial spec request 209 super.measureHorizontal(widthMeasureSpec, heightMeasureSpec); 210 } 211 212 /** 213 * Returns the tab indicator view at the given index. 214 * 215 * @param index the zero-based index of the tab indicator view to return 216 * @return the tab indicator view at the given index 217 */ 218 public View getChildTabViewAt(int index) { 219 return getChildAt(index); 220 } 221 222 /** 223 * Returns the number of tab indicator views. 224 * @return the number of tab indicator views. 225 */ 226 public int getTabCount() { 227 return getChildCount(); 228 } 229 230 /** 231 * Sets the drawable to use as a divider between the tab indicators. 232 * @param drawable the divider drawable 233 */ 234 @Override 235 public void setDividerDrawable(Drawable drawable) { 236 super.setDividerDrawable(drawable); 237 } 238 239 /** 240 * Sets the drawable to use as a divider between the tab indicators. 241 * @param resId the resource identifier of the drawable to use as a 242 * divider. 243 */ 244 public void setDividerDrawable(int resId) { 245 setDividerDrawable(getResources().getDrawable(resId)); 246 } 247 248 /** 249 * Sets the drawable to use as the left part of the strip below the 250 * tab indicators. 251 * @param drawable the left strip drawable 252 */ 253 public void setLeftStripDrawable(Drawable drawable) { 254 mLeftStrip = drawable; 255 requestLayout(); 256 invalidate(); 257 } 258 259 /** 260 * Sets the drawable to use as the left part of the strip below the 261 * tab indicators. 262 * @param resId the resource identifier of the drawable to use as the 263 * left strip drawable 264 */ 265 public void setLeftStripDrawable(int resId) { 266 setLeftStripDrawable(getResources().getDrawable(resId)); 267 } 268 269 /** 270 * Sets the drawable to use as the right part of the strip below the 271 * tab indicators. 272 * @param drawable the right strip drawable 273 */ 274 public void setRightStripDrawable(Drawable drawable) { 275 mRightStrip = drawable; 276 requestLayout(); 277 invalidate(); 278 } 279 280 /** 281 * Sets the drawable to use as the right part of the strip below the 282 * tab indicators. 283 * @param resId the resource identifier of the drawable to use as the 284 * right strip drawable 285 */ 286 public void setRightStripDrawable(int resId) { 287 setRightStripDrawable(getResources().getDrawable(resId)); 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 onPopulateAccessibilityEvent(event); 404 // Dispatch only to the selected tab. 405 if (mSelectedTab != -1) { 406 View tabView = getChildTabViewAt(mSelectedTab); 407 if (tabView != null && tabView.getVisibility() == VISIBLE) { 408 return tabView.dispatchPopulateAccessibilityEvent(event); 409 } 410 } 411 return false; 412 } 413 414 @Override 415 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 416 super.onInitializeAccessibilityEvent(event); 417 event.setClassName(TabWidget.class.getName()); 418 event.setItemCount(getTabCount()); 419 event.setCurrentItemIndex(mSelectedTab); 420 } 421 422 423 @Override 424 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { 425 // this class fires events only when tabs are focused or selected 426 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && isFocused()) { 427 event.recycle(); 428 return; 429 } 430 super.sendAccessibilityEventUnchecked(event); 431 } 432 433 @Override 434 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 435 super.onInitializeAccessibilityNodeInfo(info); 436 info.setClassName(TabWidget.class.getName()); 437 } 438 439 /** 440 * Sets the current tab and focuses the UI on it. 441 * This method makes sure that the focused tab matches the selected 442 * tab, normally at {@link #setCurrentTab}. Normally this would not 443 * be an issue if we go through the UI, since the UI is responsible 444 * for calling TabWidget.onFocusChanged(), but in the case where we 445 * are selecting the tab programmatically, we'll need to make sure 446 * focus keeps up. 447 * 448 * @param index The tab that you want focused (highlighted in orange) 449 * and selected (tab brought to the front of the widget) 450 * 451 * @see #setCurrentTab 452 */ 453 public void focusCurrentTab(int index) { 454 final int oldTab = mSelectedTab; 455 456 // set the tab 457 setCurrentTab(index); 458 459 // change the focus if applicable. 460 if (oldTab != index) { 461 getChildTabViewAt(index).requestFocus(); 462 } 463 } 464 465 @Override 466 public void setEnabled(boolean enabled) { 467 super.setEnabled(enabled); 468 469 final int count = getTabCount(); 470 for (int i = 0; i < count; i++) { 471 View child = getChildTabViewAt(i); 472 child.setEnabled(enabled); 473 } 474 } 475 476 @Override 477 public void addView(View child) { 478 if (child.getLayoutParams() == null) { 479 final LinearLayout.LayoutParams lp = new LayoutParams( 480 0, 481 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); 482 lp.setMargins(0, 0, 0, 0); 483 child.setLayoutParams(lp); 484 } 485 486 // Ensure you can navigate to the tab with the keyboard, and you can touch it 487 child.setFocusable(true); 488 child.setClickable(true); 489 490 super.addView(child); 491 492 // TODO: detect this via geometry with a tabwidget listener rather 493 // than potentially interfere with the view's listener 494 child.setOnClickListener(new TabClickListener(getTabCount() - 1)); 495 child.setOnFocusChangeListener(this); 496 } 497 498 @Override 499 public void removeAllViews() { 500 super.removeAllViews(); 501 mSelectedTab = -1; 502 } 503 504 /** 505 * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. 506 */ 507 void setTabSelectionListener(OnTabSelectionChanged listener) { 508 mSelectionChangedListener = listener; 509 } 510 511 /** {@inheritDoc} */ 512 public void onFocusChange(View v, boolean hasFocus) { 513 if (v == this && hasFocus && getTabCount() > 0) { 514 getChildTabViewAt(mSelectedTab).requestFocus(); 515 return; 516 } 517 518 if (hasFocus) { 519 int i = 0; 520 int numTabs = getTabCount(); 521 while (i < numTabs) { 522 if (getChildTabViewAt(i) == v) { 523 setCurrentTab(i); 524 mSelectionChangedListener.onTabSelectionChanged(i, false); 525 if (isShown()) { 526 // a tab is focused so send an event to announce the tab widget state 527 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 528 } 529 break; 530 } 531 i++; 532 } 533 } 534 } 535 536 // registered with each tab indicator so we can notify tab host 537 private class TabClickListener implements OnClickListener { 538 539 private final int mTabIndex; 540 541 private TabClickListener(int tabIndex) { 542 mTabIndex = tabIndex; 543 } 544 545 public void onClick(View v) { 546 mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); 547 } 548 } 549 550 /** 551 * Let {@link TabHost} know that the user clicked on a tab indicator. 552 */ 553 static interface OnTabSelectionChanged { 554 /** 555 * Informs the TabHost which tab was selected. It also indicates 556 * if the tab was clicked/pressed or just focused into. 557 * 558 * @param tabIndex index of the tab that was selected 559 * @param clicked whether the selection changed due to a touch/click 560 * or due to focus entering the tab through navigation. Pass true 561 * if it was due to a press/click and false otherwise. 562 */ 563 void onTabSelectionChanged(int tabIndex, boolean clicked); 564 } 565 } 566