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