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