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