自定义TabLayout,支持自定义 Tab、ViewPager2 联动、循环滚动、自定义指示器

72 阅读3分钟

闲来无事,写了一个自定义的TabLayout,支持自定义 Tab、ViewPager2 联动、循环滚动、自定义指示器,并可使用IndicatorFactory类来统一管理各种指示器样式。

1️⃣ CustomTabLayout.java

package com.example.customtablayout;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;

/**
 * 自定义 TabLayout,支持:
 * - 自定义布局
 * - 循环滚动
 * - ViewPager2 联动
 * - 自定义指示器
 */
public class CustomTabLayout extends HorizontalScrollView {

    private LinearLayout mTabStrip;
    private SparseArray<View> mTabViews = new SparseArray<>();
    private int mSelectedPosition = -1;

    private Paint mIndicatorPaint;
    protected Rect mIndicatorRect = new Rect();

    private IndicatorDrawer mIndicatorDrawer = null;

    // ViewPager2 联动
    private ViewPager2 mViewPager2;
    private boolean mIsVpScroll = false;

    // 循环滚动
    private boolean mEnableLoopScroll = false;
    private int mRealTabCount = 0;

    public CustomTabLayout(Context context) {
        this(context, null);
    }

    public CustomTabLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setHorizontalScrollBarEnabled(false);
        mTabStrip = new LinearLayout(context);
        mTabStrip.setOrientation(LinearLayout.HORIZONTAL);
        addView(mTabStrip, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));

        mIndicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mIndicatorPaint.setColor(0xFF2196F3); // 默认蓝色
        mIndicatorPaint.setStyle(Paint.Style.FILL);
    }

    /** 添加一个文本 Tab */
    public void addTab(@NonNull String title) {
        View tab = createDefaultTab(title);
        addCustomTab(tab);
    }

    /** 添加一个自定义 View 的 Tab */
    public void addCustomTab(@NonNull View customView) {
        int index = mRealTabCount;
        mRealTabCount++;

        final int position = index;
        customView.setOnClickListener(v -> selectTab(position));

        mTabStrip.addView(customView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
        mTabViews.put(index, customView);

        if (mEnableLoopScroll) {
            duplicateTabs();
            post(this::scrollToMiddle);
        }

        if (mSelectedPosition == -1) {
            selectTab(0);
        }
    }

    /** 默认的 Tab 样式(纯文本) */
    private View createDefaultTab(String title) {
        androidx.appcompat.widget.AppCompatTextView tv = new androidx.appcompat.widget.AppCompatTextView(getContext());
        tv.setText(title);
        tv.setGravity(android.view.Gravity.CENTER);
        tv.setPadding(40, 0, 40, 0);
        tv.setTextSize(16);
        return tv;
    }

    /** 设置选中的 Tab */
    public void selectTab(int position) {
        if (position < 0 || position >= mRealTabCount) return;
        mSelectedPosition = position;
        View tab = mTabViews.get(position);
        if (tab != null) {
            smoothScrollTo(tab.getLeft(), 0);
            mIndicatorRect.left = tab.getLeft();
            mIndicatorRect.right = tab.getRight();
            mIndicatorRect.top = getHeight() - 6;
            mIndicatorRect.bottom = getHeight();
            invalidate();
        }
    }

    /** 绑定 ViewPager2 */
    public void setupWithViewPager2(@NonNull ViewPager2 viewPager2) {
        this.mViewPager2 = viewPager2;

        viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                super.onPageScrolled(position, positionOffset, positionOffsetPixels);
                scrollIndicatorWithOffset(position, positionOffset);
            }

            @Override
            public void onPageSelected(int position) {
                super.onPageSelected(position);
                if (!mIsVpScroll) {
                    selectTab(position);
                }
            }
        });
    }

    /** 指示器跟随 ViewPager2 滑动 */
    private void scrollIndicatorWithOffset(int position, float offset) {
        if (position < 0 || position >= mTabStrip.getChildCount() - 1) return;

        View currentTab = mTabStrip.getChildAt(position);
        View nextTab = mTabStrip.getChildAt(position + 1);

        int left = (int) (currentTab.getLeft() + (nextTab.getLeft() - currentTab.getLeft()) * offset);
        int right = (int) (currentTab.getRight() + (nextTab.getRight() - currentTab.getRight()) * offset);

        mIndicatorRect.left = left;
        mIndicatorRect.right = right;
        mIndicatorRect.top = getHeight() - 6;
        mIndicatorRect.bottom = getHeight();

        if (mIndicatorDrawer instanceof IndicatorFactory.GradientAnimIndicator) {
            ((IndicatorFactory.GradientAnimIndicator) mIndicatorDrawer).setProgress(offset);
        }

        invalidate();
    }

    /** 启用循环滚动 */
    public void setEnableLoopScroll(boolean enable) {
        this.mEnableLoopScroll = enable;
        if (enable) {
            duplicateTabs();
            post(this::scrollToMiddle);
        }
    }

    private void duplicateTabs() {
        if (mRealTabCount == 0) return;
        mTabStrip.removeAllViews();
        for (int i = 0; i < mRealTabCount * 2; i++) {
            int index = i % mRealTabCount;
            View original = mTabViews.get(index);
            if (original == null) continue;
            View copy = createDefaultTab(((androidx.appcompat.widget.AppCompatTextView) original).getText().toString());
            final int finalIndex = index;
            copy.setOnClickListener(v -> selectTab(finalIndex));
            mTabStrip.addView(copy);
        }
    }

    private void scrollToMiddle() {
        int midIndex = mRealTabCount;
        if (midIndex < mTabStrip.getChildCount()) {
            View midView = mTabStrip.getChildAt(midIndex);
            smoothScrollTo(midView.getLeft(), 0);
        }
    }

    /** 设置自定义指示器 */
    public void setIndicatorDrawer(@Nullable IndicatorDrawer drawer) {
        this.mIndicatorDrawer = drawer;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mIndicatorDrawer != null) {
            mIndicatorDrawer.draw(canvas, mIndicatorRect, mIndicatorPaint);
        } else {
            canvas.drawRect(mIndicatorRect, mIndicatorPaint);
        }
    }

    /** 指示器接口 */
    public interface IndicatorDrawer {
        void draw(Canvas canvas, Rect rect, Paint paint);
    }
}

2️⃣ IndicatorFactory

package com.example.customtablayout;

import android.content.Context;
import android.graphics.*;
import android.graphics.drawable.Drawable;

import androidx.annotation.NonNull;

/**
 * 指示器工厂:集中管理各种样式
 */
public class IndicatorFactory {

    /** 默认下划线 */
    public static CustomTabLayout.IndicatorDrawer lineIndicator() {
        return (canvas, rect, paint) -> canvas.drawRect(rect, paint);
    }

    /** 圆点 */
    public static CustomTabLayout.IndicatorDrawer dotIndicator(int radiusDp, @NonNull Context context) {
        return new DotIndicator(radiusDp, context);
    }

    /** 背景块 */
    public static CustomTabLayout.IndicatorDrawer blockIndicator(int cornerDp, @NonNull Context context) {
        return new BlockIndicator(cornerDp, context);
    }

    /** Drawable */
    public static CustomTabLayout.IndicatorDrawer drawableIndicator(@NonNull Drawable drawable) {
        return new DrawableIndicator(drawable);
    }

    /** 渐变动画 */
    public static GradientAnimIndicator gradientAnimIndicator(int startColor, int endColor, int cornerDp, @NonNull Context context) {
        return new GradientAnimIndicator(startColor, endColor, cornerDp, context);
    }

    // ======== 内部实现类 ========

    private static class DotIndicator implements CustomTabLayout.IndicatorDrawer {
        private int radius;

        public DotIndicator(int radiusDp, Context context) {
            this.radius = dpToPx(context, radiusDp);
        }

        @Override
        public void draw(Canvas canvas, Rect rect, Paint paint) {
            int cx = (rect.left + rect.right) / 2;
            int cy = rect.bottom - radius - dpToPx(canvas.getContext(), 2);
            canvas.drawCircle(cx, cy, radius, paint);
        }
    }

    private static class BlockIndicator implements CustomTabLayout.IndicatorDrawer {
        private int cornerRadius;

        public BlockIndicator(int cornerDp, Context context) {
            this.cornerRadius = dpToPx(context, cornerDp);
        }

        @Override
        public void draw(Canvas canvas, Rect rect, Paint paint) {
            RectF rf = new RectF(rect);
            rf.top = 0;
            canvas.drawRoundRect(rf, cornerRadius, cornerRadius, paint);
        }
    }

    private static class DrawableIndicator implements CustomTabLayout.IndicatorDrawer {
        private Drawable mDrawable;

        public DrawableIndicator(Drawable drawable) {
            this.mDrawable = drawable;
        }

        @Override
        public void draw(Canvas canvas, Rect rect, Paint paint) {
            mDrawable.setBounds(rect.left, rect.top, rect.right, rect.bottom);
            mDrawable.draw(canvas);
        }
    }

    public static class GradientAnimIndicator implements CustomTabLayout.IndicatorDrawer {
        private int startColor;
        private int endColor;
        private float progress;
        private int cornerRadius;

        public GradientAnimIndicator(int startColor, int endColor, int cornerDp, Context context) {
            this.startColor = startColor;
            this.endColor = endColor;
            this.cornerRadius = dpToPx(context, cornerDp);
        }

        public void setProgress(float progress) {
            this.progress = progress;
        }

        @Override
        public void draw(Canvas canvas, Rect rect, Paint paint) {
            LinearGradient shader = new LinearGradient(
                    rect.left, rect.top,
                    rect.right, rect.bottom,
                    startColor,
                    endColor,
                    Shader.TileMode.CLAMP
            );
            paint.setShader(shader);

            int inset = (int) (rect.width() * (1 - progress) * 0.2f);
            RectF rf = new RectF(rect.left + inset, rect.top, rect.right - inset, rect.bottom);

            canvas.drawRoundRect(rf, cornerRadius, cornerRadius, paint);
            paint.setShader(null);
        }
    }

    private static int dpToPx(Context context, float dp) {
        return (int) (dp * context.getResources().getDisplayMetrics().density + 0.5f);
    }
}

3️⃣ 使用示例 MainActivity.java

package com.example.customtablayout;

import android.graphics.Color;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CustomTabLayout tabLayout = findViewById(R.id.customTabLayout);
        ViewPager2 viewPager = findViewById(R.id.viewPager);

        // 添加 Tab
        tabLayout.addTab("首页");
        tabLayout.addTab("发现");
        tabLayout.addTab("消息");
        tabLayout.addTab("我的");

        // 配置 ViewPager2
        viewPager.setAdapter(new FragmentStateAdapter(this) {
            @NonNull
            @Override
            public Fragment createFragment(int position) {
                return SampleFragment.newInstance("页面 " + position);
            }

            @Override
            public int getItemCount() {
                return 4;
            }
        });

        // 绑定
        tabLayout.setupWithViewPager2(viewPager);

        // 启用循环滚动
        tabLayout.setEnableLoopScroll(true);

        // 切换不同指示器试试
        //tabLayout.setIndicatorDrawer(IndicatorFactory.lineIndicator());
        //tabLayout.setIndicatorDrawer(IndicatorFactory.dotIndicator(4, this));
        //tabLayout.setIndicatorDrawer(IndicatorFactory.blockIndicator(8, this));
        tabLayout.setIndicatorDrawer(
                IndicatorFactory.gradientAnimIndicator(
                        Color.parseColor("#FF6FD8"), // 粉
                        Color.parseColor("#3813C2"), // 紫
                        6,
                        this
                )
        );
    }
}

4️⃣ activity_main.xml 示例

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.customtablayout.CustomTabLayout
        android:id="@+id/customTabLayout"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:background="#FFFFFF" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>