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