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