本文原创http://blog.csdn.net/yanbin1079415046,转载请注明出处。
TabHost是一个简单的控件。TabHost的使用有两种方式,一种是继承TabActivity,从中取得TabHost。另一种方式是通过findViewById来找到你布局中定义的TabHost。如果你要仿TabHost也不难,根据TabHost的原理,我们可以通过一个LinearLayout和一个FrameLayout来实现。LinearLayout用来存放Tabs,FrameLayout用来存放TabContent,如果你的TabContent为直接可以展示的View,则直接使用View.setVisibility(View.VISIBLE);和View.setVisibility(View.GONE);来进行TabContent的切换。关于TabHost的基本使用可以自己网上找也可以参考我DEMO中的例子。效果图如下,其中的资源文件来自一位哥们的例子。由于暂时找不到了该文章,无法给出链接。在此对这位哥们表示感谢,有知道的朋友可以在回复中给出链接,谢谢。
如果你的TabContent为另一个Activity,此时你就需要一个名为LocalActivityManager的类来管理TabContent了。当然封装起来就有简单有难了。这里请参考农民伯伯的这篇文章。使用ActivityGroup来切换Activity和Layout。
关于LocalActivityManager的介绍请看这篇文章:LocalActivityManager简介
现在开始进入我们的正题,对TabHost源码的分析。首先说一下两个相关的类,因此此处我不对它们进行介绍,所以稍微说一下。TabActivity是一个继承自android.app.ActivityGroup的Activity,TabActivity的主要做事是让我们可以轻松的得到TabWidget,TabHost以及设置默认选中的Tab,分别对应的方法为:getTabHost(),getTabWidget()和setDefaultTab(...)。ActivityGroup的主要作用是创建一个LocalActivityManager,这个LocalActivityManager可以用来管理TabContent中Activity的切换(变更生命周期方法),这里的TabContent是以启动一个新的Activity的方式创建的。更具体的内容可以参见这两篇文章:
TabActivity的使用
ActivityGroup简介
在介绍TabHost之前,先和下面的两个类,两个接口混一下眼熟。
a、TabSpec
/** * 持有tab和tabContent 的对象,还有一个用来跟踪该tab的标签tag。 * A tab has a tab indicator, content, and a tag that is used to keep * track of it. This builder helps choose among these options. * * For the tab indicator, your choices are: * 1) set a label * 2) set a label and an icon * * For the tab content, your choices are: * 1) the id of a {@link View} * 2) a {@link TabContentFactory} that creates the {@link View} content. * 3) an {@link Intent} that launches an {@link android.app.Activity}. */ public class TabSpec {//...}
b、TabWidget
/** * 存放的是Tabhost这个容器中与每个页面相关的tab标签。它的主要作用是完成tab切换时的显示。 * Tabhost中会先使用addView(View view)方法将所有的tab加入到TabWidget中,当点击某一个tab的时候,调用focusCurrentTab(int * index)方法将某一个tab取出(获取焦点等操作)。 * Displays a list of tab labels representing each page in the parent's tab * collection. The container object for this widget is * {@link android.widget.TabHost TabHost}. When the user selects a tab, this * object sends a message to the parent container, TabHost, to tell it to switch * the displayed page. You typically won't use many methods directly on this * object. The container TabHost is used to add labels, add the callback * handler, and manage callbacks. You might call this object to iterate the list * of tabs, or to tweak the layout of the tab list, but most methods should be * called on the containing TabHost object. */ public class TabWidget extends LinearLayout{//...}
c、IndicatorStrategy
/** * 创建tab的接口 * Specifies what you do to create a tab indicator. */ private static interface IndicatorStrategy { View createIndicatorView();//返回构建出来的tab对应的view }
d、ContentStrategy
/** *创建TabContent的接口 * Specifies what you do to manage the tab content. */ private static interface ContentStrategy { View getContentView();//返回构建出来的tabcontent对应的view /** * 关闭上一个tabcontent(将控件隐藏或者remove掉) * Perhaps do something when the tab associated with this content has * been closed (i.e make it invisible, or remove it). */ void tabClosed(); }
在程序中,TabHost的使用方法一般如下,我们将根据这个使用方式来分析代码。
//TabHost mTabHost = (TabHost)findViewById(android.R.id.tabhost); 需要手动调用setup()方法 mTabHost.setup(); TabHost mTabHost = getTabHost(); mTabHost.addTab(mTabHost.newTabSpec("tag").setIndicator(...).setContent(...));
1、构建TabHost,这一步将得到TabHost对象,两种方式:getTabHost()或者findViewById(android.R.id.tabhost);
public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchModeChangeListener { private TabWidget mTabWidget;//TabWidget,用来设置Tab,用android.R.id.tabs来标识 private FrameLayout mTabContent;//用来设置TabContent,用android.R.id.tabcontent来标识 //TabSpec持有Tab和TabContent。 private List<TabSpec> mTabSpecs = new ArrayList<TabSpec>(2); protected int mCurrentTab = -1; private View mCurrentView = null; protected LocalActivityManager mLocalActivityManager = null;//很重要的一个类,用来管理通过Intent方式创建的TabContent private OnTabChangeListener mOnTabChangeListener;//tab切换监听 private OnKeyListener mTabKeyListener; private int mTabLayoutId;//这个变量将得到一个系统的布局文件,供设置指示器的时候用(title方式或icon + title方式) /** *顾名思义是启动方法,当我们继承TabActivity的时候,不需要手动调用该方法, *但是当你自己通过findViewById获取TabHost的时候必须要调用该方法。 *这个方法相当于我们普通的activity中对控件的初始化。 */ public void setup() { //看到这句应该明白了为什么我们需要设置android.R.id.tabs了吧。 mTabWidget = (TabWidget) findViewById(com.android.internal.R.id.tabs); if (mTabWidget == null) { throw new RuntimeException( "Your TabHost must have a TabWidget whose id attribute is 'android.R.id.tabs'"); } //Tab选中事件的监听,setCurrentTab(tabIndex);这就是设置当前的Tab与当前的Tabcontent的方法, //具体看下面对setCurrentTab()方法的分析 mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() { public void onTabSelectionChanged(int tabIndex, boolean clicked) { setCurrentTab(tabIndex); if (clicked) { mTabContent.requestFocus(View.FOCUS_FORWARD); } } }); //mTabContents是一个FrameLayout,它寻找的是id为android.R.id.tabcontent的控件。这是TabHost要有 //名为android.R.id.tabcontent的FrameLayout的原因。 mTabContent = (FrameLayout) findViewById(com.android.internal.R.id.tabcontent); if (mTabContent == null) { throw new RuntimeException( "Your TabHost must have a FrameLayout whose id attribute is " + "'android.R.id.tabcontent'"); } }
2、addTab(TabSpec tabSpec)方法,目的是构建一个持有IndicatorStrategy(声明tab的策略)和ContentStrategy(声明tabContent的策略)的对象。
2.1 构建TabSpec
//调用TabHost类的newTabSpec()得到TabSpec对象。 public TabSpec newTabSpec(String tag) { return new TabSpec(tag); } //持有tab和tabContent 的对象 TabSpec public class TabSpec { private String mTag; private IndicatorStrategy mIndicatorStrategy; private ContentStrategy mContentStrategy; //只有一个构造方法,所以必须在构建的时候传递一个String参数,这个参数很重要,他是每个Tab的标志。即文章开始处说的那个Tag。 private TabSpec(String tag) { mTag = tag; } //三种创建tab的方式 public TabSpec setIndicator(CharSequence label) { mIndicatorStrategy = new LabelIndicatorStrategy(label); return this; } public TabSpec setIndicator(CharSequence label, Drawable icon) { mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon); return this; } public TabSpec setIndicator(View view) { mIndicatorStrategy = new ViewIndicatorStrategy(view); return this; } //三种创建tabcontent的方式 public TabSpec setContent(int viewId) { mContentStrategy = new ViewIdContentStrategy(viewId); return this; } public TabSpec setContent(TabContentFactory contentFactory) { mContentStrategy = new FactoryContentStrategy(mTag, contentFactory); return this; } public TabSpec setContent(Intent intent) { mContentStrategy = new IntentContentStrategy(mTag, intent); return this; } //得到当前的tag标签 public String getTag() { return mTag; } }
2.2 设置Tab的过程,setIndicator(...)方法,以及其对应的三种IndicatorStrategy,以下是对它的三种类型参数的讲解。
LabelIndicatorStrategy和LabelAndIconIndicatorStrategy都是使用布局填充器LayoutInflater去填充一个id为mTabLayoutId的布局文件(系统内置),并且把TabWidget作为其父窗体来创建一个Tab。
关于获取mTabLayoutId的代码如下:
TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TabWidget, com.android.internal.R.attr.tabWidgetStyle, 0); mTabLayoutId = a.getResourceId(R.styleable.TabWidget_tabLayout, 0); a.recycle(); if (mTabLayoutId == 0) { mTabLayoutId = R.layout.tab_indicator_holo; }
ViewIndicatorStrategy则是直接返回我们自己创建的view作为某一个tab,具体实现如下,比较简单,就不做解释了。
private class ViewIndicatorStrategy implements IndicatorStrategy { private final View mView; private ViewIndicatorStrategy(View view) { mView = view; } public View createIndicatorView() { return mView; } }
通过上面的操作,mIndicatorStrategy就指向了一个特定的IndicatorStrategy,然后系统会在一个特定的地方调用该接口的createIndicatorView()方法,此时就
返回了一个View,该View就是我们的Tab。
2.3 设置TabContent的过程(准确的说并没有创建出一个具体的TabContent,TabContent会在某一个Tab选中的
时候被创建。但是每一个TabSpec中持有构建特定的TabContent策略的引用),setContent()方法,以及其对应的三种ContentStrategy
a、第一种方式为setContent(int viewId),即以某一个view作为tabcontent,并且注意:该view必须为tabcontent,即我们的mTabContent的子控件。
//设置content的方法,可以看到内部需要构建一个ViewIdContentStrategy public TabSpec setContent(int viewId) { mContentStrategy = new ViewIdContentStrategy(viewId); return this; } //ViewIdContentStrategy的实现 private class ViewIdContentStrategy implements ContentStrategy { private final View mView; private ViewIdContentStrategy(int viewId) { mView = mTabContent.findViewById(viewId);//注意这句,这就是我们的view必须为mTabContent的子控件的原因 if (mView != null) { mView.setVisibility(View.GONE); } else { throw new RuntimeException("Could not create tab content because " + "could not find view with id " + viewId); } } //getContentView的过程为View.VISIBLE,tabClosed的过程为View.GONE public View getContentView() { mView.setVisibility(View.VISIBLE); return mView; } public void tabClosed() { mView.setVisibility(View.GONE); } }
b、第二种方式为:setContent(TabContentFactory contentFactory) TabContentFactory是一个接口
//用一个tag来标识一个content,createTabContent返回的是一个view,意思就是说,在实现类里你可以使用 //LayoutInflater填充出来。这个tag(mTag)就是在你使用mTabhost.newSpec("xxx")设置的这个xxx。具体参见demo public interface TabContentFactory { View createTabContent(String tag); } public TabSpec setContent(TabContentFactory contentFactory) { mContentStrategy = new FactoryContentStrategy(mTag, contentFactory); return this; } //FactoryContentStrategy的实现,就是把你自己构造的view作为tabcontent(createTabContent的具体实现) private class FactoryContentStrategy implements ContentStrategy { private View mTabContent; private final CharSequence mTag; private TabContentFactory mFactory; public FactoryContentStrategy(CharSequence tag, TabContentFactory factory) { mTag = tag; mFactory = factory; } //下面两个方法含义同上 public View getContentView() { if (mTabContent == null) { mTabContent = mFactory.createTabContent(mTag.toString()); } mTabContent.setVisibility(View.VISIBLE); return mTabContent; } public void tabClosed() { mTabContent.setVisibility(View.GONE); } }
c、第三种方式 setContent(Intent intent) 启动一个新的activity作为tabcontent,最常用的方式
public TabSpec setContent(Intent intent) { mContentStrategy = new IntentContentStrategy(mTag, intent); return this; } //IntentContentStrategy的具体实现(这个比较有意思,得好好看看) private class IntentContentStrategy implements ContentStrategy { private final String mTag; private final Intent mIntent; private View mLaunchedView; //构造方法,使用mTag来指示这个tabcontent,mTag含义参见第二种方式中的说明。 private IntentContentStrategy(String tag, Intent intent) { mTag = tag; mIntent = intent; } public View getContentView() { //调用了setup()方法后才会得到LocalActivityManager。有一种方式会自动调用,上面说过。 if (mLocalActivityManager == null) { throw new IllegalStateException("Did you forget to call 'public void setup(LocalActivityManager activityGroup)'?"); } //根据intent去开启不同的Activity。该操作由LocalActivityManager来完成,LocalActivityManager的内容参看下面给出的 //参考文章。这段代码是TabHost比较核心的地方了,涉及到了LocalActivityManager的用法。 final Window w = mLocalActivityManager.startActivity( mTag, mIntent); final View wd = w != null ? w.getDecorView() : null; if (mLaunchedView != wd && mLaunchedView != null) { if (mLaunchedView.getParent() != null) { mTabContent.removeView(mLaunchedView); } } mLaunchedView = wd; if (mLaunchedView != null) { mLaunchedView.setVisibility(View.VISIBLE); mLaunchedView.setFocusableInTouchMode(true); ((ViewGroup) mLaunchedView).setDescendantFocusability( FOCUS_AFTER_DESCENDANTS); } return mLaunchedView; } public void tabClosed() { if (mLaunchedView != null) { mLaunchedView.setVisibility(View.GONE); } } }
2.4 addTab()方法实现
addTab的方法,它主要是增加一个tab以及一个将来构建TabContent的引用,并把这个tab增加到TabWidget中,然后再把拥有了tab的tabSpec增加到名为mTabSpecs的ArrayList中,它存放的一个个的tabSpec,而每一个tabSpec表示的就是某一个tab与其tabcontent的对应。
public void addTab(TabSpec tabSpec) { //必须要通过setIndicator()和setContent()分别设置IndicatorStrategy和ContentStrategy if (tabSpec.mIndicatorStrategy == null) { throw new IllegalArgumentException("you must specify a way to create the tab indicator."); } if (tabSpec.mContentStrategy == null) { throw new IllegalArgumentException("you must specify a way to create the tab content"); } //调用IndicatorStrategy的具体实现来创建tab View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView(); tabIndicator.setOnKeyListener(mTabKeyListener); // If this is a custom view, then do not draw the bottom strips for // the tab indicators. //设置不需要tabs下面的下划线,默认是true //关于如何去除tabwidget的下划线,请参照这位哥们的文章: //http://blog.csdn.net/qqiabc521/article/details/7676670 if (tabSpec.mIndicatorStrategy instanceof ViewIndicatorStrategy) { mTabWidget.setStripEnabled(false); } //将上面的tabIndicator添加到TabWidget中成为一个Tab,接下来就是该Tab的绘制过程了。 mTabWidget.addView(tabIndicator); //将拥有了一个Tab和一个TabContent引用(由setContent方法完成)的TabSpec加入到mTabSpecs集合中。(ArrayList) mTabSpecs.add(tabSpec); //如果没有在你的代码中调用setCurrentTab(int index)方法,默认就是选中第一个Tab。 if (mCurrentTab == -1) { setCurrentTab(0); } }
3、TabHost算是构建完了,TabSpec也有了。那么接下来就是TabHost的显示了。上面说过,如果你没有手动调用setCurrentTab(int index)方法,将默认调用setCurrentTab(0)这个方法,下面就来看看setCurrentTab()这个方法。
/** *比较重要的一个方法,完成Tab以及TabContent的切换。如果你在代码中没有设置,TabHost将默认调用setCurrentTab(0); */ public void setCurrentTab(int index) { //... //这里的tabClosed()是ContentStrategy,内容策略接口中的方法。 //有三个实现类,上面已经说过。 //tabClosed()说白了就是将三种策略下构建的布局View.setVisibility(View.GONE); //因为TabContent是FramLayout,这就很好理解了。就像桌子上铺了多张纸(tab数),然后根据tab的id可以 //将某一张抽出来放到上面,这一步由LocalActivityManager来实现。 if (mCurrentTab != -1) { mTabSpecs.get(mCurrentTab).mContentStrategy.tabClosed();//隐藏掉上一个View } //得到mTabSpecs中第index个TabSpec(从这里可以看出,我们addTab的顺序跟Tab的显示是完全相同的。) mCurrentTab = index; final TabHost.TabSpec spec = mTabSpecs.get(index); // Call the tab widget's focusCurrentTab(), instead of just // selecting the tab. //让某一个Tab选中并且获得焦点。最终将调用TabWidget的setCurrentTab的方法来设置选中的Tab。可以想象成一个LinearLayout,里面有一些编好号 //了的控件,然后根据id让其获得焦点。 mTabWidget.focusCurrentTab(mCurrentTab);//显示当前的Tab // tab content //显示TabContent,如果为空的话,将根据策略的 //不同来构建view,如果不为空,直接取出。 //getContentView()方法在此处调用,也就是说,在将某一个TabSpec添加到mTabSpecs集合中时,只构建了Tab, //而没有构建TabContent(仅持有一个构建需要的策略). //通过前面的addTab方法可以很清楚的看到这一点。 mCurrentView = spec.mContentStrategy.getContentView(); //...一些获取焦点的事件未给出 } }
4、Tab的切换与对应TabContent的显示
上面的setup()方法中我们看到了
mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() { public void onTabSelectionChanged(int tabIndex, boolean clicked) { setCurrentTab(tabIndex); if (clicked) { mTabContent.requestFocus(View.FOCUS_FORWARD); } } });
这个回调方法就可以将TabWidget中的某一个tab取出,并使用setCurrentTab(tabIndex)设置对应的Tab和TabContent了。
那么TabWidget中的某一个Tab是如何被取出的呢?下面就分析一下。
请看看我们的2.4中的有这样一段代码:
mTabWidget.addView(tabIndicator);进入到TabWidget的addView方法,玄机就在这里。
看addView()方法:
@Override public void addView(View child) { if (child.getLayoutParams() == null) { final LinearLayout.LayoutParams lp = new LayoutParams( 0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); lp.setMargins(0, 0, 0, 0); child.setLayoutParams(lp); } // Ensure you can navigate to the tab with the keyboard, and you can touch it child.setFocusable(true); child.setClickable(true); super.addView(child); // TODO: detect this via geometry with a tabwidget listener rather // than potentially interfere with the view's listener child.setOnClickListener(new TabClickListener(getTabCount() - 1));//注意这句,你新增的那个指示器被加了一个点击事件。 child.setOnFocusChangeListener(this); }
TabClickListener类如下:
// registered with each tab indicator so we can notify tab host private class TabClickListener implements OnClickListener { private final int mTabIndex; //私有构造方法 private TabClickListener(int tabIndex) { mTabIndex = tabIndex; } public void onClick(View v) { mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true); //mTabIndex通过(getTabCount() - 1)传了进来 } }
通过上面的代码我们就知道了这一点:每一个子Tab都是一个child,那么它就对应着一个index(new TabClickListener(getTabCount() - 1))。当某一个tab被选中的时候,系统将给这个child执行requestFocus操作,它当然也就可以获得这个child所对应的tabIndex了。到这里,我们就跟着TabHost使用的过程过了一遍TabHost的源码,有分析的不到位的地方还望指正,谢谢!