1/* 2 * Copyright (C) 2012 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.support.v4.app; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.os.Bundle; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.support.annotation.NonNull; 25import android.support.annotation.Nullable; 26import android.util.AttributeSet; 27import android.view.View; 28import android.view.ViewGroup; 29import android.widget.FrameLayout; 30import android.widget.LinearLayout; 31import android.widget.TabHost; 32import android.widget.TabWidget; 33 34import java.util.ArrayList; 35 36/** 37 * Special TabHost that allows the use of {@link Fragment} objects for 38 * its tab content. When placing this in a view hierarchy, after inflating 39 * the hierarchy you must call {@link #setup(Context, FragmentManager, int)} 40 * to complete the initialization of the tab host. 41 * 42 * <p>Here is a simple example of using a FragmentTabHost in an Activity: 43 * 44 * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabs.java 45 * complete} 46 * 47 * <p>This can also be used inside of a fragment through fragment nesting: 48 * 49 * {@sample frameworks/support/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java 50 * complete} 51 */ 52public class FragmentTabHost extends TabHost 53 implements TabHost.OnTabChangeListener { 54 private final ArrayList<TabInfo> mTabs = new ArrayList<>(); 55 56 private FrameLayout mRealTabContent; 57 private Context mContext; 58 private FragmentManager mFragmentManager; 59 private int mContainerId; 60 private TabHost.OnTabChangeListener mOnTabChangeListener; 61 private TabInfo mLastTab; 62 private boolean mAttached; 63 64 static final class TabInfo { 65 final @NonNull String tag; 66 final @NonNull Class<?> clss; 67 final @Nullable Bundle args; 68 Fragment fragment; 69 70 TabInfo(@NonNull String _tag, @NonNull Class<?> _class, @Nullable Bundle _args) { 71 tag = _tag; 72 clss = _class; 73 args = _args; 74 } 75 } 76 77 static class DummyTabFactory implements TabHost.TabContentFactory { 78 private final Context mContext; 79 80 public DummyTabFactory(Context context) { 81 mContext = context; 82 } 83 84 @Override 85 public View createTabContent(String tag) { 86 View v = new View(mContext); 87 v.setMinimumWidth(0); 88 v.setMinimumHeight(0); 89 return v; 90 } 91 } 92 93 static class SavedState extends BaseSavedState { 94 String curTab; 95 96 SavedState(Parcelable superState) { 97 super(superState); 98 } 99 100 SavedState(Parcel in) { 101 super(in); 102 curTab = in.readString(); 103 } 104 105 @Override 106 public void writeToParcel(Parcel out, int flags) { 107 super.writeToParcel(out, flags); 108 out.writeString(curTab); 109 } 110 111 @Override 112 public String toString() { 113 return "FragmentTabHost.SavedState{" 114 + Integer.toHexString(System.identityHashCode(this)) 115 + " curTab=" + curTab + "}"; 116 } 117 118 public static final Parcelable.Creator<SavedState> CREATOR 119 = new Parcelable.Creator<SavedState>() { 120 @Override 121 public SavedState createFromParcel(Parcel in) { 122 return new SavedState(in); 123 } 124 125 @Override 126 public SavedState[] newArray(int size) { 127 return new SavedState[size]; 128 } 129 }; 130 } 131 132 public FragmentTabHost(Context context) { 133 // Note that we call through to the version that takes an AttributeSet, 134 // because the simple Context construct can result in a broken object! 135 super(context, null); 136 initFragmentTabHost(context, null); 137 } 138 139 public FragmentTabHost(Context context, AttributeSet attrs) { 140 super(context, attrs); 141 initFragmentTabHost(context, attrs); 142 } 143 144 private void initFragmentTabHost(Context context, AttributeSet attrs) { 145 final TypedArray a = context.obtainStyledAttributes(attrs, 146 new int[] { android.R.attr.inflatedId }, 0, 0); 147 mContainerId = a.getResourceId(0, 0); 148 a.recycle(); 149 150 super.setOnTabChangedListener(this); 151 } 152 153 private void ensureHierarchy(Context context) { 154 // If owner hasn't made its own view hierarchy, then as a convenience 155 // we will construct a standard one here. 156 if (findViewById(android.R.id.tabs) == null) { 157 LinearLayout ll = new LinearLayout(context); 158 ll.setOrientation(LinearLayout.VERTICAL); 159 addView(ll, new FrameLayout.LayoutParams( 160 ViewGroup.LayoutParams.MATCH_PARENT, 161 ViewGroup.LayoutParams.MATCH_PARENT)); 162 163 TabWidget tw = new TabWidget(context); 164 tw.setId(android.R.id.tabs); 165 tw.setOrientation(TabWidget.HORIZONTAL); 166 ll.addView(tw, new LinearLayout.LayoutParams( 167 ViewGroup.LayoutParams.MATCH_PARENT, 168 ViewGroup.LayoutParams.WRAP_CONTENT, 0)); 169 170 FrameLayout fl = new FrameLayout(context); 171 fl.setId(android.R.id.tabcontent); 172 ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0)); 173 174 mRealTabContent = fl = new FrameLayout(context); 175 mRealTabContent.setId(mContainerId); 176 ll.addView(fl, new LinearLayout.LayoutParams( 177 LinearLayout.LayoutParams.MATCH_PARENT, 0, 1)); 178 } 179 } 180 181 /** 182 * @deprecated Don't call the original TabHost setup, you must instead 183 * call {@link #setup(Context, FragmentManager)} or 184 * {@link #setup(Context, FragmentManager, int)}. 185 */ 186 @Override @Deprecated 187 public void setup() { 188 throw new IllegalStateException( 189 "Must call setup() that takes a Context and FragmentManager"); 190 } 191 192 public void setup(Context context, FragmentManager manager) { 193 ensureHierarchy(context); // Ensure views required by super.setup() 194 super.setup(); 195 mContext = context; 196 mFragmentManager = manager; 197 ensureContent(); 198 } 199 200 public void setup(Context context, FragmentManager manager, int containerId) { 201 ensureHierarchy(context); // Ensure views required by super.setup() 202 super.setup(); 203 mContext = context; 204 mFragmentManager = manager; 205 mContainerId = containerId; 206 ensureContent(); 207 mRealTabContent.setId(containerId); 208 209 // We must have an ID to be able to save/restore our state. If 210 // the owner hasn't set one at this point, we will set it ourselves. 211 if (getId() == View.NO_ID) { 212 setId(android.R.id.tabhost); 213 } 214 } 215 216 private void ensureContent() { 217 if (mRealTabContent == null) { 218 mRealTabContent = (FrameLayout)findViewById(mContainerId); 219 if (mRealTabContent == null) { 220 throw new IllegalStateException( 221 "No tab content FrameLayout found for id " + mContainerId); 222 } 223 } 224 } 225 226 @Override 227 public void setOnTabChangedListener(OnTabChangeListener l) { 228 mOnTabChangeListener = l; 229 } 230 231 public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class<?> clss, 232 @Nullable Bundle args) { 233 tabSpec.setContent(new DummyTabFactory(mContext)); 234 235 final String tag = tabSpec.getTag(); 236 final TabInfo info = new TabInfo(tag, clss, args); 237 238 if (mAttached) { 239 // If we are already attached to the window, then check to make 240 // sure this tab's fragment is inactive if it exists. This shouldn't 241 // normally happen. 242 info.fragment = mFragmentManager.findFragmentByTag(tag); 243 if (info.fragment != null && !info.fragment.isDetached()) { 244 final FragmentTransaction ft = mFragmentManager.beginTransaction(); 245 ft.detach(info.fragment); 246 ft.commit(); 247 } 248 } 249 250 mTabs.add(info); 251 addTab(tabSpec); 252 } 253 254 @Override 255 protected void onAttachedToWindow() { 256 super.onAttachedToWindow(); 257 258 final String currentTag = getCurrentTabTag(); 259 260 // Go through all tabs and make sure their fragments match 261 // the correct state. 262 FragmentTransaction ft = null; 263 for (int i = 0, count = mTabs.size(); i < count; i++) { 264 final TabInfo tab = mTabs.get(i); 265 tab.fragment = mFragmentManager.findFragmentByTag(tab.tag); 266 if (tab.fragment != null && !tab.fragment.isDetached()) { 267 if (tab.tag.equals(currentTag)) { 268 // The fragment for this tab is already there and 269 // active, and it is what we really want to have 270 // as the current tab. Nothing to do. 271 mLastTab = tab; 272 } else { 273 // This fragment was restored in the active state, 274 // but is not the current tab. Deactivate it. 275 if (ft == null) { 276 ft = mFragmentManager.beginTransaction(); 277 } 278 ft.detach(tab.fragment); 279 } 280 } 281 } 282 283 // We are now ready to go. Make sure we are switched to the 284 // correct tab. 285 mAttached = true; 286 ft = doTabChanged(currentTag, ft); 287 if (ft != null) { 288 ft.commit(); 289 mFragmentManager.executePendingTransactions(); 290 } 291 } 292 293 @Override 294 protected void onDetachedFromWindow() { 295 super.onDetachedFromWindow(); 296 mAttached = false; 297 } 298 299 @Override 300 protected Parcelable onSaveInstanceState() { 301 Parcelable superState = super.onSaveInstanceState(); 302 SavedState ss = new SavedState(superState); 303 ss.curTab = getCurrentTabTag(); 304 return ss; 305 } 306 307 @Override 308 protected void onRestoreInstanceState(Parcelable state) { 309 if (!(state instanceof SavedState)) { 310 super.onRestoreInstanceState(state); 311 return; 312 } 313 SavedState ss = (SavedState) state; 314 super.onRestoreInstanceState(ss.getSuperState()); 315 setCurrentTabByTag(ss.curTab); 316 } 317 318 @Override 319 public void onTabChanged(String tabId) { 320 if (mAttached) { 321 final FragmentTransaction ft = doTabChanged(tabId, null); 322 if (ft != null) { 323 ft.commit(); 324 } 325 } 326 if (mOnTabChangeListener != null) { 327 mOnTabChangeListener.onTabChanged(tabId); 328 } 329 } 330 331 @Nullable 332 private FragmentTransaction doTabChanged(@Nullable String tag, 333 @Nullable FragmentTransaction ft) { 334 final TabInfo newTab = getTabInfoForTag(tag); 335 if (mLastTab != newTab) { 336 if (ft == null) { 337 ft = mFragmentManager.beginTransaction(); 338 } 339 340 if (mLastTab != null) { 341 if (mLastTab.fragment != null) { 342 ft.detach(mLastTab.fragment); 343 } 344 } 345 346 if (newTab != null) { 347 if (newTab.fragment == null) { 348 newTab.fragment = Fragment.instantiate(mContext, 349 newTab.clss.getName(), newTab.args); 350 ft.add(mContainerId, newTab.fragment, newTab.tag); 351 } else { 352 ft.attach(newTab.fragment); 353 } 354 } 355 356 mLastTab = newTab; 357 } 358 359 return ft; 360 } 361 362 @Nullable 363 private TabInfo getTabInfoForTag(String tabId) { 364 for (int i = 0, count = mTabs.size(); i < count; i++) { 365 final TabInfo tab = mTabs.get(i); 366 if (tab.tag.equals(tabId)) { 367 return tab; 368 } 369 } 370 return null; 371 } 372} 373