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

前段时间看见了今日头条的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

下一篇算是自我学习的文章:Android的线程池

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容