本文原创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的方式创建的。更具体的内容可以参见这两篇文章:
在介绍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的源码,有分析的不到位的地方还望指正,谢谢!