TabHost.java revision 53175148c9b962c0ffe9cea0f3dc68bc0196e044
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.app.LocalActivityManager; 20import android.content.Context; 21import android.content.Intent; 22import android.graphics.drawable.Drawable; 23import android.util.AttributeSet; 24import android.view.KeyEvent; 25import android.view.LayoutInflater; 26import android.view.SoundEffectConstants; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.ViewTreeObserver; 30import android.view.Window; 31import com.android.internal.R; 32 33import java.util.ArrayList; 34import java.util.List; 35 36/** 37 * Container for a tabbed window view. This object holds two children: a set of tab labels that the 38 * user clicks to select a specific tab, and a FrameLayout object that displays the contents of that 39 * page. The individual elements are typically controlled using this container object, rather than 40 * setting values on the child elements themselves. 41 */ 42public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchModeChangeListener { 43 44 private TabWidget mTabWidget; 45 private FrameLayout mTabContent; 46 private List<TabSpec> mTabSpecs = new ArrayList<TabSpec>(2); 47 /** 48 * This field should be made private, so it is hidden from the SDK. 49 * {@hide} 50 */ 51 protected int mCurrentTab = -1; 52 private View mCurrentView = null; 53 /** 54 * This field should be made private, so it is hidden from the SDK. 55 * {@hide} 56 */ 57 protected LocalActivityManager mLocalActivityManager = null; 58 private OnTabChangeListener mOnTabChangeListener; 59 private OnKeyListener mTabKeyListener; 60 61 public TabHost(Context context) { 62 super(context); 63 initTabHost(); 64 } 65 66 public TabHost(Context context, AttributeSet attrs) { 67 super(context, attrs); 68 initTabHost(); 69 } 70 71 private final void initTabHost() { 72 setFocusableInTouchMode(true); 73 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 74 75 mCurrentTab = -1; 76 mCurrentView = null; 77 } 78 79 /** 80 * Get a new {@link TabSpec} associated with this tab host. 81 * @param tag required tag of tab. 82 */ 83 public TabSpec newTabSpec(String tag) { 84 return new TabSpec(tag); 85 } 86 87 88 89 /** 90 * <p>Call setup() before adding tabs if loading TabHost using findViewById(). 91 * <i><b>However</i></b>: You do not need to call setup() after getTabHost() 92 * in {@link android.app.TabActivity TabActivity}. 93 * Example:</p> 94<pre>mTabHost = (TabHost)findViewById(R.id.tabhost); 95mTabHost.setup(); 96mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); 97 */ 98 public void setup() { 99 mTabWidget = (TabWidget) findViewById(com.android.internal.R.id.tabs); 100 if (mTabWidget == null) { 101 throw new RuntimeException( 102 "Your TabHost must have a TabWidget whose id attribute is 'android.R.id.tabs'"); 103 } 104 105 // KeyListener to attach to all tabs. Detects non-navigation keys 106 // and relays them to the tab content. 107 mTabKeyListener = new OnKeyListener() { 108 public boolean onKey(View v, int keyCode, KeyEvent event) { 109 switch (keyCode) { 110 case KeyEvent.KEYCODE_DPAD_CENTER: 111 case KeyEvent.KEYCODE_DPAD_LEFT: 112 case KeyEvent.KEYCODE_DPAD_RIGHT: 113 case KeyEvent.KEYCODE_DPAD_UP: 114 case KeyEvent.KEYCODE_DPAD_DOWN: 115 case KeyEvent.KEYCODE_ENTER: 116 return false; 117 118 } 119 mTabContent.requestFocus(View.FOCUS_FORWARD); 120 return mTabContent.dispatchKeyEvent(event); 121 } 122 123 }; 124 125 mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() { 126 public void onTabSelectionChanged(int tabIndex, boolean clicked) { 127 setCurrentTab(tabIndex); 128 if (clicked) { 129 mTabContent.requestFocus(View.FOCUS_FORWARD); 130 } 131 } 132 }); 133 134 mTabContent = (FrameLayout) findViewById(com.android.internal.R.id.tabcontent); 135 if (mTabContent == null) { 136 throw new RuntimeException( 137 "Your TabHost must have a FrameLayout whose id attribute is 'android.R.id.tabcontent'"); 138 } 139 } 140 141 /** 142 * If you are using {@link TabSpec#setContent(android.content.Intent)}, this 143 * must be called since the activityGroup is needed to launch the local activity. 144 * 145 * This is done for you if you extend {@link android.app.TabActivity}. 146 * @param activityGroup Used to launch activities for tab content. 147 */ 148 public void setup(LocalActivityManager activityGroup) { 149 setup(); 150 mLocalActivityManager = activityGroup; 151 } 152 153 154 @Override 155 protected void onAttachedToWindow() { 156 super.onAttachedToWindow(); 157 final ViewTreeObserver treeObserver = getViewTreeObserver(); 158 if (treeObserver != null) { 159 treeObserver.addOnTouchModeChangeListener(this); 160 } 161 } 162 163 @Override 164 protected void onDetachedFromWindow() { 165 super.onDetachedFromWindow(); 166 final ViewTreeObserver treeObserver = getViewTreeObserver(); 167 if (treeObserver != null) { 168 treeObserver.removeOnTouchModeChangeListener(this); 169 } 170 } 171 172 /** 173 * {@inheritDoc} 174 */ 175 public void onTouchModeChanged(boolean isInTouchMode) { 176 if (!isInTouchMode) { 177 // leaving touch mode.. if nothing has focus, let's give it to 178 // the indicator of the current tab 179 if (!mCurrentView.hasFocus() || mCurrentView.isFocused()) { 180 mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus(); 181 } 182 } 183 } 184 185 /** 186 * Add a tab. 187 * @param tabSpec Specifies how to create the indicator and content. 188 */ 189 public void addTab(TabSpec tabSpec) { 190 191 if (tabSpec.mIndicatorStrategy == null) { 192 throw new IllegalArgumentException("you must specify a way to create the tab indicator."); 193 } 194 195 if (tabSpec.mContentStrategy == null) { 196 throw new IllegalArgumentException("you must specify a way to create the tab content"); 197 } 198 View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView(); 199 tabIndicator.setOnKeyListener(mTabKeyListener); 200 201 // If this is a custom view, then do not draw the bottom strips for 202 // the tab indicators. 203 if (tabSpec.mIndicatorStrategy instanceof ViewIndicatorStrategy) { 204 mTabWidget.setDrawBottomStrips(false); 205 } 206 mTabWidget.addView(tabIndicator); 207 mTabSpecs.add(tabSpec); 208 209 if (mCurrentTab == -1) { 210 setCurrentTab(0); 211 } 212 } 213 214 215 /** 216 * Removes all tabs from the tab widget associated with this tab host. 217 */ 218 public void clearAllTabs() { 219 mTabWidget.removeAllViews(); 220 initTabHost(); 221 mTabContent.removeAllViews(); 222 mTabSpecs.clear(); 223 requestLayout(); 224 invalidate(); 225 } 226 227 public TabWidget getTabWidget() { 228 return mTabWidget; 229 } 230 231 public int getCurrentTab() { 232 return mCurrentTab; 233 } 234 235 public String getCurrentTabTag() { 236 if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) { 237 return mTabSpecs.get(mCurrentTab).getTag(); 238 } 239 return null; 240 } 241 242 public View getCurrentTabView() { 243 if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) { 244 return mTabWidget.getChildTabViewAt(mCurrentTab); 245 } 246 return null; 247 } 248 249 public View getCurrentView() { 250 return mCurrentView; 251 } 252 253 public void setCurrentTabByTag(String tag) { 254 int i; 255 for (i = 0; i < mTabSpecs.size(); i++) { 256 if (mTabSpecs.get(i).getTag().equals(tag)) { 257 setCurrentTab(i); 258 break; 259 } 260 } 261 } 262 263 /** 264 * Get the FrameLayout which holds tab content 265 */ 266 public FrameLayout getTabContentView() { 267 return mTabContent; 268 } 269 270 @Override 271 public boolean dispatchKeyEvent(KeyEvent event) { 272 final boolean handled = super.dispatchKeyEvent(event); 273 274 // unhandled key ups change focus to tab indicator for embedded activities 275 // when there is nothing that will take focus from default focus searching 276 if (!handled 277 && (event.getAction() == KeyEvent.ACTION_DOWN) 278 && (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) 279 && (mCurrentView.isRootNamespace()) 280 && (mCurrentView.hasFocus()) 281 && (mCurrentView.findFocus().focusSearch(View.FOCUS_UP) == null)) { 282 mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus(); 283 playSoundEffect(SoundEffectConstants.NAVIGATION_UP); 284 return true; 285 } 286 return handled; 287 } 288 289 290 @Override 291 public void dispatchWindowFocusChanged(boolean hasFocus) { 292 mCurrentView.dispatchWindowFocusChanged(hasFocus); 293 } 294 295 public void setCurrentTab(int index) { 296 if (index < 0 || index >= mTabSpecs.size()) { 297 return; 298 } 299 300 if (index == mCurrentTab) { 301 return; 302 } 303 304 // notify old tab content 305 if (mCurrentTab != -1) { 306 mTabSpecs.get(mCurrentTab).mContentStrategy.tabClosed(); 307 } 308 309 mCurrentTab = index; 310 final TabHost.TabSpec spec = mTabSpecs.get(index); 311 312 // Call the tab widget's focusCurrentTab(), instead of just 313 // selecting the tab. 314 mTabWidget.focusCurrentTab(mCurrentTab); 315 316 // tab content 317 mCurrentView = spec.mContentStrategy.getContentView(); 318 319 if (mCurrentView.getParent() == null) { 320 mTabContent 321 .addView( 322 mCurrentView, 323 new ViewGroup.LayoutParams( 324 ViewGroup.LayoutParams.FILL_PARENT, 325 ViewGroup.LayoutParams.FILL_PARENT)); 326 } 327 328 if (!mTabWidget.hasFocus()) { 329 // if the tab widget didn't take focus (likely because we're in touch mode) 330 // give the current tab content view a shot 331 mCurrentView.requestFocus(); 332 } 333 334 //mTabContent.requestFocus(View.FOCUS_FORWARD); 335 invokeOnTabChangeListener(); 336 } 337 338 /** 339 * Register a callback to be invoked when the selected state of any of the items 340 * in this list changes 341 * @param l 342 * The callback that will run 343 */ 344 public void setOnTabChangedListener(OnTabChangeListener l) { 345 mOnTabChangeListener = l; 346 } 347 348 private void invokeOnTabChangeListener() { 349 if (mOnTabChangeListener != null) { 350 mOnTabChangeListener.onTabChanged(getCurrentTabTag()); 351 } 352 } 353 354 /** 355 * Interface definition for a callback to be invoked when tab changed 356 */ 357 public interface OnTabChangeListener { 358 void onTabChanged(String tabId); 359 } 360 361 362 /** 363 * Makes the content of a tab when it is selected. Use this if your tab 364 * content needs to be created on demand, i.e. you are not showing an 365 * existing view or starting an activity. 366 */ 367 public interface TabContentFactory { 368 /** 369 * Callback to make the tab contents 370 * 371 * @param tag 372 * Which tab was selected. 373 * @return The view to display the contents of the selected tab. 374 */ 375 View createTabContent(String tag); 376 } 377 378 379 /** 380 * A tab has a tab indicator, content, and a tag that is used to keep 381 * track of it. This builder helps choose among these options. 382 * 383 * For the tab indicator, your choices are: 384 * 1) set a label 385 * 2) set a label and an icon 386 * 387 * For the tab content, your choices are: 388 * 1) the id of a {@link View} 389 * 2) a {@link TabContentFactory} that creates the {@link View} content. 390 * 3) an {@link Intent} that launches an {@link android.app.Activity}. 391 */ 392 public class TabSpec { 393 394 private String mTag; 395 396 private IndicatorStrategy mIndicatorStrategy; 397 private ContentStrategy mContentStrategy; 398 399 private TabSpec(String tag) { 400 mTag = tag; 401 } 402 403 /** 404 * Specify a label as the tab indicator. 405 */ 406 public TabSpec setIndicator(CharSequence label) { 407 mIndicatorStrategy = new LabelIndicatorStrategy(label); 408 return this; 409 } 410 411 /** 412 * Specify a label and icon as the tab indicator. 413 */ 414 public TabSpec setIndicator(CharSequence label, Drawable icon) { 415 mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon); 416 return this; 417 } 418 419 /** 420 * Specify a view as the tab indicator. 421 */ 422 public TabSpec setIndicator(View view) { 423 mIndicatorStrategy = new ViewIndicatorStrategy(view); 424 return this; 425 } 426 427 /** 428 * Specify the id of the view that should be used as the content 429 * of the tab. 430 */ 431 public TabSpec setContent(int viewId) { 432 mContentStrategy = new ViewIdContentStrategy(viewId); 433 return this; 434 } 435 436 /** 437 * Specify a {@link android.widget.TabHost.TabContentFactory} to use to 438 * create the content of the tab. 439 */ 440 public TabSpec setContent(TabContentFactory contentFactory) { 441 mContentStrategy = new FactoryContentStrategy(mTag, contentFactory); 442 return this; 443 } 444 445 /** 446 * Specify an intent to use to launch an activity as the tab content. 447 */ 448 public TabSpec setContent(Intent intent) { 449 mContentStrategy = new IntentContentStrategy(mTag, intent); 450 return this; 451 } 452 453 454 public String getTag() { 455 return mTag; 456 } 457 } 458 459 /** 460 * Specifies what you do to create a tab indicator. 461 */ 462 private static interface IndicatorStrategy { 463 464 /** 465 * Return the view for the indicator. 466 */ 467 View createIndicatorView(); 468 } 469 470 /** 471 * Specifies what you do to manage the tab content. 472 */ 473 private static interface ContentStrategy { 474 475 /** 476 * Return the content view. The view should may be cached locally. 477 */ 478 View getContentView(); 479 480 /** 481 * Perhaps do something when the tab associated with this content has 482 * been closed (i.e make it invisible, or remove it). 483 */ 484 void tabClosed(); 485 } 486 487 /** 488 * How to create a tab indicator that just has a label. 489 */ 490 private class LabelIndicatorStrategy implements IndicatorStrategy { 491 492 private final CharSequence mLabel; 493 494 private LabelIndicatorStrategy(CharSequence label) { 495 mLabel = label; 496 } 497 498 public View createIndicatorView() { 499 LayoutInflater inflater = 500 (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 501 View tabIndicator = inflater.inflate(R.layout.tab_indicator, 502 mTabWidget, // tab widget is the parent 503 false); // no inflate params 504 505 final TextView tv = (TextView) tabIndicator.findViewById(R.id.title); 506 tv.setText(mLabel); 507 508 return tabIndicator; 509 } 510 } 511 512 /** 513 * How we create a tab indicator that has a label and an icon 514 */ 515 private class LabelAndIconIndicatorStrategy implements IndicatorStrategy { 516 517 private final CharSequence mLabel; 518 private final Drawable mIcon; 519 520 private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) { 521 mLabel = label; 522 mIcon = icon; 523 } 524 525 public View createIndicatorView() { 526 LayoutInflater inflater = 527 (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 528 View tabIndicator = inflater.inflate(R.layout.tab_indicator, 529 mTabWidget, // tab widget is the parent 530 false); // no inflate params 531 532 final TextView tv = (TextView) tabIndicator.findViewById(R.id.title); 533 tv.setText(mLabel); 534 535 final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.icon); 536 iconView.setImageDrawable(mIcon); 537 538 return tabIndicator; 539 } 540 } 541 542 /** 543 * How to create a tab indicator by specifying a view. 544 */ 545 private class ViewIndicatorStrategy implements IndicatorStrategy { 546 547 private final View mView; 548 549 private ViewIndicatorStrategy(View view) { 550 mView = view; 551 } 552 553 public View createIndicatorView() { 554 return mView; 555 } 556 } 557 558 /** 559 * How to create the tab content via a view id. 560 */ 561 private class ViewIdContentStrategy implements ContentStrategy { 562 563 private final View mView; 564 565 private ViewIdContentStrategy(int viewId) { 566 mView = mTabContent.findViewById(viewId); 567 if (mView != null) { 568 mView.setVisibility(View.GONE); 569 } else { 570 throw new RuntimeException("Could not create tab content because " + 571 "could not find view with id " + viewId); 572 } 573 } 574 575 public View getContentView() { 576 mView.setVisibility(View.VISIBLE); 577 return mView; 578 } 579 580 public void tabClosed() { 581 mView.setVisibility(View.GONE); 582 } 583 } 584 585 /** 586 * How tab content is managed using {@link TabContentFactory}. 587 */ 588 private class FactoryContentStrategy implements ContentStrategy { 589 private View mTabContent; 590 private final CharSequence mTag; 591 private TabContentFactory mFactory; 592 593 public FactoryContentStrategy(CharSequence tag, TabContentFactory factory) { 594 mTag = tag; 595 mFactory = factory; 596 } 597 598 public View getContentView() { 599 if (mTabContent == null) { 600 mTabContent = mFactory.createTabContent(mTag.toString()); 601 } 602 mTabContent.setVisibility(View.VISIBLE); 603 return mTabContent; 604 } 605 606 public void tabClosed() { 607 mTabContent.setVisibility(View.INVISIBLE); 608 } 609 } 610 611 /** 612 * How tab content is managed via an {@link Intent}: the content view is the 613 * decorview of the launched activity. 614 */ 615 private class IntentContentStrategy implements ContentStrategy { 616 617 private final String mTag; 618 private final Intent mIntent; 619 620 private View mLaunchedView; 621 622 private IntentContentStrategy(String tag, Intent intent) { 623 mTag = tag; 624 mIntent = intent; 625 } 626 627 public View getContentView() { 628 if (mLocalActivityManager == null) { 629 throw new IllegalStateException("Did you forget to call 'public void setup(LocalActivityManager activityGroup)'?"); 630 } 631 final Window w = mLocalActivityManager.startActivity( 632 mTag, mIntent); 633 final View wd = w != null ? w.getDecorView() : null; 634 if (mLaunchedView != wd && mLaunchedView != null) { 635 if (mLaunchedView.getParent() != null) { 636 mTabContent.removeView(mLaunchedView); 637 } 638 } 639 mLaunchedView = wd; 640 641 // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activities for now so they can get 642 // focus if none of their children have it. They need focus to be able to 643 // display menu items. 644 // 645 // Replace this with something better when Bug 628886 is fixed... 646 // 647 if (mLaunchedView != null) { 648 mLaunchedView.setVisibility(View.VISIBLE); 649 mLaunchedView.setFocusableInTouchMode(true); 650 ((ViewGroup) mLaunchedView).setDescendantFocusability( 651 FOCUS_AFTER_DESCENDANTS); 652 } 653 return mLaunchedView; 654 } 655 656 public void tabClosed() { 657 if (mLaunchedView != null) { 658 mLaunchedView.setVisibility(View.GONE); 659 } 660 } 661 } 662 663} 664