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