tabhost简单使用及tabhost源码分析

本文原创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的源码,有分析的不到位的地方还望指正,谢谢!

DEMO下载地址:http://download.csdn.net/detail/yanbin1079415046/4566715















 

你可能感兴趣的:(String,null,Class,tabs,interface,methods)