TabHost的使用及原理

一、TabHost是什么

TabHost在文档中的定义是:Container for a tabbed window view.直译过来是一个选项卡式窗口视图容器。简单来说,就是类似于微信首页那种,有几个选择卡,点击选择卡可以跳到不同的页面。

二、在了解TabHost之前,有以下知识是需要了解的:

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会跳转到哪里。(如果这里不懂没有关系,讲原理的时候会加上源码,到时候就懂了)

三、TabHost有3种实现方式:(通过TabSpec的setContent方法实现)

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));
    }

}

效果图:
TabHost的使用及原理_第1张图片 TabHost的使用及原理_第2张图片 TabHost的使用及原理_第3张图片

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());

效果图:

Tab1.java效果图:
TabHost的使用及原理_第4张图片
Tab2.java效果图:
TabHost的使用及原理_第5张图片
Tab3.java效果图:
TabHost的使用及原理_第6张图片
TabHost效果图:
TabHost的使用及原理_第7张图片 TabHost的使用及原理_第8张图片 TabHost的使用及原理_第9张图片

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的使用及原理_第10张图片 TabHost的使用及原理_第11张图片 TabHost的使用及原理_第12张图片

到这里为止,所有的使用都讲完了,源码可以在此处下载。源码下载

接下来要讲TabHost的原理了,对原理感兴趣的朋友可以看一看。本文会对TabHost的源码进行讲解,本文展示的源码不是全部源码,只截出一些重要的代码放出,基本不影响阅读,如果想要看完整源码,可以在android的sdk中找一下TabHost.java,自行阅读。本文使用的是android4.4的源码。

五、TabHost原理讲解

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。所以不会显示图片。

转载请注明出处,谢谢!

你可能感兴趣的:(Android-UI)