可切换颜色的tab(仿今日头条tablayout)

本文介绍了一种自定义TabLayout的方法,通过实现局部文字颜色变化,达到在滑动过程中平滑过渡的效果。文中详细展示了如何使用Canvas的clipRect方法来实现这一效果,并提供了完整的代码实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前段时间看见了今日头条的tablayout,感觉相当新鲜,也比较感兴趣,效果就是下边这个
gif5新文件.gif

像这种效果应该是需要自定义的View来实现的,可以看到在滑动过程中,两个相邻的tab是有局部颜色的变换的,前一个tab部分恢复成黑色,后一个tab会部分变为红色,这取决于滑动的距离.

首先每一个tab应该都是自定义的View,因为这涉及到了局部文字变色,所以应该先定制一个能够局部文字变色的View,普通的View当然不支持啦~

先来说下思路~主要用的方法是canvas的clipRect方法,先来看下这个方法啥子意思哟..

/**
* Intersect the current clip with the specified rectangle, which is
* expressed in local coordinates.
*
* @param left The left side of the rectangle to intersect with the
* current clip
* @param top The top of the rectangle to intersect with the current clip
* @param right The right side of the rectangle to intersect with the
* current clip
* @param bottom The bottom of the rectangle to intersect with the current
* clip
* @return true if the resulting clip is non-empty
*/
public boolean clipRect(int left, int top, int right, int bottom) {
return nClipRect(mNativeCanvasWrapper, left, top, right, bottom,
Region.Op.INTERSECT.nativeInt);
}

解释一下,里边的四个参数裁剪范围的左上右下的位置,比较好理解,需要注意的是,使用完这个方法后需要及时的恢复绘制范围,所以完整代码如下

canvas.save();
canvas.clipRect(left, top, right, bottom);
//再做绘制操作例如本片要用到的drawText()
canvas.restore();

知道了这个方法,那么就想想怎么绘制出两种颜色的文本了,先上个图

图中的1部分为黑色,2部分为红色,那么再绘制过程中我们只需要利用clipRect这个方法,分别裁剪出1部分的范围以及2部分的范围,分别使用不同颜色绘制就OK啦~但是总体的绘制起点以及文本都是一样的,这样就看起来是一个文本两种颜色,其实我们是绘制了两边,还是比较好理解的

话不多说,直接上代码
““
public class ColorClipView extends View {

private Paint paint;//画笔
private String text = "我是不哦车网";//绘制的文本
private int textSize = sp2px(18);//文本字体大小

private int textWidth;//文本的宽度
private int textHeight;//文本的高度

private int textUnselectColor = R.color.colorPrimary;//文本未选中字体颜色
private int textSelectedColor = R.color.colorAccent;//文本选中颜色

private static final int DIRECTION_LEFT = 0;
private static final int DIRECTION_RIGHT = 1;
private static final int DIRECTION_TOP = 2;
private static final int DIRECTION_BOTTOM = 3;

private int mDirection = DIRECTION_LEFT;

private Rect textRect = new Rect();//文本显示区域

private int startX;//X轴开始绘制的坐标

private int startY;//y轴开始绘制的坐标

private int baseLineY;//基线的位置

private float progress;


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

public ColorClipView(Context context, AttributeSet attrs) {
    super(context, attrs);

    //初始化各个属性包括画笔

    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    TypedArray ta = context.obtainStyledAttributes(attrs,
            R.styleable.ColorClipView);
    text = ta.getString(R.styleable.ColorClipView_text);
    textSize = ta.getDimensionPixelSize(R.styleable.ColorClipView_text_size, textSize);

// textUnselectColor = ta.getColor(R.styleable.ColorClipView_text_unselected_color, textUnselectColor);
// textSelectedColor = ta.getColor(R.styleable.ColorClipView_text_selected_color, textSelectedColor);
mDirection = ta.getInt(R.styleable.ColorClipView_direction, mDirection);
progress = ta.getFloat(R.styleable.ColorClipView_progress, 0);
ta.recycle();//用完就得收!
paint.setTextSize(textSize);
}

private int sp2px(float dpVal) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
            dpVal, getResources().getDisplayMetrics());
}

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

public void setTextSize(int mTextSize) {
    this.textHeight = mTextSize;
    paint.setTextSize(mTextSize);
    requestLayout();
    invalidate();
}

public void setText(String text) {
    this.text = text;
    requestLayout();
    invalidate();
}

public void setDirection(int direction) {
    this.mDirection = direction;
    invalidate();
}

public void setTextUnselectColor(int unselectColor) {
    this.textUnselectColor = unselectColor;
    invalidate();
}

public void setTextSelectedColor(int selectedColor) {
    this.textSelectedColor = selectedColor;
    invalidate();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureText();//测量文本的长宽

    int width = measureWidth(widthMeasureSpec);//通过模式的不同来测量出实际的宽度
    int height = measureHeight(heightMeasureSpec);//通过模式的不同来测量出实际的高度
    setMeasuredDimension(width, height);
    startX = (getMeasuredWidth() - getPaddingRight() - getPaddingLeft()) / 2 - textWidth / 2;
    startY = (textHeight - getPaddingBottom() - getPaddingTop());
}

private int measureHeight(int heightMeasureSpec) {
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);
    int realSize = 0;
    switch (mode) {
        case MeasureSpec.EXACTLY:
            realSize = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.UNSPECIFIED:
            realSize = textHeight;
            realSize += getPaddingTop() + getPaddingBottom();
            break;
    }
    realSize = mode == MeasureSpec.AT_MOST ? Math.min(realSize, size) : realSize;
    return realSize;
}

private int measureWidth(int widthMeasureSpec) {
    int mode = MeasureSpec.getMode(widthMeasureSpec);//通过widthMeasureSpec拿到Mode
    int size = MeasureSpec.getSize(widthMeasureSpec);//同理
    int realSize = 0;//最后返回的值
    switch (mode) {
        case MeasureSpec.EXACTLY://精确模式下直接用给出的宽度
            realSize = size;
            break;
        case MeasureSpec.AT_MOST://最大模式
        case MeasureSpec.UNSPECIFIED://未指定模式
            //这两种情况下,用测量出的宽度加上左右padding
            realSize = textWidth;
            realSize = realSize + getPaddingLeft() + getPaddingRight();
            break;
    }
    //如果mode为最大模式,不应该大于父类传入的值,所以取最小
    realSize = mode == MeasureSpec.AT_MOST ? Math.min(realSize, size) : realSize;
    return realSize;
}

private void measureText() {
    textWidth = (int) paint.measureText(text);//测量文本宽度
    Log.d("tag", "measureText=" + paint.measureText(text));


    //直接通过获得文本显示范围,再获得高度
    //参数里,text 是要测量的文字
    //start 和 end 分别是文字的起始和结束位置,textRect 是存储文字显示范围的对象,方法在测算完成之后会把结果写进 textRect。
    paint.getTextBounds(text, 0, text.length(), textRect);
    textHeight = textRect.height();

    //通过文本的descent线与top线的距离来测量文本高度,这是其中一种测量方法
    Paint.FontMetrics fm = paint.getFontMetrics();
    textHeight = (int) Math.ceil(fm.descent - fm.top);

    baseLineY = (int) (textHeight / 2 - (fm.bottom - fm.top) / 2 - fm.top);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //OK~开始绘制咯~
    //首先先判断方向是左还是右呢?  是上还是下呢? 真期待....
    Log.e("tag", "OnDraw");
    if (mDirection == DIRECTION_LEFT) {
        //绘制朝左的选中文字
        drawHorizontalText(canvas, textSelectedColor, startX,
                (int) (startX + progress * textWidth));
        //绘制朝左的未选中文字
        drawHorizontalText(canvas, textUnselectColor, (int) (startX + progress
                * textWidth), startX + textWidth);
    } else if (mDirection == DIRECTION_RIGHT) {
        //绘制朝右的选中文字
        drawHorizontalText(canvas, textSelectedColor,
                (int) (startX + (1 - progress) * textWidth), startX
                        + textWidth);
        //绘制朝右的未选中文字
        drawHorizontalText(canvas, textUnselectColor, startX,
                (int) (startX + (1 - progress) * textWidth));
    } else if (mDirection == DIRECTION_TOP) {
        //绘制朝上的选中文字
        drawVerticalText(canvas, textSelectedColor, startY,
                (int) (startY + progress * textHeight));
        //绘制朝上的未选中文字
        drawVerticalText(canvas, textUnselectColor, (int) (startY + progress
                * textHeight), startY + textHeight);
    } else {
        //绘制朝下的选中文字
        drawVerticalText(canvas, textSelectedColor,
                (int) (startY + (1 - progress) * textHeight),
                startY + textHeight);
        //绘制朝下的未选中文字
        drawVerticalText(canvas, textUnselectColor, startY,
                (int) (startY + (1 - progress) * textHeight));
    }

}

private void drawHorizontalText(Canvas canvas, int color, int startX, int endX) {
    paint.setColor(color);
    canvas.save();
    Log.e("tag", "getMeasuredHeight" + getMeasuredHeight());
    canvas.clipRect(startX, 0, endX, getMeasuredHeight());
    canvas.drawText(text, this.startX, baseLineY, paint);
    canvas.restore();
}

private void drawVerticalText(Canvas canvas, int color, int startY, int endY) {
    paint.setColor(color);
    canvas.save();
    canvas.clipRect(0, startY, getMeasuredWidth(), endY);
    canvas.drawText(text, this.startX,
            this.startY, paint);
    canvas.restore();
}

}
““
上边代码我自己重写了onMeasure方法,自己测量了高度与宽度,其实我么你可以直接继承TextView,这样可以不用重写onMeasure方法,直接交给TextView去测量…

还有关于绘制文字这一点,也就是drawText这个方法需要说一下

/**
* Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
* based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
super.drawText(text, x, y, paint);
}

上边的四个参数意思分别是,绘制文本,绘制起点X坐标,绘制起点Y坐标,用来绘制的画笔
这里的需要上一张图

如图所示,在绘制文本的时候绘制起点在左下角而不是左上角,这个需要特殊说明一下…
然后再上一张图来看下效果,这里我加了手势操作来改变文本颜色
xixi.gif

然后每个tab我们制作完了,需要把tab放到tablayout中,并且会随着滑动距离而改变相邻两个的tab的不分颜色,OK~我们有需要自定义一个继承于tablayout的View,重写addtab方法,将我们刚才的自定义View加入进去,并且加入滑动监听,从而改变颜色,直接上代码
““
public class ColorClipTabLayout extends TabLayout {

private int tabTextSize;//每个tab字体大小
private int tabSelectedTextColor;//每个tab选中字体颜色
private int tabTextColor;//每个tab未选中颜色
private static final int INVALID_TAB_POS = -1;

//最后的选中位置
private int lastSelectedTabPosition = INVALID_TAB_POS;

private ViewPager viewPager;//所绑定的viewpager

private ColorClipTabLayoutOnPageChangeListener colorClipTabLayoutOnPageChangeListener;


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

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

public ColorClipTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    if (attrs != null) {
        // Text colors/sizes come from the text appearance first
        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColorClipTabLayout);
        //Tab字体大小
        tabTextSize = ta.getDimensionPixelSize(R.styleable.ColorClipTabLayout_text_size, 72);
        //Tab文字颜色
        tabTextColor = ta.getColor(R.styleable.ColorClipTabLayout_text_unselected_color, Color.parseColor("#000000"));
        tabSelectedTextColor = ta.getColor(R.styleable.ColorClipTabLayout_text_selected_color, Color.parseColor("#cc0000"));
        ta.recycle();
    }
}

@Override
public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
    //通过addTab的方式将colorClipView作为customView传入tab
    ColorClipView colorClipView = new ColorClipView(getContext());
    colorClipView.setProgress(setSelected ? 1 : 0);
    colorClipView.setText(tab.getText() + "");
    colorClipView.setTextSize(tabTextSize);
    colorClipView.setTag(position);
    colorClipView.setTextSelectedColor(tabSelectedTextColor);
    colorClipView.setTextUnselectColor(tabTextColor);
    LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
    colorClipView.setLayoutParams(layoutParams);
    tab.setCustomView(colorClipView);
    super.addTab(tab, position, setSelected);
    int selectedTabPosition = getSelectedTabPosition();
    if ((selectedTabPosition == INVALID_TAB_POS && position == 0) || (selectedTabPosition == position)) {
        setSelectedView(position);
    }

    setTabWidth(position, colorClipView);
}

@Override
public void setupWithViewPager(@Nullable ViewPager viewPager, boolean autoRefresh) {
    super.setupWithViewPager(viewPager, autoRefresh);
    try {
        if (viewPager != null)
            this.viewPager = viewPager;
        colorClipTabLayoutOnPageChangeListener = new ColorClipTabLayoutOnPageChangeListener(this);
        colorClipTabLayoutOnPageChangeListener.reset();
        viewPager.addOnPageChangeListener(colorClipTabLayoutOnPageChangeListener);

// }
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void removeAllTabs() {
    lastSelectedTabPosition = getSelectedTabPosition();
    super.removeAllTabs();
}

@Override
public int getSelectedTabPosition() {
    final int selectedTabPositionAtParent = super.getSelectedTabPosition();
    return selectedTabPositionAtParent == INVALID_TAB_POS ?
            lastSelectedTabPosition : selectedTabPositionAtParent;
}

public void setLastSelectedTabPosition(int lastSelectedTabPosition) {
    lastSelectedTabPosition = lastSelectedTabPosition;
}

public void setCurrentItem(int position) {
    if (viewPager != null)
        viewPager.setCurrentItem(position);
}

private void setTabWidth(int position, ColorClipView colorClipView) {
    ViewGroup slidingTabStrip = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) slidingTabStrip.getChildAt(position);
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);

    int w = MeasureSpec.makeMeasureSpec(0,
            MeasureSpec.UNSPECIFIED);
    int h = MeasureSpec.makeMeasureSpec(0,
            MeasureSpec.UNSPECIFIED);
    //手动测量一下
    colorClipView.measure(w, h);
    params.width = colorClipView.getMeasuredWidth() + tabView.getPaddingLeft() + tabView.getPaddingRight();
    //设置tabView的宽度
    tabView.setLayoutParams(params);
}

private void setSelectedView(int position) {
    final int tabCount = getTabCount();
    if (position < tabCount) {
        for (int i = 0; i < tabCount; i++) {
            getColorClipView(i).setProgress(i == position ? 1 : 0);
        }
    }
}

public void tabScrolled(int position, float positionOffset) {

    if (positionOffset == 0.0F) {
        return;
    }
    ColorClipView currentTrackView = getColorClipView(position);
    ColorClipView nextTrackView = getColorClipView(position + 1);
    currentTrackView.setDirection(1);
    currentTrackView.setProgress(1.0F - positionOffset);
    nextTrackView.setDirection(0);
    nextTrackView.setProgress(positionOffset);
}

private ColorClipView getColorClipView(int position) {
    return (ColorClipView) getTabAt(position).getCustomView();
}

public static class ColorClipTabLayoutOnPageChangeListener extends TabLayoutOnPageChangeListener {

    private final WeakReference<ColorClipTabLayout> mTabLayoutRef;
    private int mPreviousScrollState;
    private int mScrollState;

    public ColorClipTabLayoutOnPageChangeListener(TabLayout tabLayout) {
        super(tabLayout);
        mTabLayoutRef = new WeakReference<>((ColorClipTabLayout) tabLayout);
    }

    @Override
    public void onPageScrollStateChanged(final int state) {
        mPreviousScrollState = mScrollState;
        mScrollState = state;
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        super.onPageScrolled(position, positionOffset, positionOffsetPixels);
        ColorClipTabLayout tabLayout = mTabLayoutRef.get();
        if (tabLayout == null) return;
        final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
                mPreviousScrollState == SCROLL_STATE_DRAGGING;
        if (updateText) {
            Log.e("tag", "positionOffset" + positionOffset);
            tabLayout.tabScrolled(position, positionOffset);
        }
    }

    @Override
    public void onPageSelected(int position) {
        super.onPageSelected(position);
        ColorClipTabLayout tabLayout = mTabLayoutRef.get();
        mPreviousScrollState = SCROLL_STATE_SETTLING;
        tabLayout.setSelectedView(position);
    }

    void reset() {
        mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
    }

}

}
““
来,看下效果
xixi2.gif

OJBK,这就是我想要的效果,话不多说,代码已经上传到github,可以下下来看一看,喜欢的可以star一下
恩,你们都是最帅的最美的…

最后推荐一波扔物线的自定义View教程,真的很有帮助HenCoder

参考:
Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器
自适应Tab宽度可以滑动文字逐渐变色的TabLayout

github地址:ColorTabLayout

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值