闲来无事,写了一个自定义的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>