Android技术知识Android知识android

文本跟随滑动渐变的 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()
}

效果分析

  1. 文本在滑动的过程中根据 ViewPager 的偏移量左右渐变

  2. Tab 类型

    • 用户可自定义分割线
    • 不可滑动: 平分控件的宽度
    • 可滑动: 指定Tab的宽度
  3. Indicator

    • 指定高度颜色
    • 指定 Indicator 占 TabWdth 的百分比

实现思路

  1. 文本: 使用自定义View,

    • 从左右两个方向去绘制 Text 文本
    • 根据滑动的百分比, 控制两种颜色的边界
  2. Tab 使用 RecyclerView 封装, 根据是否设置了 Tab 的宽度, 来选择不同的 LayoutManager

    • 未指定 Tab 的宽度, 使用 GridLayoutManager 平分控件空间
    • 指定了 Tab 的宽度, 使用 LinearLayoutManager, 超出屏幕后可滚动查看
  3. 处理 Tab/Indicator 与 ViewPager 的联动

    • 新建一个容器, 将 Tab 填入
    • 绑定 ViewPager, 给ViewPager 添加一个 addOnPageChangeListener,
      • 根据滑动的偏移量来改变 Tab 字体的渐变色
      • 根据滑动的偏移量来绘制 Indicator

细节展示与分析

  1. 文本绘制的细节
/**
 * 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();
    }

}

  1. 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());
            }
        }
    }

}
  1. 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());
    }

}

GitHub 传送门

https://github.com/FrankChoo/GradualTabLayout

上一篇下一篇

猜你喜欢

热点阅读