在实际开发中,咱们经常需求运用viewpage调配SlideTabLayout联动翻滚运用。以前google官方并没有SlideTabLayout相关Widget供咱们直接运用,因而需求咱们自界说实现。

后来google在material design中推出了TabLayout,但自界说才能缺乏,动画款式有限,往往不能满足咱们的要求。

不过在github上也许多关于SlideTabLayout,咱们能够去搜一下,有不是有许多start的。但相似ios西瓜视频主页那种,随着页面滑动,被选中的tab逐步扩大的作用确很少,或许运用流畅性不行。

![Demo](i.postimg.cc/0y0Ms5rJ/de…)

TabLayout改造,支持对TabView扩展

改造TabLayout

下面我但咱们在官方TabLayout的基础上,去增加一些事情接口供咱们扩展运用。

首先把TabLayout的源码复制出来,一共没几个文件很简单。

TabLayout改造,支持对TabView扩展

上面三个类是三种款式的滑动指示器,首要作用是依据滑动进度或许tab方位确认指示器的宽度和方位。

TabLayout改造,支持对TabView扩展

TabItem没什么,便是一个Tab声明,虽然是一个View但是终究不会被添加到TabLayout布局中,首要用于xml中声明在TabLayout中有几个TabItem,能够指定icon,text,customView。终究会被转换为TabView添加到TabLayout布局中。

TabLayout便是今日的主角,承继自HorizontalScrollView,具有了横向翻滚才能,直接子View是SlidingTabIndicator,它承继自LinearLayout,由于TabView都是线性横向排列,能够直接复用丈量和布局。

TabLayout整体视图结构还是比较简单的,一个HorizontalScrollView下包裹一个LinearLayout,LinearLayout里边是一个个TabView。

TabLayoutMadiator首要是为了兼容ViewPager2,这个就不多叙述。

本期咱们的扩展目标首要是在滑动的过程中对TabView进行处理。

使命分化

目前作用是TabView中的文字随着翻滚扩大或缩小,扩大缩小会涉及到一个问题,便是会改动View的显现巨细,假如TabView之间的间隔太小,还或许形成TabView重叠显现。为了更好的用户体会,在扩大缩小的一起我会取平移每一个TabView,确保每一个TabView文字之间的间隔始终是相等的。

  • 目标1:动态缩放文字
  • 目前2:动态水平移动TabView的方位

为了保持TabView之间的间隔不变,又会带来一个新的问题,会形成所有TabView的z总宽度是不等于LinearLayout的宽度,由于总是会有一个或许两个TabView是被缩放的,假如咱们设置的是一个扩大作用,那就会形成Tab总宽度大于LinearLayout的宽度,终究一个TabView会被截断。为了处理这个问题,咱们需求重写LinearLayout的逻辑,预留出额外的空间供TabView平移。预留出的空间是最宽的tabView乘以最大扩大系数。

  • 目标3:从头丈量LinearLayout的宽度,预留空间

为了预留空间还会带来一个新的问题,便是翻滚到LinearLayout的最右边,或许会多出空白区域,由于咱们预留的是TabView的最大空间,假如当前选中的TabView不是宽度最大的那个,就会呈现空白区域。因而咱们需求动态改动最大翻滚间隔。

  • 目标4:动态改动最大翻滚间隔

事情接口界说

界说tabLayout一些关键节点的事情接口,让外部操控器有机会去改动Tablayout的默许行为,从而达到定制的作用。

public interface ITabEventListener {
    // 匹配形式
    boolean matchMode(@TabLayout.Mode int mode);
    // tabView的宽高确认
    default void onTabViewLayout(@NonNull TabLayout.TabView tabView) {
    }
    default void onReMeasureChildren(@NonNull LinearLayout slidingTabIndicator, Consumer<Integer> action) {
    }
    default void onRelayoutChildren(@NonNull LinearLayout slidingTabIndicator) {
    }
    default void onUpdateProgress(@NonNull LinearLayout slidingTabIndicator, @NonNull TabLayout.TabView currentTab, @Nullable TabLayout.TabView nextTab, float progress) {
    }
    default int getScaleTabContentWidth(@NonNull TabLayout.TabView tabView, int originSize) {
        return originSize;
    }
    default int getScaleTabContentHeight(@NonNull TabLayout.TabView tabView, int originSize) {
        return originSize;
    }
    default int getScaleTabWidth(@NonNull TabLayout.TabView tabView, int originSize) {
        return originSize;
    }
    default int getScaleTabHeight(@NonNull TabLayout.TabView tabView, int originSize) {
        return originSize;
    }
    default int getScaleTabLeft(@NonNull TabLayout.TabView tabView, int left) {
        return left;
    }
    default int getScaleTabTop(@NonNull TabLayout.TabView tabView, int top) {
        return top;
    }
    default int transformScrollX(int x) {
        return x;
    }
    default int transformScrollY(int y) {
        return y;
    }
}

在TabLayout中刺进事情点

能够改动最大翻滚间隔

   @Override
    public void scrollTo(int x, int y) {
        if (tabEventListener != null && tabEventListener.matchMode(mode)) {
            x = tabEventListener.transformScrollX(x);
            y = tabEventListener.transformScrollY(y);
        }
        super.scrollTo(x, y);
    }
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (tabEventListener != null && tabEventListener.matchMode(mode)) {
            scrollX = tabEventListener.transformScrollX(scrollX);
            scrollY = tabEventListener.transformScrollY(scrollY);
        }
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
    }

layout TabView,能够确认TabView的宽高

@Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
      super.onLayout(changed, l, t, r, b);
      if (tabEventListener != null && tabEventListener.matchMode(mode)) {
          tabEventListener.onTabViewLayout(this);
      }
  }

提供给指示器计算方位运用,由于缩放TabView的一起,咱们也需求改动指示器的方位

int getScaleContentWidth() {
            int result = getContentWidth();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                result = tabEventListener.getScaleTabContentWidth(this, result);
            }
            return result;
        }
        int getScaleContentHeight() {
            int result = getContentHeight();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                result = tabEventListener.getScaleTabContentHeight(this, result);
            }
            return result;
        }
        int getScaleTabWidth() {
            int width = getWidth();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                width = tabEventListener.getScaleTabWidth(this, width);
            }
            return width;
        }
        int getScaleTabHeight() {
            int height = getHeight();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                height = tabEventListener.getScaleTabHeight(this, height);
            }
            return height;
        }
        int getScaleLeft() {
            int left = getLeft();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                left = tabEventListener.getScaleTabLeft(this, left);
            }
            return left;
        }
        int getScaleTop() {
            int top = getTop();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                top = tabEventListener.getScaleTabTop(this, top);
            }
            return top;
        }
        int getScaleRight() {
            int right = getRight();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                right = getScaleLeft() + tabEventListener.getScaleTabWidth(this, getWidth());
            }
            return right;
        }
        int getScaleBottom() {
            int bottom = getBottom();
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                bottom = getScaleTop() + tabEventListener.getScaleTabHeight(this, getHeight());
            }
            return bottom;
        }

linearLayout 从头丈量

@Override
        protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                tabEventListener.onReMeasureChildren(this, new Consumer<Integer>() {
                    @Override
                    public void accept(Integer width) {
                        setMeasuredDimension(width, getMeasuredHeight());
                    }
                });
            }
    }

linearLayout 从头布局

@Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                tabEventListener.onRelayoutChildren(this);
            }
            if (indicatorAnimator != null && indicatorAnimator.isRunning()) {
                // It's possible that the tabs' layout is modified while the indicator is animating (ex. a
                // new tab is added, or a tab is removed in onTabSelected). This would change the target end
                // position of the indicator, since the tab widths are different. We need to modify the
                // animation's updateListener to pick up the new target positions.
                updateOrRecreateIndicatorAnimation(
                        /* recreateAnimation= */ false, getSelectedTabPosition(), /* duration= */ -1);
            } else {
                // If we've been laid out, update the indicator position
                jumpIndicatorToSelectedPosition();
            }
        }

依据进度改动TabView的巨细

private void tweenIndicatorPosition(View startTitle, View endTitle, float fraction) {
            boolean hasVisibleTitle = startTitle != null && startTitle.getWidth() > 0;
            if (hasVisibleTitle) {
                if (tabEventListener != null && tabEventListener.matchMode(mode)) {
                    tabEventListener.onUpdateProgress(this, (TabView) startTitle, (TabView) endTitle, fraction);
                }
                tabIndicatorInterpolator.updateIndicatorForOffset(
                        TabLayout.this, startTitle, endTitle, fraction, tabSelectedIndicator);
            } else {
                // Hide the indicator by setting the drawable's width to 0 and off screen.
                tabSelectedIndicator.setBounds(
                        -1, tabSelectedIndicator.getBounds().top, -1, tabSelectedIndicator.getBounds().bottom);
            }
            ViewCompat.postInvalidateOnAnimation(this);
        }

在TabIndicatorInterpolator中刺进事情点

依据缩放值确认新鸿沟

static RectF calculateTabViewContentBounds(
            @NonNull TabView tabView, @Dimension(unit = Dimension.DP) int minWidth) {
        int tabViewContentWidth = tabView.getScaleContentWidth();
        int tabViewContentHeight = tabView.getScaleContentHeight();
        int minWidthPx = (int) ViewUtils.dpToPx(tabView.getContext(), minWidth);
        if (tabViewContentWidth < minWidthPx) {
            tabViewContentWidth = minWidthPx;
        }
        int tabViewCenterX = (tabView.getScaleLeft() + tabView.getScaleRight()) / 2;
        int tabViewCenterY = (tabView.getScaleTop() + tabView.getScaleBottom()) / 2;
        int contentLeftBounds = tabViewCenterX - (tabViewContentWidth / 2);
        int contentTopBounds = tabViewCenterY - (tabViewContentHeight / 2);
        int contentRightBounds = tabViewCenterX + (tabViewContentWidth / 2);
        int contentBottomBounds = tabViewCenterY + (tabViewCenterX / 2);
        return new RectF(contentLeftBounds, contentTopBounds, contentRightBounds, contentBottomBounds);
    }
    static RectF calculateIndicatorWidthForTab(TabLayout tabLayout, @Nullable View tab) {
        if (tab == null) {
            return new RectF();
        }
        // If the indicator should fit to the tab's content, calculate the content's width
        if (!tabLayout.isTabIndicatorFullWidth() && tab instanceof TabView) {
            return calculateTabViewContentBounds((TabView) tab, MIN_INDICATOR_WIDTH);
        }
        if (tab instanceof TabView) {
            TabView tabView = (TabView) tab;
            return new RectF(tabView.getScaleLeft(), tabView.getScaleTop(), tabView.getScaleRight(), tabView.getScaleBottom());
        }
        // Return the entire width of the tab
        return new RectF(tab.getLeft(), tab.getTop(), tab.getRight(), tab.getBottom());
    }

监听事情,对TabView进行自界说操控

tabEventListener = object : ITabEventListener {
            private val selectedTabScale = 1.6f
            private var startView: TabView? = null
            private var endView: TabView? = null
            private var maxScrollX = 0f
            private var extraWidth = 0
            override fun matchMode(mode: Int): Boolean {
                return TabLayout.MODE_SCROLLABLE == mode
            }
            override fun onReMeasureChildren(slidingTabIndicator: LinearLayout, action: Consumer<Int>) {
                slidingTabIndicator.clipChildren = false
                slidingTabIndicator.clipToPadding = false
                slidingTabIndicator.apply {
                    if (selectedTabScale == 1f) {
                        return
                    }
                    val largestTabWidth = children.fold(0) { acc, child ->
                        val tabView = child as TabView
                        if (child.visibility == View.VISIBLE) {
                            acc.coerceAtLeast(tabView.textView.measuredWidth)
                        } else {
                            acc
                        }
                    }
                    if (largestTabWidth <= 0) {
                        // If we don't have a largest child yet, skip until the next measure pass
                        return@apply
                    }
                    extraWidth = ((selectedTabScale - 1f) * largestTabWidth).roundToInt()
                    action.accept(measuredWidth + extraWidth)
                }
            }
            override fun onRelayoutChildren(slidingTabIndicator: LinearLayout) {
                layoutChildren(slidingTabIndicator, true)
            }
            override fun onTabViewLayout(tabView: TabLayout.TabView) {
                tabView.clipChildren = false
                tabView.clipToPadding = false
                tabView.textView.apply {
                    pivotX = 0f
                    pivotY = height * 0.8f
                }
            }
            override fun onUpdateProgress(slidingTabIndicator: LinearLayout, currentTab: TabView, nextTab: TabView?, progress: Float) {
                if (startView != null && startView != currentTab && startView != nextTab) {
                    startView?.textView?.apply {
                        scaleX = 1f
                        scaleY = 1f
                    }
                }
                if (endView != null && endView != currentTab && endView != nextTab) {
                    endView?.textView?.apply {
                        scaleX = 1f
                        scaleY = 1f
                    }
                }
                currentTab.textView.apply {
                    val scale = selectedTabScale.plus(1.minus(selectedTabScale).times(progress))
                    scaleX = scale
                    scaleY = scale
                    startView = currentTab
                }
                nextTab?.textView?.apply {
                    val scale = 1.plus(selectedTabScale.minus(1f).times(progress))
                    scaleX = scale
                    scaleY = scale
                    endView = nextTab
                }
                layoutChildren(slidingTabIndicator, false)
            }
            private fun layoutChildren(slidingTabIndicator: LinearLayout, fromOnLayout: Boolean) {
                var translationX = 0f
                slidingTabIndicator.forEach { child ->
                    val tabView = child as TabView
                    if (child.visibility == View.VISIBLE) {
                        val rightGap = (tabView.textView.scaleX - 1).takeIf {
                            it != 0f
                        }?.let { (it * tabView.textView.measuredWidth) } ?: 0f
                        child.translationX = translationX
                        translationX += rightGap
                    }
                }
                maxScrollX = slidingTabIndicator.right - (slidingTabIndicator.parent as View).width - extraWidth + translationX
            }
            override fun getScaleTabContentWidth(tabView: TabView, originSize: Int): Int {
                return tabView.textView.scaleX.times(originSize).toInt()
            }
            override fun getScaleTabContentHeight(tabView: TabView, originSize: Int): Int {
                return tabView.textView.scaleY.times(originSize).toInt()
            }
            override fun getScaleTabWidth(tabView: TabView, originSize: Int): Int {
                return originSize.plus(tabView.textView.scaleX.minus(1).times(tabView.textView.width).toInt())
            }
            override fun getScaleTabHeight(tabView: TabView, originSize: Int): Int {
                return originSize.plus(tabView.textView.scaleY.minus(1).times(tabView.textView.height).toInt())
            }
            override fun transformScrollX(x: Int): Int {
                return maxScrollX.toInt().coerceAtMost(x)
            }
        }

终究作用

![Demo](i.postimg.cc/0y0Ms5rJ/de…

TabLayout改造,支持对TabView扩展