基于TabLayout源码实现自定义TabLayout

本文详细解析TabLayout与ViewPager的绑定原理,探讨如何自定义tab底部indicator及滑动机制。针对点击Tab时的平滑切换问题,提出了通过监听positionOffset改进的解决方案,以提高用户体验。
摘要由CSDN通过智能技术生成

目录

  • 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存在双向绑定的机制:

image
绑定流程如下:
这里写图片描述
通过监听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 = 0;

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

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

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

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 >= 0 && mIndicatorRight > mIndicatorLeft) {
                //自定义画圆
                //canvas.drawCircle((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight, mSelectedIndicatorHeight, mSelectedIndicatorPaint);
                //自定义三角形
                Path path = new Path();
                path.moveTo((mIndicatorLeft + mIndicatorRight) / 2, getHeight() - mSelectedIndicatorHeight - 10);
                path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 - mSelectedIndicatorHeight - 10, getHeight());
                path.lineTo((mIndicatorLeft + mIndicatorRight) / 2 + mSelectedIndicatorHeight + 10, 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可以看出来:

02-04 08:35:59.965 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1 
02-04 08:36:08.591 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageSelected: 2
02-04 08:36:08.597 7206-7206/com.deli.newsdemo D/mTabLayoutRef: onPageScrolled:1 

==所以,以最后一次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>0)
                tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
            }
        }

三、遇到的问题

遇到的最主要的问题就是在tab滑动机制自定义时,由于两个监听用了同一种动画,所以监听结果的顺序就很重要,不然显示的结果差强人意,通过debug发现返回position的顺序是最后返回onPageScrolled方法而不是onPageSelected,才发现问题所在。

四、总结

这次在写自己的demo的时候,本来是想仿写网易新闻和今日头条的顶部滑动菜单栏,然后发现都有这种点击tab时菜单栏无滚动效果的问题,通过看了TabLayout的源码,并改写才完善了这个功能,提高了用户体验,自己也积累了不少知识,总之再小的功能都有不断发掘和革新的价值!

附:Demo地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值