TabHost在文档中的定义是:Container for a tabbed window view.直译过来是一个选项卡式窗口视图容器。简单来说,就是类似于微信首页那种,有几个选择卡,点击选择卡可以跳到不同的页面。
1、TabHost的xml文件中,TabHost、TabWidget和显示TabHost的部分(通常用FrameLayout),这3部分的id是不可修改的,必须使用Android提供的id。
2、TabHost在自定义前,需要初始化,也就是调用tab.setup();(tab是TabHost对象)
3、使用TabHost在几个activity间跳转时,需要继承AcitvityGroup类。并且在初始化时需要调用tab.setup(ActivityGroup.getLocalActivityManager());
4、TabSpec是TabHost的内部类,TabHost需要通过tab.addTab(TabSpec对象)来添加组件,添加的组件之间可以通过TabHost跳转。而实现跳转最重要的是TabSpec的setContent方法,简单来说就是TabSpec的setContent方法传入什么参数,决定TabHost会跳转到哪里。(如果这里不懂没有关系,讲原理的时候会加上源码,到时候就懂了)
1、第一种是把需要改变的布局全部写在TabHost的xml中,然后setContent传入id。
2、第二种是在几个Activity间跳转,setContent传入intent对象。
3、第三种是自己定义几种View,然后在这几种View之间跳转。setContent传入TabHost的内部类TabContentFactory的对象。需要重写TabContentFactory的抽象方法。
1、第一种:把需要改变的布局全部写在TabHost的xml中
这种方法是将所有要跳转的布局全部写在TabHost控件中。在给TabHost的对象添加组件时,给TabSpec的setContent方法传入int类型的参数,参数的意义为:要跳转到的布局的id,这个布局为TabHost的子控件。示例如下:
tabspec_int.xml
<TabHost
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_height="fill_parent"
android:layout_width="fill_parent">
<RelativeLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TabWidget
android:id="@android:id/tabs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
TabWidget>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="@android:id/tabs">
<LinearLayout
android:id="@+id/tab1"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第一个tab的布局" />
LinearLayout>
<LinearLayout
android:id="@+id/tab2"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第二个tab的布局" />
LinearLayout>
<LinearLayout
android:id="@+id/tab3"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="第三个tab的布局" />
LinearLayout>
FrameLayout>
RelativeLayout>
TabHost>
上面的xml中,tabcontent中有3个tab的布局,id分别为tab1、tab2和tab3。在TabspecInt.java中,在给tab.addTab添加组件时,给TabSpec的setContent传入tab1、tab2和tab3的id。
TabspecInt.java
package com.example.tabtest;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TabHost;
public class TabspecInt extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tabspec_int);
TabHost tab = (TabHost) findViewById(android.R.id.tabhost);
//初始化TabHost容器
tab.setup();
//在TabHost创建标签,然后设置:标题/图标/标签页布局
tab.addTab(tab.newTabSpec("tab1").setIndicator("标签1" , getResources().getDrawable(R.drawable.ic_launcher)).setContent(R.id.tab1));
tab.addTab(tab.newTabSpec("tab2").setIndicator("标签2" , null).setContent(R.id.tab2));
tab.addTab(tab.newTabSpec("tab3").setIndicator("标签3" , null).setContent(R.id.tab3));
}
}
2、第二种:在几个Activity间跳转
这种方法应该是最常用的方法。意思就是让TabHost可以在几个Activity之间进行跳转。在给TabHost的对象添加组件时,给TabSpec的setContent方法传入intent类型的参数,参数的意义为:跳转到哪个activity(和startActivity的参数相同)。示例如下:
tabspec_intent.xml
<TabHost
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_height="fill_parent"
android:layout_width="fill_parent">
<RelativeLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TabWidget
android:id="@android:id/tabs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
TabWidget>
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_above="@android:id/tabs">
FrameLayout>
RelativeLayout>
TabHost>
tabspec_intent.xml 和 tabspec_int.xml 相比,在tabcontent中,什么组件都没有。这个layout看起来更加简单。之后会在tabcontent的位置显示若干Activity。在TabspecInt.java中,在给tab.addTab添加组件时,给TabSpec的setContent传入intent对象,intent对象中包含要跳转到的activity名。
TabspecIntent.java
package com.example.tabtest;
import android.app.Activity;
import android.app.LocalActivityManager;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.TabHost;
import android.app.ActivityGroup;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TabHost;
public class TabspecIntent extends ActivityGroup {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tabspec_intent);
TabHost tab = (TabHost) findViewById(android.R.id.tabhost);
//初始化TabHost容器
tab.setup(getLocalActivityManager());
//在TabHost创建标签,然后设置:标题/图标/标签页布局
tab.addTab(tab.newTabSpec("tab1").setIndicator("标签1" , getResources().getDrawable(R.drawable.ic_launcher)).setContent(new Intent(this, Tab1.class)));
tab.addTab(tab.newTabSpec("tab2").setIndicator("标签2" , null).setContent(new Intent(this, Tab2.class)));
tab.addTab(tab.newTabSpec("tab3").setIndicator("标签3" , null).setContent(new Intent(this, Tab3.class)));
}
}
这里要注意的是:这个activity继承的是ActivityGroup类,并且在tab初始化时,调用的tab.setup(getLocalActivityManager());
效果图:
3、第三种:在自己定义的几个view之间跳转。
这里的几个view都是用LayoutInflater类生成xml的,然后把这些view添加为TabHost的组件。这种方法需要实现TabHost的一个内部接口:TabContentFactory,并且实现TabContentFactory的方法:createTabContent,这个方法返回的是View类型对象,也就是点击选择卡后,跳转时要展示的View。示例如下:
xml还可以使用tabspec_intent.xml,因为这种方法实现要跳转到的view时,用到的xml并没有写在TabHost中。因此,xml的TabHost中的tabcontent中,也是什么组件都没有。
TabspecFactory.java
package com.example.tabtest;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory;
public class TabspecFactory extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tabspec_intent);
TabHost tab = (TabHost) findViewById(android.R.id.tabhost);
//初始化TabHost容器
tab.setup();
//在TabHost创建标签,然后设置:标题/图标/标签页布局
tab.addTab(tab.newTabSpec("tab1").setIndicator("标签1" , getResources().getDrawable(R.drawable.ic_launcher)).setContent(factory1));
tab.addTab(tab.newTabSpec("tab2").setIndicator("标签2" , null).setContent(factory2));
tab.addTab(tab.newTabSpec("tab3").setIndicator("标签3" , null).setContent(factory3));
}
private TabContentFactory factory1 = new TabContentFactory() {
@Override
public View createTabContent(String tag) {
return LayoutInflater.from(TabspecFactory.this).inflate(R.layout.tab1, null);
}
};
private TabContentFactory factory2 = new TabContentFactory() {
@Override
public View createTabContent(String tag) {
return LayoutInflater.from(TabspecFactory.this).inflate(R.layout.tab2, null);
}
};
private TabContentFactory factory3 = new TabContentFactory() {
@Override
public View createTabContent(String tag) {
return LayoutInflater.from(TabspecFactory.this).inflate(R.layout.tab3, null);
}
};
}
到这里为止,所有的使用都讲完了,源码可以在此处下载。源码下载
接下来要讲TabHost的原理了,对原理感兴趣的朋友可以看一看。本文会对TabHost的源码进行讲解,本文展示的源码不是全部源码,只截出一些重要的代码放出,基本不影响阅读,如果想要看完整源码,可以在android的sdk中找一下TabHost.java,自行阅读。本文使用的是android4.4的源码。
TabHost是通过addTab接口,添加若干组件,然后可以实现在若干组件之间跳转。首先看看addTab的源码:
TabHost.java的addTab方法:
public void addTab(TabSpec tabSpec) {
......
View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView();
tabIndicator.setOnKeyListener(mTabKeyListener);
......
mTabWidget.addView(tabIndicator);
mTabSpecs.add(tabSpec);
......
}
addTab的参数是TabSpec类。TabSpec类是TabHost的内部类,记录了TabHost中组件的信息,里面包含了indicator和content,indicator是TabHost的选择卡上的相关信息,content是TabHost点击选择卡后所展示内容的信息。
对于indicator,你可以有两种设定方式:
1)设定一个label,也就是选择卡上显示的文字。
2)设定一个label和一个icon,也就是选择卡上显示的文字和显示的图标。
对于content,你可以有3中设定方式:
1)设定一个View,需要将要跳转的View写在TabHost的xml中。
2)设定一个intent,可以展示对应的activity。
3)设定一个TabHost.TabContentFactory,TabContentFactory是TabHost的一个内部接口,其中包含一个方法:createTabContent。使用的时候,需要继承TabContentFactory,并让createTabContent方法返回需要跳转的View。这种设定方式和第一种相比,不需要把要跳转的View的布局写在TabHost的xml中,可以写在其他的xml中或者直接在java中用代码实现一个View,然后在createTabContent方法中返回这个View,这个View就是点击选择卡后要跳转到的界面。
下来看一看TabSpec类的源码:
public class TabSpec {
private String mTag;
private IndicatorStrategy mIndicatorStrategy;
private ContentStrategy mContentStrategy;
private TabSpec(String tag) {
mTag = tag;
}
/**
* Specify a label as the tab indicator.
*/
public TabSpec setIndicator(CharSequence label) {
mIndicatorStrategy = new LabelIndicatorStrategy(label);
return this;
}
/**
* Specify a label and icon as the tab indicator.
*/
public TabSpec setIndicator(CharSequence label, Drawable icon) {
mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon);
return this;
}
/**
* Specify a view as the tab indicator.
*/
public TabSpec setIndicator(View view) {
mIndicatorStrategy = new ViewIndicatorStrategy(view);
return this;
}
/**
* Specify the id of the view that should be used as the content
* of the tab.
*/
public TabSpec setContent(int viewId) {
mContentStrategy = new ViewIdContentStrategy(viewId);
return this;
}
/**
* Specify a {@link android.widget.TabHost.TabContentFactory} to use to
* create the content of the tab.
*/
public TabSpec setContent(TabContentFactory contentFactory) {
mContentStrategy = new FactoryContentStrategy(mTag, contentFactory);
return this;
}
/**
* Specify an intent to use to launch an activity as the tab content.
*/
public TabSpec setContent(Intent intent) {
mContentStrategy = new IntentContentStrategy(mTag, intent);
return this;
}
public String getTag() {
return mTag;
}
}
主要看看setContent方法。一共有3种参数,分别是:int类型、intent类型和TabContentFactory类型。
接下来分别看一下实现:
int类型:
private class ViewIdContentStrategy implements ContentStrategy {
private final View mView;
private ViewIdContentStrategy(int viewId) {
mView = mTabContent.findViewById(viewId);
if (mView != null) {
mView.setVisibility(View.GONE);
} else {
throw new RuntimeException("Could not create tab content because " +
"could not find view with id " + viewId);
}
}
public View getContentView() {
mView.setVisibility(View.VISIBLE);
return mView;
}
}
private FrameLayout mTabContent;
mTabContent = (FrameLayout) findViewById(com.android.internal.R.id.tabcontent);
ContentStrategy是一个内部接口,里面有两个方法getContentView和tabClosed。在TabHost的选择卡发生改变的时候,会调用TabHost.setCurrentTab方法,这个方法中会调用spec.mContentStrategy.getContentView()获取跳转后要展示的View。
如果Tabspec.setContent的参数是int类型时,就会调用ViewIdContentStrategy.getContentView。同理,如果Tabspec.setContent的参数是intent类型,会调用IntentContentStrategy.getContentView;如果Tabspec.setContent的参数是TabContentFactory类型,会调用FactoryContentStrategy.getContentView。
TabContentFactory类型:
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;
}
}
mFactory是TabSpec.setContent传入的FactoryContentStrategy对象。mFactory.createTabContent方法是在Activity中实现的。
intent类型:
private class IntentContentStrategy implements ContentStrategy {
private final String mTag;
private final Intent mIntent;
private View mLaunchedView;
private IntentContentStrategy(String tag, Intent intent) {
mTag = tag;
mIntent = intent;
}
public View getContentView() {
......
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;
......
return mLaunchedView;
}
public void tabClosed() {
if (mLaunchedView != null) {
mLaunchedView.setVisibility(View.GONE);
}
}
}
protected LocalActivityManager mLocalActivityManager = null;
public void setup(LocalActivityManager activityGroup) {
setup();
mLocalActivityManager = activityGroup;
}
在IntentContentStrategy类中,可以看到getContentView方法的返回参数mLaunchedView就是mLocalActivityManager.startActivity(mTag, mIntent).getDecorView();
而mLocalActivityManager变量是在TabHost的setup(LocalActivityManager)中初始化的,这也就是为什么前面说,使用TabHost在Activity间跳转时,需要继承ActivityGroup类。并且在初始化时需要调用tab.setup(ActivityGroup.getLocalActivityManager());
ActivityGroup类的getLocalActivityManager方法可以获取LocalActivityManager对象,来看一看ActivityGroup的代码:
ActivityGroup.java
package android.app;
import java.util.HashMap;
import android.content.Intent;
import android.os.Bundle;
@Deprecated
public class ActivityGroup extends Activity {
protected LocalActivityManager mLocalActivityManager;
public ActivityGroup(boolean singleActivityMode) {
mLocalActivityManager = new LocalActivityManager(this, singleActivityMode);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
......
mLocalActivityManager.dispatchCreate(states);
}
@Override
protected void onResume() {
super.onResume();
mLocalActivityManager.dispatchResume();
}
@Override
protected void onPause() {
super.onPause();
mLocalActivityManager.dispatchPause(isFinishing());
}
@Override
protected void onStop() {
super.onStop();
mLocalActivityManager.dispatchStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mLocalActivityManager.dispatchDestroy(isFinishing());
}
public final LocalActivityManager getLocalActivityManager() {
return mLocalActivityManager;
}
}
可以看出,ActivityGroup类就是创建了LocalActivityManager对象,并且调用了LocalActivityManager的方法。所以,Tabspec_intent.java可以写成下面的这样:
Tabspec_intent.xml另一种写法:
public class TabspecIntent extends Activity {
LocalActivityManager mLocalActivityManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tabspec_intent);
TabHost tab = (TabHost) findViewById(android.R.id.tabhost);
mLocalActivityManager = new LocalActivityManager(this, true);
mLocalActivityManager.dispatchCreate(savedInstanceState);
//初始化TabHost容器
tab.setup(mLocalActivityManager);
//在TabHost创建标签,然后设置:标题/图标/标签页布局
tab.addTab(tab.newTabSpec("tab1").setIndicator("标签1" , getResources().getDrawable(R.drawable.ic_launcher)).setContent(new Intent(this, Tab1.class)));
tab.addTab(tab.newTabSpec("tab2").setIndicator("标签2" , null).setContent(new Intent(this, Tab2.class)));
tab.addTab(tab.newTabSpec("tab3").setIndicator("标签3" , null).setContent(new Intent(this, Tab3.class)));
}
@Override
protected void onResume() {
super.onResume();
mLocalActivityManager.dispatchResume();
}
@Override
protected void onPause() {
super.onPause();
mLocalActivityManager.dispatchPause(isFinishing());
}
@Override
protected void onStop() {
super.onStop();
mLocalActivityManager.dispatchStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mLocalActivityManager.dispatchDestroy(isFinishing());
}
}
如上面代码所示,Tabspec_intent.java可以继承Activity,只需要在Activity中创建LocalActivityManager对象,并调用LocalActivityManager的方法即可。
现在再来看看LocalActivityManager类是如何让Acttivity可以放入TabHost中的。前面说过,关键的代码是:
mLocalActivityManager.startActivity(mTag, mIntent).getDecorView();
mLocalActivityManager.java
public Window startActivity(String id, Intent intent) {
......
}
public Activity getActivity(String id) {
LocalActivityRecord r = mActivities.get(id);
return r != null ? r.activity : null;
}
看LocalActivityManager的源码,可以知道:每个要放入TabHost的Activity,只需要调用一次LocalActivityManager.startActivity即可,之后可以使用LocalActivityManager.getActivity获取Activity,然后再调用Activity.getWindow().getDecorView()获取View。当然,每次都调用LocalActivityManager.startActivity也是可以的。
也就是说,可以不使用TabHost,也可以达到点击选择卡,跳转到不同页面。
具体实现与LocalActivityManager的源码的源码解析有关。不过LocalActivityManager已经不建议使用了,建议使用Fragment,但是了解一下Tab实现方法,如果哪天有这个需要,也可以用一用。
可以看到代码中,的确给选择卡中设置了图片,可是效果图中并没有图片。
可以看到Tabspec.java中,给选择卡添加图片的接口如下:
public TabSpec setIndicator(CharSequence label, Drawable icon) {
mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon);
return this;
}
LabelAndIconIndicatorStrategy.java
private class LabelAndIconIndicatorStrategy implements IndicatorStrategy {
private final CharSequence mLabel;
private final Drawable mIcon;
private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) {
mLabel = label;
mIcon = icon;
}
public View createIndicatorView() {
final Context context = getContext();
LayoutInflater inflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View tabIndicator = inflater.inflate(mTabLayoutId,
mTabWidget, // tab widget is the parent
false); // no inflate params
final TextView tv = (TextView) tabIndicator.findViewById(R.id.title);
final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.icon);
......
return tabIndicator;
}
}
这里使用的布局是mTabLayoutId,这个变量的定义如下:
private int mTabLayoutId;
mTabLayoutId = R.layout.tab_indicator_holo;
R.layout.tab_indicator_holo在sdk中没有,源码中可以找到。在源码中搜索tab_indicator_holo,可以在
Intel_x86_sysimg_4.4_Source_Files_20131206\frameworks\base\core\res\res\layout文件夹中找到tab_indicator_holo.xml,代码如下:
tab_indicator_holo.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="?android:attr/actionBarSize"
android:orientation="horizontal"
style="@android:style/Widget.Holo.Tab">
<ImageView
android:id="@android:id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:visibility="gone" />
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
style="@android:style/Widget.Holo.TabText" />
LinearLayout>
上面的ImageView的visibility为gone。所以不会显示图片。
转载请注明出处,谢谢!