读源码:TabLayout

Posted Iaouei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读源码:TabLayout相关的知识,希望对你有一定的参考价值。

一,内部类及分析其关系:


Tab类和TabView类和SlidingTabStrip类为TabLayout提供了三个基本的元素。
TabLayoutOnPageChangeListener和ViewPagerOnTabSelectedListener实现了ViewPager类的两个接口,作用是监听ViewPager页面改变和Tab选中状态。
PagerAdapterObserver为观察者监控PagerAdapter数据变化。
所以在自定义TabLayout前,先设计好Tab模式,并且官方扩展了一个TabView子控件件。
自定义TabLayout的工作,还要通过接口完成和ViewPager的交互,通过PagerAdapterObserver,确保TabLayout能够正常工作。

二,内部类:Tab模型

Tab内部类定义了Tab的成员变量,并set和get方法,然后又封装了Tab的属性设置方法,如:

 @NonNull
        public Tab setIcon(@Nullable Drawable icon) 
            mIcon = icon;
            if (mPosition >= 0) 
                mParent.updateTab(mPosition);
            
            return this;
        

        /**
         * Set the icon displayed on this tab.
         *
         * @param resId A resource ID referring to the icon that should be displayed
         * @return The current instance for call chaining
         */
        @NonNull
        public Tab setIcon(@DrawableRes int resId) 
            return setIcon(AppCompatDrawableManager.get().getDrawable(mParent.getContext(), resId));
        

三,内部类:自定义的TabView

这个子view在应用中很方便,经常用到,设计它的作用是让Tab同时显示文字和图片,继承自LinearLayout。
其构造器设置了TabView在TabLayout中的状态,这些状态属性都是固定的不可变的,所以也决定TabView设置受到限制。

public TabView(Context context) 
            super(context);
            if (mTabBackgroundResId != 0) 
setBackgroundDrawable(AppCompatDrawableManager.get().getDrawable(context, mTabBackgroundResId));//设置背景
            
            ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,mTabPaddingEnd, mTabPaddingBottom);//设置边距
            setGravity(Gravity.CENTER);//设置位置
            setOrientation(VERTICAL);//设置方向
        

四,SlidingTabStrip:Tab下面的滑线

这个自定义view SlidingTabStrip是私有内部类,它继承自LinearLayout,作用为获取tab的最宽宽度,设置SlidingTabStrip的宽度,并设置一个动画,随着tab的改变绘制SlidingTabStrip。它有一个构造器:

SlidingTabStrip(Context context) 
            super(context);
            setWillNotDraw(false);//重写draw()不起作用时,需要添加这么一句
            mSelectedIndicatorPaint = new Paint();
        

单看这个构造器就知道使用SlidingTabStrip超级简单,在这个构造器源码中,有一个地方需要注意,因为在我们自定义view中会用到,就是setWillNotDraw(false);但看这句的字面意思就不难猜到,双重否定等于肯定,让它进行绘制。当我们自定义的view在onDrow()不起作用时,一种方法是在构造器里设置背景颜色,一种就是添加这句语句。原因可以进一步参考ViewGroup为什么不会调用onDraw
代码结构极易一目了然。在onMeasure()获取最宽tab宽度,并设置为此控件的宽度;在onLayout()进行动画并更新Indicator位置;最后draw()。

五,ViewPager的监听器:TabLayoutOnPageChangeListener

这个类是实现ViewPager.OnPageChangeListener的一个内部类,作用是监听到ViewPager页面改变的时候,更新tab和Indicator。首先注意这个类是public,并且被static修饰,static一般用来修饰成员变量或或成员函数,有static修饰内部类有什么特别呢,那就是在外面使用这个类的时候,不需要先new内部类所在的类(如在这里为TabLayout)的实例,可以直接new这个内部类的实例,比如:

TabLayout.TabLayoutOnPageChangeListener listener=new TabLayout.TabLayoutOnPageChangeListener;

如果想进一步了解可以查看用static关键字修饰类
其它没啥可解释的,方便理解,贴出主要的代码块:

 @Override
        public void onPageScrolled(int position, float positionOffset,
                int positionOffsetPixels) 
            final TabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null) 
                // 只更新选中的 text  if we're not settling, or we are settling after
                // being dragged
                final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
                        mPreviousScrollState == SCROLL_STATE_DRAGGING;
                // 更新indicator if we're not settling after being idle. This is caused
                // 这是由setCurrentItem() 引起的,并且会由onPageSelected()传来的动画处理。
                final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
                        && mPreviousScrollState == SCROLL_STATE_IDLE);
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            
        

        @Override
        public void onPageSelected(int position) 
            final TabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null && tabLayout.getSelectedTabPosition() != position) 
                //选中 tab, 如果没有 dragged/settled只更新indicator 
                // (since onPageScrolled will handle that).
                final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
                        || (mScrollState == SCROLL_STATE_SETTLING
                        && mPreviousScrollState == SCROLL_STATE_IDLE);
                tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
            
        

        private void reset() //此内部类外部调用,重置
            mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
        

六,TabLayout的tab选中监听:ViewPagerOnTabSelectedListener

方便理解,贴出这个内部类和上个内部类的类声明:

public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener 
public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener 

除了这个内部类也是public static外,与ViewPager.OnPageChangeListener相对应的是这里实现的是TabLayout.OnTabSelectedListener;这个接口是TabLayout中定义的接口,在这里实现的作用是使TabLayout中的tab能够和ViewPager的pager保持同步。
然后在TabLayout中有一个方法:

public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) 
        mOnTabSelectedListener = onTabSelectedListener;
    

mOnTabSelectedListener是这个内部类ViewPagerOnTabSelectedListener的一个实例,我们使用TabLayout时,就是通过调用这个方法设置tab的监听事件。

七,数据监听:PagerAdapterObserver

这是一个观察者,继承自DataSetObserver,DataSetObserver我不多说了,关于介绍推荐Android中的观察者DataSetObservable和DataSetObserver ,用法如果不理解可以先复习一下观察者模式,然后再百度一下。
这儿,PagerAdapterObserver实现的作用很简单,源码为下:

//监听适配器数据的变化
    private class PagerAdapterObserver extends DataSetObserver 
        //数据集改变时
        @Override
        public void onChanged() 
            populateFromPagerAdapter();
        
//当数据集失效时
        @Override
        public void onInvalidated() 
            populateFromPagerAdapter();
        
    

数据集改变和数据集失效执行的是同一个动作。populateFromPagerAdapter()是放在TabLayout中的一个方法,作用为按照page的数量添加tab,并设置文字,然后又添了一道保障。方便理解,这儿贴出:

private void populateFromPagerAdapter() 
        removeAllTabs();

        if (mPagerAdapter != null) 
            final int adapterCount = mPagerAdapter.getCount();//得到page的数量
            for (int i = 0; i < adapterCount; i++) 
                addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);//添加tab,并设置文字
            

            // 确保映射到了当前设置的 ViewPager item
            if (mViewPager != null && adapterCount > 0) 
                final int curItem = mViewPager.getCurrentItem();
                if (curItem != getSelectedTabPosition() && curItem < getTabCount()) 
                    selectTab(getTabAt(curItem));//选中tab
                
            
         else 
            removeAllTabs();
        
    

八,从头到尾缕TabLayout

这个类,先是设置了一些默认值,然后定义了两个tabs模式:MODE_SCROLLABLE和MODE_FIXED,两个tabs放进tablayout的位置:GRAVITY_FILL和GRAVITY_CENTER;然后设置了一个tab选中监听器:public interface OnTabSelectedListener,这个接口在上面介绍过的一个内部类已经实现;再接着是声明的TabLayout一些属性和一些用到的类的实例。
然后就到了构造器,看第三个构造器:

//第三个构造器
    public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);

        ThemeUtils.checkAppCompatTheme(context);

        // 使Scroll Bar不可用
        setHorizontalScrollBarEnabled(false);

        // 添加TabStrip
        mTabStrip = new SlidingTabStrip(context);
        addView(mTabStrip, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);

        //获取属性值
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
                defStyleAttr, R.style.Widget_Design_TabLayout);

        //加载属性。。。

        // 加载tab mode and gravity
        applyModeAndGravity();
    

这个构造器可以分为四个小步,获取属性值后的加载属性有兴趣可以看看源码,重点看applyModeAndGravity();理解如何加载tab mode 和 gravity的,set get有兴趣看源码,applyModeAndGravity(源码为:

//加载设置的模式和布局方式。。。
    private void applyModeAndGravity() 
        int paddingStart = 0;
        if (mMode == MODE_SCROLLABLE) 
            // If we're scrollable, or fixed at start, inset using padding
            paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
        
        ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);

        switch (mMode) 
            case MODE_FIXED:
                mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
                break;
            case MODE_SCROLLABLE:
                mTabStrip.setGravity(GravityCompat.START);
                break;
        

        updateTabViews(true);//更新TabViews
    

    private void updateTabViews(final boolean requestLayout) 
        for (int i = 0; i < mTabStrip.getChildCount(); i++) 
            View child = mTabStrip.getChildAt(i);
            child.setMinimumWidth(getTabMinWidth());
            updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
            if (requestLayout) 
                child.requestLayout();
            
        

构造器接下来就是一些属性的set get方法,和执行Tab事务的方法。我这里跳过直接看TabLayout最关键逻辑部分setupWithViewPager()。

public void setupWithViewPager(@Nullable final ViewPager viewPager) 
        if (mViewPager != null && mPageChangeListener != null) 
            // 如果已经和ViewPager建立关系, 从它移除
            mViewPager.removeOnPageChangeListener(mPageChangeListener);
        

        if (viewPager != null) 
            final PagerAdapter adapter = viewPager.getAdapter();
            if (adapter == null) 
                throw new IllegalArgumentException("ViewPager does not have a PagerAdapter set");
            

            mViewPager = viewPager;

            // 向ViewPager添加我们自定义的 OnPageChangeListener
            if (mPageChangeListener == null) 
                mPageChangeListener = new TabLayoutOnPageChangeListener(this);
            
            mPageChangeListener.reset();//重置
            viewPager.addOnPageChangeListener(mPageChangeListener);//添加

            // 再添加tab selected listener 去设置 ViewPager当前的item
            setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager));

            // 最后把 pager adapter转移过来
            setPagerAdapter(adapter, true);//设置PagerAdapter
         else 
            // 我们已经给出了个空的ViewPager ,所以需要去清除内部的状态 ,监听器,观察者,
            mViewPager = null;
            setOnTabSelectedListener(null);
            setPagerAdapter(null, true);
        
    

setupWithViewPager()做的工作是和ViewPager建立关系,将PagerAdapter适配到tabLayout,所以其中关键又是setPagerAdapter():

//设置PagerAdapter
    private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) 
        if (mPagerAdapter != null && mPagerAdapterObserver != null) 
            // 如果我们已经有了PagerAdapter, 取消注册observer
            mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
        

        mPagerAdapter = adapter;

        if (addObserver && adapter != null) 
            //在新的adapter注册观察者
            if (mPagerAdapterObserver == null) 
                mPagerAdapterObserver = new PagerAdapterObserver();
            
            adapter.registerDataSetObserver(mPagerAdapterObserver);
        

        // 最后确保映射到了这个新的adapter
        populateFromPagerAdapter();
    

setPagerAdapter()注册一个观察者,观察adapter的变化,然后根据变化映射到tab,是tab也发生相应变化,这个步骤放在了populateFromPagerAdapter(),这个方法在前面已经贴出,其做的事情是根据情况添加addTab(),选中selectTab(),移除removeAllTabs()。其中选中selectTab()逻辑有稍微复杂些:

//选中tab
    void selectTab(Tab tab, boolean updateIndicator) 
        if (mSelectedTab == tab) 
            if (mSelectedTab != null) 
                if (mOnTabSelectedListener != null) 
                    mOnTabSelectedListener.onTabReselected(mSelectedTab);//释放选中的tab
                
                animateToTab(tab.getPosition());//进行动画
            
         else 
            if (updateIndicator) //如果updateIndicator
                final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
                if (newPosition != Tab.INVALID_POSITION) 
                    setSelectedTabView(newPosition);//设置选中的TabView
                
                if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION)
                        && newPosition != Tab.INVALID_POSITION) 
                    // 如果没有正确的tab, 只 draw the indicator
                    setScrollPosition(newPosition, 0f, true);
                 else 
                    animateToTab(newPosition);//进行动画
                
            
            if (mSelectedTab != null && mOnTabSelectedListener != null) 
                mOnTabSelectedListener.onTabUnselected(mSelectedTab);
            
            mSelectedTab = tab;
            if (mSelectedTab != null && mOnTabSelectedListener != null) 
                mOnTabSelectedListener.onTabSelected(mSelectedTab);
            
        
    
//动画  改变Tab时的动画
    private void animateToTab(int newPosition) 
        if (newPosition == Tab.INVALID_POSITION) 
            return;
        

        if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
                || mTabStrip.childrenNeedLayout()) 
            // If we don't have a window token, or we haven't been laid out yet just draw the new
            // position now
            setScrollPosition(newPosition, 0f, true);
            return;
        

        final int startScrollX = getScrollX();
        final int targetScrollX = calculateScrollXForTab(newPosition, 0);

        if (startScrollX != targetScrollX) 
            if (mScrollAnimator == null) 
                mScrollAnimator = ViewUtils.createAnimator();
                mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
                mScrollAnimator.setDuration(ANIMATION_DURATION);
                mScrollAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() 
                    @Override
                    public void onAnimationUpdate(ValueAnimatorCompat animator) 
                        scrollTo(animator.getAnimatedIntValue(), 0);
                    
                );
            

            mScrollAnimator.setIntValues(startScrollX, targetScrollX);
            mScrollAnimator.start();
        

        // Now animate the indicator
        mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
    
//计算水平方向的滑动
    private int calculateScrollXForTab(int position, float positionOffset) 
        if (mMode == MODE_SCROLLABLE) 
            final View selectedChild = mTabStrip.getChildAt(position);
            final View nextChild = position + 1 < mTabStrip.getChildCount()
                    ? mTabStrip.getChildAt(position + 1)
                    : null;
            final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
            final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;

            return selectedChild.getLeft()
                    + ((int) ((selectedWidth + nextWidth) * positionOffset * 0.5f))
                    + (selectedChild.getWidth() / 2)
                    - (getWidth() / 2);
        
        return 0;
    

其他的方法都比较常见,很好理解,就不做解释了。最后推荐一篇非常不错的使用TabLayout及要点深入博文,希望对完全理解有所帮助:TabLayout:另一种Tab的实现方式

以上是关于读源码:TabLayout的主要内容,如果未能解决你的问题,请参考以下文章

Android TabLayout 使用进阶(含源码)

在jdbc基础上进阶一小步的C3p0 连接池(DBCP 不能读xml配置文件,已淘汰) 和DBUtils 中两个主要类QueryRunner和ResultSetHandler的使用

错误记录TabLayout 升级支持库版本后报错 ( support:design 支持库升级到 28.0.0 后源码发生变更 )

TabLayout中Indicator的样式修改

TabLayout如何设置下划线(Indicator)宽度

TabLayout设置下划线(Indicator)宽度