文本跟随滑动渐变的 TabLayout
2018-05-24 本文已影响94人
SharryChoo
效果展示
效果展示.gif使用方式
Step1. Add it in your root build.gradle at the end of repositories
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Step2. Add the dependency
dependencies {
// ...
implementation 'com.github.FrankChoo:GradualTabLayout:v1.0'
// 如果引入第三方库时,不引入其自身的依赖会报错
// e: Supertypes of the following classes cannot be resolved. Please make sure you have the required dependencies in the classpath:
implementation 'com.android.support:recyclerview-v7:26.1.0'
}
Step3. 使用
// 布局文件
<com.frank.gradualtablayout.GradualTabLayout
android:id="@+id/tabLayout1"
android:layout_width="match_parent"
android:layout_height="70dp"
app:indicatorColor="@color/colorIndicator"
app:indicatorHeight="3dp"
app:indicatorZoomScale="0.5"
app:tabCheckedColor="@color/colorChecked"
app:tabCutLineColor="@color/colorUnchecked"
app:tabCutLineWidth="0.5dp"
app:tabRateInDisplayWidth="0.3"
app:tabTextSize="15sp"
app:tabUncheckedColor="@color/colorUnchecked" />
// Kotlin 代码
val count = 10
with(tabLayout1) {
// configCutLine(1, Color.LTGRAY)// 配置分割线
// configIndicator(3, 0.2f, Color.BLUE)// 配置指示器
// configTextStyle(18f, Color.BLUE, Color.LTGRAY)// 配置文本样式
bindViewPager(viewPager)
for (index in 0 until count) {
addItem("第${index}页")
}
apply()
}
效果分析
-
文本在滑动的过程中根据 ViewPager 的偏移量左右渐变
-
Tab 类型
- 用户可自定义分割线
- 不可滑动: 平分控件的宽度
- 可滑动: 指定Tab的宽度
-
Indicator
- 指定高度颜色
- 指定 Indicator 占 TabWdth 的百分比
实现思路
-
文本: 使用自定义View,
- 从左右两个方向去绘制 Text 文本
- 根据滑动的百分比, 控制两种颜色的边界
-
Tab 使用 RecyclerView 封装, 根据是否设置了 Tab 的宽度, 来选择不同的 LayoutManager
- 未指定 Tab 的宽度, 使用 GridLayoutManager 平分控件空间
- 指定了 Tab 的宽度, 使用 LinearLayoutManager, 超出屏幕后可滚动查看
-
处理 Tab/Indicator 与 ViewPager 的联动
- 新建一个容器, 将 Tab 填入
- 绑定 ViewPager, 给ViewPager 添加一个 addOnPageChangeListener,
- 根据滑动的偏移量来改变 Tab 字体的渐变色
- 根据滑动的偏移量来绘制 Indicator
细节展示与分析
- 文本绘制的细节
/**
* Created by Frank on 2017/8/29.
* Email: xiaoyu.zhu@1hai.cn
* Version: 1.0
* Description: 自定义颜色可以渐变的 TextView, 还有底部指示器
*/
class GradualTextView extends AppCompatTextView {
public static final int DIRECTION_LEFT = 0;// 左滑
public static final int DIRECTION_RIGHT = 1;// 目标灰色字体移动的方向, 从右往左
private float mProgress = 0f;
private int mDirection = DIRECTION_LEFT;
private Paint mOriginPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
private Paint mGradualPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
public GradualTextView(Context context) {
this(context, null);
}
public GradualTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GradualTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mGradualPaint.setTextSize(size);
mOriginPaint.setTextSize(size);
}
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mGradualPaint.setTextSize(getTextSize());
mOriginPaint.setTextSize(getTextSize());
}
public void setupColor(int checkedColor, int uncheckedColor) {
mGradualPaint.setColor(checkedColor);
mOriginPaint.setColor(uncheckedColor);
}
public void updateProgress(float progress) {
mProgress = progress;
invalidate();
}
public void setDirection(int direction) {
mDirection = direction;
invalidate();
}
public void setChecked(boolean isChecked) {
mDirection = isChecked ? DIRECTION_LEFT : DIRECTION_RIGHT;
updateProgress(isChecked ? 0 : 1);
}
@Override
protected void onDraw(Canvas canvas) {
int middle = (int) (mProgress * getWidth());
if (mDirection == DIRECTION_LEFT) { // 原始文本在左
drawText(canvas, mOriginPaint, 0, middle);
drawText(canvas, mGradualPaint, middle, getWidth());
} else {// 原始文本在右
drawText(canvas, mGradualPaint, 0, getWidth() - middle);
drawText(canvas, mOriginPaint, getWidth() - middle, getWidth());
}
}
/**
* 绘制文本
*
* @param canvas
* @param paint
* @param clipStart 画布截取的起始位置
* @param clipEnd 画布截取的终点位置
*/
private void drawText(Canvas canvas, Paint paint, int clipStart, int clipEnd) {
canvas.save();
// 1. 裁剪画布
canvas.clipRect(clipStart, 0, clipEnd, getHeight());
// 2. 计算绘制起始位置
Rect textRect = new Rect();
paint.getTextBounds(getText().toString(), 0, getText().length(), textRect);
int startX = getWidth() / 2 - textRect.width() / 2;
// 3. 计算基线
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
int baseLine = getHeight() / 2 + dy;
canvas.drawText(getText().toString(), startX, baseLine, paint);
canvas.restore();
}
}
- Tab 封装细节
/**
* Created by Frank on 2018/5/21.
* Email: frankchoochina@gmail.com
* Version: 2.0
* Description: 滑动颜色渐变的文本指示器
* <p>
* 1. 支持指定 Tab 宽度(Tab 超出屏幕可以滑动)
* 2. 支持让 Tab 均分 ViewGroup 的空间(Tab 不可滑动)
*/
public class GradualTabView extends RecyclerView {
// 样式尺寸相关参数
int tabWidth;// 每一个 Tab 的宽度
int cutLineWidth;// 分割线的宽度
float textSize;// 文本的大小
// 相关颜色
int textCheckedColor;// 文本被选中的颜色
int textUncheckedColor;// 文本未被选中的颜色
int cutLineColor;// 分割线的颜色
// 布局管理与回调
private LayoutManager mLayoutManager;// 布局管理
private OnTabTextClickListener mListener;
public GradualTabView(Context context) {
this(context, null);
}
public GradualTabView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GradualTabView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public interface OnTabTextClickListener {
void onClick(int position);
}
/**
* 设置条目点击监听器
*/
public void setOnTabClickListener(OnTabTextClickListener listener) {
mListener = listener;
}
public void apply(List<CharSequence> tabs) {
if (tabWidth != 0) {// 定宽处理
mLayoutManager = new LinearLayoutManager(getContext(), LinearLayout.HORIZONTAL, false);
} else {// 平分布局处理
mLayoutManager = new GridLayoutManager(getContext(), tabs.size());
}
setLayoutManager(mLayoutManager);
setAdapter(new TabAdapter(tabs));
}
/**
* 更新指定角标的颜色
*
* @param position
*/
public void updateIndicatorPosition(int position) {
for (int i = 0; i < getChildCount(); i++) {
int absolutePosition = getChildAdapterPosition(getChildAt(i));
GradualTextView target = get(absolutePosition);
if (target == null) continue;
target.setChecked(absolutePosition == position);
}
}
/**
* 获取绝对路径上的 GradualTextView
*
* @param absolutePosition 当屏幕上没有这个 absolutePosition, 则返回 null
*/
public GradualTextView get(int absolutePosition) {
LinearLayout itemView = (LinearLayout) mLayoutManager.findViewByPosition(absolutePosition);
if (itemView == null) return null;
return (GradualTextView) itemView.getChildAt(0);
}
/**
* 用于展示的适配器
*/
private class TabAdapter extends RecyclerView.Adapter<TabAdapter.TabViewHolder> {
private List<CharSequence> mTabTexts;
TabAdapter(List<CharSequence> tabs) {
mTabTexts = tabs;
}
@Override
public TabViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 创建容器
LinearLayout container = new LinearLayout(parent.getContext());
container.setLayoutParams(new ViewGroup.LayoutParams(
tabWidth == 0 ? ViewGroup.LayoutParams.MATCH_PARENT : tabWidth,
ViewGroup.LayoutParams.MATCH_PARENT));
container.setOrientation(LinearLayout.HORIZONTAL);
return new TabViewHolder(container);
}
@Override
public void onBindViewHolder(final TabViewHolder holder, int position) {
// 设置一个 Tag
holder.itemView.setTag(position);
holder.tv.setText(mTabTexts.get(position));
holder.tv.setChecked(false);
}
@Override
public int getItemCount() {
return mTabTexts.size();
}
class TabViewHolder extends ViewHolder implements OnClickListener {
final GradualTextView tv;
public TabViewHolder(ViewGroup itemView) {
super(itemView);
itemView.setOnClickListener(this);
// 1. 创建文本
tv = createTextView(itemView.getContext());
itemView.addView(tv);
// 2. 创建分割线
itemView.addView(createCutLineView(itemView.getContext()));
}
/**
* 创建文本
*/
private GradualTextView createTextView(Context context) {
GradualTextView tv = new GradualTextView(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,
ViewGroup.LayoutParams.MATCH_PARENT);
params.weight = 1;
tv.setLayoutParams(params);
tv.setGravity(Gravity.CENTER);
tv.setTextSize(textSize);
tv.setupColor(textCheckedColor, textUncheckedColor);
return tv;
}
/**
* 创建分割线
*/
private View createCutLineView(Context context) {
View cutLine = new View(context);
cutLine.setLayoutParams(new LinearLayout.LayoutParams(cutLineWidth,
ViewGroup.LayoutParams.MATCH_PARENT));
cutLine.setBackgroundColor(cutLineColor);
return cutLine;
}
@Override
public void onClick(View view) {
if (mListener == null) return;
mListener.onClick((int) view.getTag());
}
}
}
}
- Tab 与 ViewPager 的绑定与 Indicator 的绘制细节
/**
* Created by Frank on 2018/5/22.
* Email: frankchoochina@gmail.com
* Version: 2.0
* Description:
* 1. 默认样式 Tab 不可滚动, 每个 Tab 平分控件的空间
* 2. 默认不绘制 Indicator, 除非用户设置了其高度
*/
public class GradualTabLayout extends LinearLayout implements ViewPager.OnPageChangeListener, GradualTabView.OnTabTextClickListener {
// Tabs相关
private List<CharSequence> mTabs = new ArrayList<>();
private GradualTabView mTabView;
private int mTabWidth = 0;
private float mTabTextSize = 15f;
private float mTabRateInDisplayWidth = 0f;
private int mTabCutLineWidth = 0;
private int mTabCutLineColor = Color.LTGRAY;
private int mTabCheckedColor = Color.RED;
private int mTabUncheckedColor = Color.LTGRAY;
// Indicator 指示器
private int mIndicatorColor = Color.RED;
private Rect mIndicatorRect;// 指示器的绘制区域
private int mIndicatorHeight = 0;// 指示器高度
private float mIndicatorLeft = 0f;// 指示器的左坐标(未按照 mIndicatorZoomScale 缩放之前的 Left)
private float mIndicatorZoomScale = 1f;// 指示器缩放比
private Paint mIndicatorPaint;
// ViewPager 滚动控制器
private ViewPager mBindViewPager;// 绑定的 ViewPager
private float mLastPositionOffsetPixels = -1;// 最后一次滑动 ViewPager 的页面偏移像素
private int mLastControlPosition = 0;// ViewPager 当前控制的页码
public GradualTabLayout(Context context) {
this(context, null);
}
public GradualTabLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GradualTabLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GradualTabLayout);
// 解析 Tab 相关属性
mTabWidth = (int) array.getDimension(R.styleable.GradualTabLayout_tabWidth, mTabWidth);
mTabTextSize = (int) array.getDimension(R.styleable.GradualTabLayout_tabTextSize, sp2px(mTabTextSize));
mTabRateInDisplayWidth = array.getFloat(R.styleable.GradualTabLayout_tabRateInDisplayWidth, mTabRateInDisplayWidth);
mTabCutLineWidth = (int) array.getDimension(R.styleable.GradualTabLayout_tabCutLineWidth, mTabWidth);
mTabCutLineColor = array.getColor(R.styleable.GradualTabLayout_tabCutLineColor, mTabCutLineColor);
mTabCheckedColor = array.getColor(R.styleable.GradualTabLayout_tabCheckedColor, mTabCheckedColor);
mTabUncheckedColor = array.getColor(R.styleable.GradualTabLayout_tabUncheckedColor, mTabUncheckedColor);
// 解析 Indicator 相关属性
mIndicatorHeight = (int) array.getDimension(R.styleable.GradualTabLayout_indicatorHeight, mIndicatorHeight);
mIndicatorZoomScale = array.getFloat(R.styleable.GradualTabLayout_indicatorZoomScale, mIndicatorZoomScale);
mIndicatorColor = array.getColor(R.styleable.GradualTabLayout_indicatorColor, mIndicatorColor);
array.recycle();
init();
}
private void init() {
// 1. 设置布局方向
setOrientation(VERTICAL);
// 2. 设置并添加文本指示器
mTabView = new GradualTabView(getContext());
mTabView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mTabView.setOnTabClickListener(this);
addView(mTabView);
// 3. 初始化绘制工具
mIndicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mIndicatorPaint.setColor(mIndicatorColor);
mIndicatorRect = new Rect();
setWillNotDraw(false);
}
/**
* 配置指示器
*/
public GradualTabLayout configIndicator(int height, float zoomScale, @ColorInt int color) {
mIndicatorHeight = height;
mIndicatorZoomScale = zoomScale;
mIndicatorColor = color;
mIndicatorPaint.setColor(mIndicatorColor);
return this;
}
/**
* 配置分割线样式
*/
public GradualTabLayout configCutLine(int width, @ColorInt int color) {
mTabCutLineWidth = width;
mTabCutLineColor = color;
return this;
}
/**
* 配置字体样式
*/
public GradualTabLayout configTextStyle(float size, @ColorInt int checkedColor, @ColorInt int uncheckedColor) {
mTabTextSize = sp2px(size);
mTabCheckedColor = checkedColor;
mTabUncheckedColor = uncheckedColor;
return this;
}
/**
* 添加文本条目
*/
public GradualTabLayout addItem(CharSequence text) {
mTabs.add(text);
return this;
}
/**
* 设置每一个 Tab 的宽度
*/
public GradualTabLayout setTabWidth(int width) {
mTabWidth = width;
return this;
}
/**
* 设置每一个 Tab 的宽度为屏幕的百分比
*/
public GradualTabLayout setTabWidth(float rateOnDisplayWidth) {
mTabRateInDisplayWidth = rateOnDisplayWidth;
return this;
}
/**
* 绑定 ViewPager
*/
public GradualTabLayout bindViewPager(ViewPager viewPager) {
mBindViewPager = viewPager;
mBindViewPager.addOnPageChangeListener(this);
return this;
}
public void apply() {
if (mTabRateInDisplayWidth != 0f) {
mTabWidth = (int) (getResources().getDisplayMetrics().widthPixels * mTabRateInDisplayWidth);
}
mTabView.tabWidth = mTabWidth;
mTabView.textSize = mTabTextSize;
mTabView.cutLineWidth = mTabCutLineWidth;
mTabView.cutLineColor = mTabCutLineColor;
mTabView.textCheckedColor = mTabCheckedColor;
mTabView.textUncheckedColor = mTabUncheckedColor;
mTabView.setPadding(0, 0, 0, mIndicatorHeight);
mTabView.apply(mTabs);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// 以 View 的中心为基准, 右滑时, position 为左边的条目, 左滑时为当前条目
// Log.e("TAG", "position: " + position + ", positionOffset: " + positionOffset);
boolean isLeft = mLastPositionOffsetPixels < positionOffsetPixels;
boolean isNeedChangeDirect = isNeedChangeDirection(isLeft, position);
performCurrentPageScrolled(isLeft, isNeedChangeDirect, position, positionOffset);
performRightCompanionPageScrolled(isLeft, isNeedChangeDirect, position + 1, positionOffset);
mLastPositionOffsetPixels = positionOffsetPixels;
}
@Override
public void onPageSelected(final int position) {
mTabView.smoothScrollToPosition(position);// 将 Indicator 移动到指定位置
mTabView.updateIndicatorPosition(position);// 更新 Indicator 的颜色
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onClick(int position) {
mBindViewPager.setCurrentItem(position, false);
}
@Override
protected void onDraw(Canvas canvas) {
// 计算有效的宽度
int validateWidth = (int) (mTabWidth * mIndicatorZoomScale);
mIndicatorRect.left = (int) (mIndicatorLeft + (mTabWidth - validateWidth) / 2);
mIndicatorRect.right = mIndicatorRect.left + validateWidth;
mIndicatorRect.bottom = getHeight() - getPaddingBottom();
mIndicatorRect.top = mIndicatorRect.bottom - mIndicatorHeight;
// 开始绘制
canvas.drawRect(mIndicatorRect, mIndicatorPaint);
}
/**
* 处理当前 Page 的滑动
*/
private void performCurrentPageScrolled(boolean isLeft, boolean isNeedChangeDirect, int position, float positionOffset) {
// if (position == mTabs.size() - 1) return;// 判断是否为最后一个位置
// 1. 获取当前 ViewPager 正在控制的 View
GradualTextView currentView = mTabView.get(position);
if (currentView == null) return;// 获取到的 View 为 null, 则不执行渐变
if (isNeedChangeDirect) {
currentView.setDirection(isLeft ? DIRECTION_LEFT : DIRECTION_RIGHT);
}
currentView.updateProgress(positionOffset);
// 根据当前正在控制的 View 来更新 Indicator
updateIndicatorPosition(currentView, positionOffset);
}
/**
* 处理右部伴生 Page 的滑动
*/
private void performRightCompanionPageScrolled(boolean isLeft, boolean isNeedChangeDirect, int position, float positionOffset) {
// 获取当前 ViewPager 正在控制的右边的 View
if (position == mTabs.size()) return;// 判断是否为最后一个位置
GradualTextView companionView = mTabView.get(position);
if (companionView == null) return;// 获取到的 View 为 null, 则不执行渐变
if (isNeedChangeDirect) {
companionView.setDirection(isLeft ? DIRECTION_RIGHT : DIRECTION_LEFT);
}
companionView.updateProgress(1 - positionOffset);
}
/**
* 更新索引值
*/
private void updateIndicatorPosition(View target, float positionOffset) {
if (target == null) return;
// 1. 获取 target 在屏幕上的坐标
int[] targetLocationArray = new int[2];
target.getLocationOnScreen(targetLocationArray);
// 2. 获取当前 ViewGroup 在屏幕上的坐标
int[] ownLocationArray = new int[2];
getLocationOnScreen(ownLocationArray);
// 3. 获取 mTabWidth 的值
if (mTabWidth == 0) {
mTabWidth = mTabView.tabWidth == 0 ? getWidth() / mTabs.size()
: mTabView.tabWidth;
}
// 4. 计算指示器的左边坐标
mIndicatorLeft = targetLocationArray[0] - ownLocationArray[0] + positionOffset * mTabWidth;
invalidate();
}
/**
* 是否需要改变文本渐变方向
*/
private boolean isNeedChangeDirection(boolean isLeft, int curControlPosition) {
// 更新索引的绘制的方向:
// Condition1 -> 控制的 View 改变了, Condition2 -> 手指左滑
boolean isNeedChangeDirection = mLastControlPosition != curControlPosition || isLeft;
if (isNeedChangeDirection) mLastControlPosition = curControlPosition;
return isNeedChangeDirection;
}
private int sp2px(float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
getResources().getDisplayMetrics());
}
}