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