TabWidget.java revision 41c738849c1fdf29357048ec9f5e48143dbb026a
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.ViewGroup; 30import android.view.View.OnFocusChangeListener; 31 32/** 33 * 34 * Displays a list of tab labels representing each page in the parent's tab 35 * collection. The container object for this widget is 36 * {@link android.widget.TabHost TabHost}. When the user selects a tab, this 37 * object sends a message to the parent container, TabHost, to tell it to switch 38 * the displayed page. You typically won't use many methods directly on this 39 * object. The container TabHost is used to add labels, add the callback 40 * handler, and manage callbacks. You might call this object to iterate the list 41 * of tabs, or to tweak the layout of the tab list, but most methods should be 42 * called on the containing TabHost object. 43 * 44 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-tabwidget.html">Tab Layout 45 * tutorial</a>.</p> 46 * 47 * @attr ref android.R.styleable#TabWidget_divider 48 * @attr ref android.R.styleable#TabWidget_tabStripEnabled 49 * @attr ref android.R.styleable#TabWidget_tabStripLeft 50 * @attr ref android.R.styleable#TabWidget_tabStripRight 51 */ 52public class TabWidget extends LinearLayout implements OnFocusChangeListener { 53 private OnTabSelectionChanged mSelectionChangedListener; 54 55 private int mSelectedTab = 0; 56 57 private Drawable mLeftStrip; 58 private Drawable mRightStrip; 59 60 private boolean mDrawBottomStrips = true; 61 private boolean mStripMoved; 62 63 private Drawable mDividerDrawable; 64 65 private final Rect mBounds = new Rect(); 66 67 public TabWidget(Context context) { 68 this(context, null); 69 } 70 71 public TabWidget(Context context, AttributeSet attrs) { 72 this(context, attrs, com.android.internal.R.attr.tabWidgetStyle); 73 } 74 75 public TabWidget(Context context, AttributeSet attrs, int defStyle) { 76 super(context, attrs); 77 78 TypedArray a = 79 context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget, 80 defStyle, 0); 81 82 mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, true); 83 mDividerDrawable = a.getDrawable(R.styleable.TabWidget_divider); 84 mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft); 85 mRightStrip = 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 // Always draw the selected tab last, so that drop shadows are drawn 101 // in the correct z-order. 102 if (i == childCount - 1) { 103 return mSelectedTab; 104 } else if (i >= mSelectedTab) { 105 return i + 1; 106 } else { 107 return i; 108 } 109 } 110 111 private void initTabWidget() { 112 mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER; 113 114 final Context context = mContext; 115 final Resources resources = context.getResources(); 116 117 if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) { 118 // Donut apps get old color scheme 119 if (mLeftStrip == null) { 120 mLeftStrip = resources.getDrawable( 121 com.android.internal.R.drawable.tab_bottom_left_v4); 122 } 123 if (mRightStrip == null) { 124 mRightStrip = resources.getDrawable( 125 com.android.internal.R.drawable.tab_bottom_right_v4); 126 } 127 } else { 128 // Use modern color scheme for Eclair and beyond 129 if (mLeftStrip == null) { 130 mLeftStrip = resources.getDrawable( 131 com.android.internal.R.drawable.tab_bottom_left); 132 } 133 if (mRightStrip == null) { 134 mRightStrip = resources.getDrawable( 135 com.android.internal.R.drawable.tab_bottom_right); 136 } 137 } 138 139 // Deal with focus, as we don't want the focus to go by default 140 // to a tab other than the current tab 141 setFocusable(true); 142 setOnFocusChangeListener(this); 143 } 144 145 /** 146 * Returns the tab indicator view at the given index. 147 * 148 * @param index the zero-based index of the tab indicator view to return 149 * @return the tab indicator view at the given index 150 */ 151 public View getChildTabViewAt(int index) { 152 // If we are using dividers, then instead of tab views at 0, 1, 2, ... 153 // we have tab views at 0, 2, 4, ... 154 if (mDividerDrawable != null) { 155 index *= 2; 156 } 157 return getChildAt(index); 158 } 159 160 /** 161 * Returns the number of tab indicator views. 162 * @return the number of tab indicator views. 163 */ 164 public int getTabCount() { 165 int children = getChildCount(); 166 167 // If we have dividers, then we will always have an odd number of 168 // children: 1, 3, 5, ... and we want to convert that sequence to 169 // this: 1, 2, 3, ... 170 if (mDividerDrawable != null) { 171 children = (children + 1) / 2; 172 } 173 return children; 174 } 175 176 /** 177 * Sets the drawable to use as a divider between the tab indicators. 178 * @param drawable the divider drawable 179 */ 180 public void setDividerDrawable(Drawable drawable) { 181 mDividerDrawable = drawable; 182 requestLayout(); 183 invalidate(); 184 } 185 186 /** 187 * Sets the drawable to use as a divider between the tab indicators. 188 * @param resId the resource identifier of the drawable to use as a 189 * divider. 190 */ 191 public void setDividerDrawable(int resId) { 192 mDividerDrawable = mContext.getResources().getDrawable(resId); 193 requestLayout(); 194 invalidate(); 195 } 196 197 /** 198 * Sets the drawable to use as the left part of the strip below the 199 * tab indicators. 200 * @param drawable the left strip drawable 201 */ 202 public void setLeftStripDrawable(Drawable drawable) { 203 mLeftStrip = drawable; 204 requestLayout(); 205 invalidate(); 206 } 207 208 /** 209 * Sets the drawable to use as the left part of the strip below the 210 * tab indicators. 211 * @param resId the resource identifier of the drawable to use as the 212 * left strip drawable 213 */ 214 public void setLeftStripDrawable(int resId) { 215 mLeftStrip = mContext.getResources().getDrawable(resId); 216 requestLayout(); 217 invalidate(); 218 } 219 220 /** 221 * Sets the drawable to use as the right part of the strip below the 222 * tab indicators. 223 * @param drawable the right strip drawable 224 */ 225 public void setRightStripDrawable(Drawable drawable) { 226 mRightStrip = drawable; 227 requestLayout(); 228 invalidate(); } 229 230 /** 231 * Sets the drawable to use as the right part of the strip below the 232 * tab indicators. 233 * @param resId the resource identifier of the drawable to use as the 234 * right strip drawable 235 */ 236 public void setRightStripDrawable(int resId) { 237 mRightStrip = mContext.getResources().getDrawable(resId); 238 requestLayout(); 239 invalidate(); 240 } 241 242 /** 243 * Controls whether the bottom strips on the tab indicators are drawn or 244 * not. The default is to draw them. If the user specifies a custom 245 * view for the tab indicators, then the TabHost class calls this method 246 * to disable drawing of the bottom strips. 247 * @param stripEnabled true if the bottom strips should be drawn. 248 */ 249 public void setStripEnabled(boolean stripEnabled) { 250 mDrawBottomStrips = stripEnabled; 251 invalidate(); 252 } 253 254 /** 255 * Indicates whether the bottom strips on the tab indicators are drawn 256 * or not. 257 */ 258 public boolean isStripEnabled() { 259 return mDrawBottomStrips; 260 } 261 262 @Override 263 public void childDrawableStateChanged(View child) { 264 if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) { 265 // To make sure that the bottom strip is redrawn 266 invalidate(); 267 } 268 super.childDrawableStateChanged(child); 269 } 270 271 @Override 272 public void dispatchDraw(Canvas canvas) { 273 super.dispatchDraw(canvas); 274 275 // Do nothing if there are no tabs. 276 if (getTabCount() == 0) return; 277 278 // If the user specified a custom view for the tab indicators, then 279 // do not draw the bottom strips. 280 if (!mDrawBottomStrips) { 281 // Skip drawing the bottom strips. 282 return; 283 } 284 285 final View selectedChild = getChildTabViewAt(mSelectedTab); 286 287 final Drawable leftStrip = mLeftStrip; 288 final Drawable rightStrip = mRightStrip; 289 290 leftStrip.setState(selectedChild.getDrawableState()); 291 rightStrip.setState(selectedChild.getDrawableState()); 292 293 if (mStripMoved) { 294 final Rect bounds = mBounds; 295 bounds.left = selectedChild.getLeft(); 296 bounds.right = selectedChild.getRight(); 297 final int myHeight = getHeight(); 298 leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()), 299 myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight); 300 rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(), 301 Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight); 302 mStripMoved = false; 303 } 304 305 leftStrip.draw(canvas); 306 rightStrip.draw(canvas); 307 } 308 309 /** 310 * Sets the current tab. 311 * This method is used to bring a tab to the front of the Widget, 312 * and is used to post to the rest of the UI that a different tab 313 * has been brought to the foreground. 314 * 315 * Note, this is separate from the traditional "focus" that is 316 * employed from the view logic. 317 * 318 * For instance, if we have a list in a tabbed view, a user may be 319 * navigating up and down the list, moving the UI focus (orange 320 * highlighting) through the list items. The cursor movement does 321 * not effect the "selected" tab though, because what is being 322 * scrolled through is all on the same tab. The selected tab only 323 * changes when we navigate between tabs (moving from the list view 324 * to the next tabbed view, in this example). 325 * 326 * To move both the focus AND the selected tab at once, please use 327 * {@link #setCurrentTab}. Normally, the view logic takes care of 328 * adjusting the focus, so unless you're circumventing the UI, 329 * you'll probably just focus your interest here. 330 * 331 * @param index The tab that you want to indicate as the selected 332 * tab (tab brought to the front of the widget) 333 * 334 * @see #focusCurrentTab 335 */ 336 public void setCurrentTab(int index) { 337 if (index < 0 || index >= getTabCount()) { 338 return; 339 } 340 341 getChildTabViewAt(mSelectedTab).setSelected(false); 342 mSelectedTab = index; 343 getChildTabViewAt(mSelectedTab).setSelected(true); 344 mStripMoved = true; 345 } 346 347 /** 348 * Sets the current tab and focuses the UI on it. 349 * This method makes sure that the focused tab matches the selected 350 * tab, normally at {@link #setCurrentTab}. Normally this would not 351 * be an issue if we go through the UI, since the UI is responsible 352 * for calling TabWidget.onFocusChanged(), but in the case where we 353 * are selecting the tab programmatically, we'll need to make sure 354 * focus keeps up. 355 * 356 * @param index The tab that you want focused (highlighted in orange) 357 * and selected (tab brought to the front of the widget) 358 * 359 * @see #setCurrentTab 360 */ 361 public void focusCurrentTab(int index) { 362 final int oldTab = mSelectedTab; 363 364 // set the tab 365 setCurrentTab(index); 366 367 // change the focus if applicable. 368 if (oldTab != index) { 369 getChildTabViewAt(index).requestFocus(); 370 } 371 } 372 373 @Override 374 public void setEnabled(boolean enabled) { 375 super.setEnabled(enabled); 376 int count = getTabCount(); 377 378 for (int i = 0; i < count; i++) { 379 View child = getChildTabViewAt(i); 380 child.setEnabled(enabled); 381 } 382 } 383 384 @Override 385 public void addView(View child) { 386 if (child.getLayoutParams() == null) { 387 final LinearLayout.LayoutParams lp = new LayoutParams( 388 0, 389 ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); 390 lp.setMargins(0, 0, 0, 0); 391 child.setLayoutParams(lp); 392 } 393 394 // Ensure you can navigate to the tab with the keyboard, and you can touch it 395 child.setFocusable(true); 396 child.setClickable(true); 397 398 // If we have dividers between the tabs and we already have at least one 399 // tab, then add a divider before adding the next tab. 400 if (mDividerDrawable != null && getTabCount() > 0) { 401 ImageView divider = new ImageView(mContext); 402 final LinearLayout.LayoutParams lp = new LayoutParams( 403 mDividerDrawable.getIntrinsicWidth(), 404 LayoutParams.MATCH_PARENT); 405 lp.setMargins(0, 0, 0, 0); 406 divider.setLayoutParams(lp); 407 divider.setBackgroundDrawable(mDividerDrawable); 408 super.addView(divider); 409 } 410 super.addView(child); 411 412 // TODO: detect this via geometry with a tabwidget listener rather 413 // than potentially interfere with the view's listener 414 child.setOnClickListener(new TabClickListener(getTabCount() - 1)); 415 child.setOnFocusChangeListener(this); 416 } 417 418 /** 419 * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. 420 */ 421 void setTabSelectionListener(OnTabSelectionChanged listener) { 422 mSelectionChangedListener = listener; 423 } 424 425 public void onFocusChange(View v, boolean hasFocus) { 426 if (v == this && hasFocus && getTabCount() > 0) { 427 getChildTabViewAt(mSelectedTab).requestFocus(); 428 return; 429 } 430 431 if (hasFocus) { 432 int i = 0; 433 int numTabs = getTabCount(); 434 while (i < numTabs) { 435 if (getChildTabViewAt(i) == v) { 436 setCurrentTab(i); 437 mSelectionChangedListener.onTabSelectionChanged(i, false); 438 break; 439 } 440 i++; 441 } 442 } 443 } 444 445 // registered with each tab indicator so we can notify tab host 446 private class TabClickListener implements OnClickListener { 447 448 private final int mTabIndex; 449 450 private TabClickListener(int tabIndex) { 451 mTabIndex = tabIndex; 452 } 453 454 public void onClick(View v) { 455 mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); 456 } 457 } 458 459 /** 460 * Let {@link TabHost} know that the user clicked on a tab indicator. 461 */ 462 static interface OnTabSelectionChanged { 463 /** 464 * Informs the TabHost which tab was selected. It also indicates 465 * if the tab was clicked/pressed or just focused into. 466 * 467 * @param tabIndex index of the tab that was selected 468 * @param clicked whether the selection changed due to a touch/click 469 * or due to focus entering the tab through navigation. Pass true 470 * if it was due to a press/click and false otherwise. 471 */ 472 void onTabSelectionChanged(int tabIndex, boolean clicked); 473 } 474 475} 476 477