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