zoukankan      html  css  js  c++  java
  • 【转载】TabLayout 源码解析

    原文地址:https://github.com/Aspsine/AndroidSdkSourceAnalysis/blob/master/article/TabLayout%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md

    1. 功能介绍

    1.1 TabLayout

    Tabs跟随Actionbar在Android 3.0进入大家的视线,是一个很经典的设计。它也是Material Design 规范中提及的Component之一。Tabs or Bottom navigation?相信不少Android开发者与产品都撕过,就连微信在其中也有过抉择。Google在Google+以及Google Photo中相继采用Bottom navigation的设计把剧情推到向高潮,一度轰动整个社区。Google继而在Material Design 规范加入了Bottom navigation,表明了态度,也给这起争论画上了圆满的句号。

    在 support desgin lib 发布前,大家基本都采用PagerSlidingTabStrip来实现tab效果。其实TabLayout在实现上和PagerSlidingTabStrip十分相似,今天我们来分析TabLayout

    1.2 TabLayout使用

    TabLayout使用比较简单。既可以单独使用,也可以与ViewPager配合使用。

    1.2.1 TabLayout单独使用

    在java代码中添加Tabs

    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
    tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
    tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
    tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));

     

    也可以在xml中添加Tabs

    <android.support.design.widget.TabLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent">
    
        <android.support.design.widget.TabItem
            android:text="@string/tab_text"/>
    
        <android.support.design.widget.TabItem
            android:icon="@drawable/ic_android"/>
    
    </android.support.design.widget.TabLayout>

     

    1.2.2 与ViewPager搭配使用

    // find view
    TabLayout tabLayout = ...;
    ViewPager viewPager = ...;
    
    PagerAdapter adapter = new PagerAdapter(){
        // ...Override some methods
        // TabLayout调用这个方法获取Tab的title
        @Override
        public CharSequence getPageTitle(int position) {
            return "Tab 1";
        }
    }
    viewPager.setAdapter(adapter);
    tabLayout.setupWithViewPager(viewPager);

     

    2. 总体设计

    • TabLayout继承HorizontalScrollView天生就是一个可以横向滚动的ViewGroup. 我们知道,HorizontalScrollViewScrollView一样, 最多只能包含一个子View.

    • SlidingTabStrip继承于LinearLayout,是TabLayout的内部类。它是TabLayout唯一的子View. 所有的TabView都是它的子View.

    • TabView继承于LinearLayout,以Tab为数据源,来展示Tab的样式。最终用for循环被add进SlidingTabStrip.

    • Tab是一个简单的View Model实体类,控制TabView的title, icon, custom layout id等属性。

    • TabItem继承于View. 用于在layout xml中来描述Tab. 需要注意的是,它不会add到SlidingTabStrip中去。它的作用是从xml中获取到text,icon,custom layout id等属性。TabLayout inflate到TabItem并获取属性到装配到Tab中,最终add到SlidingTabStrip中的还是TabView.

    • OnTabSelectedListener是TabLayout中的内部接口,用于监听SlidingTabStrip中子TabView选中状态的改变。

    • Mode是TabLayout滚动模式的描述,一共有两种状态。MODE_FIXED不可滚动模式,以及MODE_SCROLLABLE可以滚动模式。

    • GravityTabViewSlidingTabStrip中layout方式的描述。分为:GRAVITY_FILL,GRAVITY_CENTER.

    3. 详细设计

    3.1 类关系图

    TabLayout

    3.2 分析

    3.2.1 TabLayout子View唯一性保证

    前面介绍TabLayout继承于HorizontalScrollView最多只能有1个子View. 但TabLayout可以在layout中添加多个子View节点. 这是怎么回事呢?

    <android.support.design.widget.TabLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent">
    
        <android.support.design.widget.TabItem
            android:text="@string/tab_text"/>
    
        <android.support.design.widget.TabItem
            android:icon="@drawable/ic_android"/>
    
    </android.support.design.widget.TabLayout>

     

    看过LayoutInflater源码的同学可能会知道这个过程:先inflate到生成View对象,再调用ViewGroup#addView(...)系列方法把view添加到ViewGroup中。我们发现TabLayout的addView(...)系列方法,都删去super调用,且调用了共同的一个方法,addViewInternal(View view)

    private void addViewInternal(final View child) {
        if (child instanceof TabItem) {
            addTabFromItemView((TabItem) child);
        } else {
            throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout");
        }
    }

     

    可见,若child非TabItem对象会抛出异常。所以xml中给TabLayout添加tab时,只能添加TabItem对象。若想添加其它View类型怎么办?TabItem有android:customView这个属性。我们继续来看。

    private void addTabFromItemView(@NonNull TabItem item) {
        final Tab tab = newTab();
        if (item.mText != null) {
            tab.setText(item.mText);
        }
        if (item.mIcon != null) {
            tab.setIcon(item.mIcon);
        }
        if (item.mCustomLayout != 0) {
            tab.setCustomView(item.mCustomLayout);
        }
        addTab(tab);
    }
    
    public Tab newTab() {
        Tab tab = sTabPool.acquire();
        if (tab == null) {
            tab = new Tab();
        }
        tab.mParent = this;
        tab.mView = createTabView(tab);
        return tab;
    }
    
    private TabView createTabView(@NonNull final Tab tab) {
        TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
        if (tabView == null) {
            tabView = new TabView(getContext());
        }
        tabView.setTab(tab);
        tabView.setFocusable(true);
        tabView.setMinimumWidth(getTabMinWidth());
        return tabView;
    }

     

    这里调newTab()方法创建了一个tab对象,并且用对象池把创建的tab对象缓存起来。然后将TabItem对象的属性都赋值给tab对象。在createTabView(Tab tab)这个方法中,首先从TabView池中获取TabView对象,如果不存在,则实例化一个对象,并调用tabView.setTab(tab)方法来进行了数据绑定。 addTab(...)有三个重载方法,最终都会调用如下方法:

    public void addTab(@NonNull Tab tab, boolean setSelected) {
        if (tab.mParent != this) {
            throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
        }
    
        addTabView(tab, setSelected);
        configureTab(tab, mTabs.size());
        if (setSelected) {
            tab.select();
        }
    }
    
    private void addTabView(Tab tab, int position, boolean setSelected) {
        final TabView tabView = tab.mView;
        mTabStrip.addView(tabView, position, createLayoutParamsForTabs());
        if (setSelected) {
            tabView.setSelected(true);
        }
    }
    
    private void configureTab(Tab tab, int position) {
        tab.setPosition(position);
        mTabs.add(position, tab);
    
        final int count = mTabs.size();
        for (int i = position + 1; i < count; i++) {
            mTabs.get(i).setPosition(i);
        }
    }

     

    addView(Tab, int, boolean)方法中,把TabView对象add进了SlidingTabStrip这个ViewGroup中。实际上SlidingTabStrip的对象mTabStrip才是TabLayout的唯一子View.在TabLayout的构造方法中:

    public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        // 禁用横向滑动条
        setHorizontalScrollBarEnabled(false);
    
        // new 一个'SlidingTabStrip'的实例,并作为唯一的子View add进'TabLayout'.
        mTabStrip = new SlidingTabStrip(context);
        super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
    
        // 省略下面的无关代码...

     

    至此,我们就明白了TabLayout中子View的一致性是如何保证的。也明白了TabView其实才是亲生的,TabItem其实是后娘养的! 这些代码都很简单,不过我们可以从中学习到很多有用的思想。

    至此,一个清晰的View层级图应该就出现在了各位同学的眼前。 TabLayout Hierarchy

    3.2.2 与ViewPager搭配使用

    有了上面的的基础,我们再来看看TabLayout是如何和它的好基友ViewPager搭配使用的。

    public void setupWithViewPager(@Nullable final ViewPager viewPager) {
        //...
        //为理解简单起见,删掉边角性干扰代码,主要来看核心逻辑
    
        mViewPager = viewPager;
    
        // Add our custom OnPageChangeListener to the ViewPager
        if (mPageChangeListener == null) {
            mPageChangeListener = new TabLayoutOnPageChangeListener(this);
        }
        mPageChangeListener.reset();
        viewPager.addOnPageChangeListener(mPageChangeListener);
    
        // Now we'll add a tab selected listener to set ViewPager's current item
        setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(viewPager));
    
        // Now we'll populate ourselves from the pager adapter
        setPagerAdapter(adapter, true);
    }
    
    public void setOnTabSelectedListener(OnTabSelectedListener onTabSelectedListener) {
        mOnTabSelectedListener = onTabSelectedListener;
    }
    
    private void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
        if (mPagerAdapter != null && mPagerAdapterObserver != null) {
            // If we already have a PagerAdapter, unregister our observer
            mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
        }
    
        mPagerAdapter = adapter;
    
        if (addObserver && adapter != null) {
            // Register our observer on the new adapter
            if (mPagerAdapterObserver == null) {
                mPagerAdapterObserver = new PagerAdapterObserver();
            }
            adapter.registerDataSetObserver(mPagerAdapterObserver);
        }
    
        // Finally make sure we reflect the new adapter
        populateFromPagerAdapter();
    }

     

    这里的TabLayoutOnPageChangeListener实现了ViewPager.OnPageChangeListener. 首先调用ViewPager对象addOnPageChangeListener(OnPageChangeListener)来监听ViewPager的滑动以及当前也的选中。然后设置ViewPagerOnTabSelectedListener对象,保证ViewPager的页面和TabLayout的item的选中状态保持一致,以及滚动的协同性。这里的监听在3.2.3中详细讲解。

    我们一般调用viewPager.getAdapter().notifyDataSetChanged()来进行ViewPager的刷新. 现在我们在ViewPager的adapter中注册一个监听器,监听ViewPager的刷新行为。目的是为了刷新ViewPager的同时也可以刷新TabLayout. 我们来看看PagerAdapterObserver这个监听器是如何刷新TabLayout的。

    private class PagerAdapterObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            populateFromPagerAdapter();
        }
    
        @Override
        public void onInvalidated() {
            populateFromPagerAdapter();
        }
    }
    
    private void populateFromPagerAdapter() {
        removeAllTabs();
    
        if (mPagerAdapter != null) {
            final int adapterCount = mPagerAdapter.getCount();
            for (int i = 0; i < adapterCount; i++) {
                addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
            }
    
            // Make sure we reflect the currently set ViewPager item
            if (mViewPager != null && adapterCount > 0) {
                final int curItem = mViewPager.getCurrentItem();
                if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
                    selectTab(getTabAt(curItem));
                }
            }
        } else {
            removeAllTabs();
        }
    }
    
    public void removeAllTabs() {
        // Remove all the views
        for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
            removeTabViewAt(i);
        }
    
        for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) {
            final Tab tab = i.next();
            i.remove();
            tab.reset();
            sTabPool.release(tab);
        }
    
        mSelectedTab = null;
    }

     

    刷新方式很简单粗暴,从SlidingTabStrip对象中移除所有的TabView,继而从View ModelmTabs中移除所有Tab对象。然后从adapter中获取tab信息,循环调用addTab(Tab, boolean)方法重新添加TabView。最后调用ViewPager对象的getCurrentItem()方法,获取当前位置,然后调用selectTab(int position)恢复TabView的选中状态(针对TabView的选中,3.2.4中有详细介绍)。

    3.2.3 ViewPager与TabLayout的Tab及indicaotr协同滚动

    public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
        private final WeakReference<TabLayout> mTabLayoutRef;
        private int mPreviousScrollState;
        private int mScrollState;
    
        public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
            mTabLayoutRef = new WeakReference<>(tabLayout);
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
            mPreviousScrollState = mScrollState;
            mScrollState = state;
        }
    
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            final TabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null) {
                // Only update the text selection if we're not settling, or we are settling after
                // being dragged
                final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
                        mPreviousScrollState == SCROLL_STATE_DRAGGING;
                // Update the indicator if we're not settling after being idle. This is caused
                // from a setCurrentItem() call and will be handled by an animation from
                // onPageSelected() instead.
                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) {
                // Select the tab, only updating the indicator if we're not being dragged/settled
                // (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;
        }
    }

     

    用过ViewPager的同学对OnPageChangeListener不会陌生,不多赘述。TabLayoutOnPageChangeListener实现了OnPageChangeListener, 在onPageScrolled(...)方法中做协同滚动处理。滚动的条件是:

    final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING && mPreviousScrollState == SCROLL_STATE_IDLE);

     

    调用TabLayout的setScrollPosition(...)方法来控制TabLayoutTabView和indocator的协同滚动。

    private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition) {
        final int roundedPosition = Math.round(position + positionOffset);
        if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
            return;
        }
    
        // Set the indicator position, if enabled
        if (updateIndicatorPosition) {
            mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
        }
    
        // Now update the scroll position, canceling any running animation
        if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
            mScrollAnimator.cancel();
        }
        scrollTo(calculateScrollXForTab(position, positionOffset), 0);
    
        // Update the 'selected state' view as we scroll, if enabled
        if (updateSelectedText) {
            setSelectedTabView(roundedPosition);
        }
    }

     

    3.2.3.1 TabLayout的Indicator协同滚动

    indicator的滚动由SlidingTabStrip来处理: ``

    // Set the indicator position, if enabled
    if (updateIndicatorPosition) {
        mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
    }

     

    这里的position是当前选中的位置。positionOffset是: 距当前Tab滑动的距离从当前tab滑动到下一个tab的总距离 这样一个范围在[0,1]间的小数。

    SlidingTabStrip#setIndicatorPositionFromTabPosition(int, float)

    void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
        if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
            mIndicatorAnimator.cancel();
        }
    
        mSelectedPosition = position;
        mSelectionOffset = positionOffset;
        updateIndicatorPosition();
    }

     

    SlidingTabStrip#updateIndicatorPosition()

    private void updateIndicatorPosition() {
        final View selectedTitle = getChildAt(mSelectedPosition);
        int left, right;
    
        if (selectedTitle != null && selectedTitle.getWidth() > 0) {
            left = selectedTitle.getLeft();
            right = selectedTitle.getRight();
    
            if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
                // Draw the selection partway between the tabs
                View nextTitle = getChildAt(mSelectedPosition + 1);
                left = (int) (mSelectionOffset * nextTitle.getLeft() +
                        (1.0f - mSelectionOffset) * left);
                right = (int) (mSelectionOffset * nextTitle.getRight() +
                        (1.0f - mSelectionOffset) * right);
            }
        } else {
            left = right = -1;
        }
    
        setIndicatorPosition(left, right);
    }

     

    通过getChildAt(mSelectedPosition), 获取到到mSelectedPosition处的TabView。若滑动的mSelectionOffset>0f且当前选中的位置mSelectedPosition不是最后一个TabView. 获取到下一个TabView,并计算出indicator的left和right。

    SlidingTabStrip#setIndicatorPosition(int, int)

    private void setIndicatorPosition(int left, int right) {
        if (left != mIndicatorLeft || right != mIndicatorRight) {
            // If the indicator's left/right has changed, invalidate
            mIndicatorLeft = left;
            mIndicatorRight = right;
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

     

    非常简单的代码,在调用ViewCompat.postInvalidateOnAnimation(this)重绘View之前,去掉一些重复绘制的帧。

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    
        // Thick colored underline below the current selection
        if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
            canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
                    mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
        }
    }

     

    绘制逻辑很简单。调用canvas.drawRect(float left, float top, float right, float bottom, Paint paint)来绘制indicator.这里:

    left = mIndicatorLeft;
    top = getHeight() - mSelectedIndicatorHeight;
    right = mIndicatorRight;
    bottom = getHeight();

     

    3.2.3.2 TabLayout的TabView协同滚动

    我们回头来看 3.2.3中setScrollPosition(...)方法

    private void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, boolean updateIndicatorPosition) {
        final int roundedPosition = Math.round(position + positionOffset);
        if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
            return;
        }
    
        // Set the indicator position, if enabled
        if (updateIndicatorPosition) {
            mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
        }
    
        // Now update the scroll position, canceling any running animation
        if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
            mScrollAnimator.cancel();
        }
        scrollTo(calculateScrollXForTab(position, positionOffset), 0);
    
        // Update the 'selected state' view as we scroll, if enabled
        if (updateSelectedText) {
            setSelectedTabView(roundedPosition);
        }
    }

     

    在3.2.3.1中我们知道indicator的滚动是通过mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset)实现的。那TabView的滚动呢?我们知道TabLayout是继承HorizonScrollView天生就是一个可以横行滚动的View,所以,我们只需要调用scrollTo(int x, int y)方法就可以实现横向滚动。

    scrollTo(calculateScrollXForTab(position, positionOffset), 0);

     

    这里x方向的偏移量调用calculateScrollXForTab(position, positionOffset)实时计算得出,y方向的偏移量为0。

    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是如何随ViewPager的滚动而滚动的。

    3.2.4 Tab选中状态

    private void setSelectedTabView(int position) {
        final int tabCount = mTabStrip.getChildCount();
        if (position < tabCount && !mTabStrip.getChildAt(position).isSelected()) {
            for (int i = 0; i < tabCount; i++) {
                final View child = mTabStrip.getChildAt(i);
                child.setSelected(i == position);
            }
        }
    }

     

    调用View的setSelected(boolean)方法。

  • 相关阅读:
    VS2013 自动添加头部注释 -C#开发
    在调用Response.End()时,会执行Thread.CurrentThread.Abort()操作
    React
    WebApi基础
    wcf
    memcached系列
    Ioc容器Autofac系列
    使用TortoiseSVN创建版本库
    使用libcurl 发送post请求
    值得推荐的C/C++框架和库
  • 原文地址:https://www.cnblogs.com/anni-qianqian/p/6830999.html
Copyright © 2011-2022 走看看