天天看點

基于TabLayout源碼實作自定義TabLayout

目錄

  • TabLayout原理
  • 具體實作
  • 遇到的問題
  • 總結

一、TabLayout原理

1.1 TabLayout與ViewPager的綁定原理

往往TabLayout都是和ViewPager關聯使用,下面就從TabLayout源碼進行分析ViewPager和TabLayout如何配合使用。

下面的代碼是最簡單的一個viewpager+tablayout+fragment的使用場景,那麼最開始就從setupWithViewPage()對源碼進行分析。

mFragments = new ArrayList<>();
        mFragments.add(new NewsTypeFragment());
        mFragments.add(new NewsTypeFragment());
        mFragments.add(new NewsTypeFragment());
        mViewPagerFragmentAdapter = new ViewPagerFragmentAdapter(getChildFragmentManager(), mFragments);
        viewpager.setAdapter(mViewPagerFragmentAdapter);
        tablayout.setupWithViewPager(viewpager);
           

viewpager和tablayout存在雙向綁定的機制:

基于TabLayout源碼實作自定義TabLayout

綁定流程如下:

基于TabLayout源碼實作自定義TabLayout

通過監聽viewpager, 與之綁定的TabLayout也随viewpager更改視圖,以下是TabLayoutOnPageChangeListener的源碼。

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(final int state) {
            mPreviousScrollState = mScrollState;
            mScrollState = state;
        }

        @Override
        public void onPageScrolled(final int position, final float positionOffset,
                final 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(final int position) {
            final TabLayout tabLayout = mTabLayoutRef.get();
            if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
                    && position < tabLayout.getTabCount()) {
                // 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);
            }
        }

        void reset() {
            mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
        }
    }
           

其中onPageScrollStateChanged() 得到viewpager的三種狀态,并儲存前置狀态和目前狀态,影響後續頁面布局和動畫效果。

/**
     * Indicates that the pager is in an idle, settled state. The current page
     * is fully in view and no animation is in progress.
     * 表示viewpager的狀态為靜止狀态(無動畫、無滑動)
     */
    public static final int SCROLL_STATE_IDLE = ;

    /**
     * Indicates that the pager is currently being dragged by the user.
     * 表示viewpager的狀态滑動狀态
     */
    public static final int SCROLL_STATE_DRAGGING = ;

    /**
     * Indicates that the pager is in the process of settling to a final position.
     */
    public static final int SCROLL_STATE_SETTLING = ;
           

public void onPageScrolled(final int position, final float positionOffset,final int positionOffsetPixels)該方法監聽的是Viewpager的位置以及每個page的偏移量(這裡解釋一下positionOffset,它對應ViewPager目前page的偏移量,其中左劃數值從0-1,右滑數值從1-0,後續會根據positionOffset計算整個HorizontalScrollView的位置)、對應的像素位置,==onPageScrolled()和下面onPageSelected() 是與TabLayout關聯最關鍵的兩個方法== ,在這個方法中,會将position和positionOffset傳遞給setScrollPosition(),并通過這個方法更新TabLayout視圖,其中包括,底部indicater(tab追蹤條),text(tab的名稱),HorizontalScrollView的偏移位置,運用對偏移量四舍五入的計算方法,設定tab标題顔色。這裡要尤其注意,onPageScrolled傳回的position會根據滑動方向改變,左滑position保持目前pager的值,而從靜止開始往右滑動則變成目前page-1,尤其區分這裡的position和onPageSelected傳回的position。

void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
                           boolean updateIndicatorPosition) {
        final int roundedPosition = Math.round(position + positionOffset);
        if (roundedPosition <  || 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), );

        // Update the 'selected state' view as we scroll, if enabled
        if (updateSelectedText) {
            setSelectedTabView(roundedPosition);
        }
    }
           

public void onPageSelected(final int position) 該方法隻有在動畫完成,頁面靜止的時候調用,position顯示目前page的頁數(從0開始)

二、具體實作

2.1 tab底部indicator自定義

原生TabLayout的底部indicator預設是矩形條,并且隻能修改其高度,是以它的可定制性非常低,而繪制矩形條的類SlidingTabStrip是私密内部類,是以為了自定義indcator需要将tablayout整體移植到自己的工程項目内,并修改SlidingTabStrip這個類。這裡提供簡單的三種自定義圖形

@Override
        public void draw(Canvas canvas) {
            super.draw(canvas);

            // Thick colored underline below the current selection
            if (mIndicatorLeft >=  && mIndicatorRight > mIndicatorLeft) {
                //自定義畫圓
                //canvas.drawCircle((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
                //自定義三角形
                Path path = new Path();
                path.moveTo((mIndicatorLeft + mIndicatorRight) / , getHeight() - mSelectedIndicatorHeight - );
                path.lineTo((mIndicatorLeft + mIndicatorRight) /  - mSelectedIndicatorHeight - , getHeight());
                path.lineTo((mIndicatorLeft + mIndicatorRight) /  + mSelectedIndicatorHeight + , getHeight());
                path.close();
                canvas.drawPath(path, mSelectedIndicatorPaint);
                //自定義矩形、條形(預設)
                //canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
                // mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
            }
        }
           

2.2 tab滑動機制自定義

通常TabLayout與fragment+ViewPager一起使用,不知道大家有沒有遇到過這種情況,當設定ViewPager的setCurrentItem方法時,可以選擇pager的滑動是否是smooth,true的時候,tablayout也是smooth,false的時候,tablayout的切換也變得生硬,包括現在的網易新聞,今日頭條的tablayout就是這種機制。産生這種不協調的原因是因為上述監聽ViewPager的onPageScrolled方法,點選tab的時候onPageScrolled方法傳回的positionOffset一直為0,每次點選tab時,最後一次調用的是onPageScrolled方法而不是onPageSelected方法,通過點選tab時候的log可以看出來:

- :: -/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled: 
- :: -/com.deli.newsdemo D/mTabLayoutRef: onPageSelected: 
- :: -/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled: 
           

==是以,以最後一次onPageScrolled的監聽為主,同時positionOffset為0,就導緻沒有動畫效果,也就是導緻點選tab很生硬的主要原因==!

那有沒有一種機制一能防止viewpager産生過渡動畫,又能讓tablayout有過渡動畫。 其實很簡單,就是監聽positionOffset,當positionOffset大于0時執行setScrollPosition方法:

@Override
        public void onPageScrolled(final int position, final float positionOffset,
                                   final int positionOffsetPixels) {
            final TabLayout tabLayout = mTabLayoutRef.get();
            Log.d("mTabLayoutRef", "onPageScrolled:1 ");
            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);
                if (positionOffset>)
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            }
        }
           

三、遇到的問題

遇到的最主要的問題就是在tab滑動機制自定義時,由于兩個監聽用了同一種動畫,是以監聽結果的順序就很重要,不然顯示的結果差強人意,通過debug發現傳回position的順序是最後傳回onPageScrolled方法而不是onPageSelected,才發現問題所在。

四、總結

這次在寫自己的demo的時候,本來是想仿寫網易新聞和今日頭條的頂部滑動菜單欄,然後發現都有這種點選tab時菜單欄無滾動效果的問題,通過看了TabLayout的源碼,并改寫才完善了這個功能,提高了使用者體驗,自己也積累了不少知識,總之再小的功能都有不斷發掘和革新的價值!

附:Demo位址