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