自定义View之歌词渐变文本控件

来源:互联网 发布:淘宝联盟新手怎么推广 编辑:程序博客网 时间:2024/04/29 17:16

自定义view之歌词渐变文本控件

前言

本文是自定义view的练习,默认读者掌握了自定义view的知识
本文的参考文章
http://blog.csdn.net/lmj623565791/article/details/44098729
使用方法:
https://github.com/CCY0122/lyrictextview

效果

这里写图片描述

实现

第一步吧我们要对外提供属性,让用户自由设置,这个一般是自定义view的老套路。在res下的values下新建的attrs.xml里定义好可以自定义的属性:

    <declare-styleable name="LyricTextView">        <attr name="text" format="string" />        <attr name="text_size" format="dimension" />        <attr name="default_color" format="color|reference" />        <attr name="changed_color" format="color|reference" />        <attr name="progress" format="float" />        <attr name="direction">            <enum name="left" value="0" />            <enum name="right" value="1" />        </attr>    </declare-styleable>

以上属性分别是文本、文本大小、默认颜色、渐变颜色、渐变百分比、渐变方向。不多说啦。然后新建LyricTextView继承view,重写前三个构造,在构造里获取属性:

 public LyricTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.LyricTextView);        text = t.getString(R.styleable.LyricTextView_text);        if(text == null){text = "";}        textSize = t.getDimension(R.styleable.LyricTextView_text_size, sp2px(16));        defaultColor = t.getColor(R.styleable.LyricTextView_default_color, DEFAULT_COLOR);        changeColor = t.getColor(R.styleable.LyricTextView_changed_color, CHANGED_COLOR);        direction = t.getInt(R.styleable.LyricTextView_direction, LEFT);        progress = t.getFloat(R.styleable.LyricTextView_progress,0);        t.recycle();        initPaint();        measureText();    }

可以看到首先是获取了xml里自定义好的属性,设置给对应变量。然后这个构造方法里还调用了两个方法initPaint(); measureText();
一个是用来初始化画笔的,一个是用来测量文本宽高的。代码如下。

private void initPaint() {        paint = new Paint(Paint.ANTI_ALIAS_FLAG);        paint.setTextSize(textSize); //必须在measureText前设置    }    /**     * 测量内容文本的宽高,当改变textSize时应重新调用此方法     */    private void measureText() {        Rect r = new Rect();        paint.getTextBounds(text, 0, text.length(), r);//一个坑:rect.width(或者说rect.right-rect.left)得到的值会比实际字长度小一点点,因此这里使用paint.measureText方法获取宽度        textHeight = r.bottom - r.top;        textWidth = (int) paint.measureText(text, 0, text.length());    }

在初始化画笔时,首先要给paint设置好字体大小,因为后面measureText里调用的方法都是在文字大小设置好的前提下获取的才是正确的值。
paint.getTextBounds是测量文字的宽高值然后放入rect里,但是实测发现一个坑:宽度会比实际宽度小一点,这会导致后面颜色渐变到100%时还有一丢丢字体没变色。因此获取文字宽度使用了paint.measureText方法。

接下来是onMeasure方法。先看代码。

 @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        width = measure(widthMeasureSpec, true);        height = measure(heightMeasureSpec, false);        setMeasuredDimension(width, height);    }    private int measure(int measureSpec, boolean isWidth) {        int mode = MeasureSpec.getMode(measureSpec);        int size = MeasureSpec.getSize(measureSpec);        switch (mode) {            case MeasureSpec.EXACTLY:                break;            case MeasureSpec.AT_MOST:            case MeasureSpec.UNSPECIFIED:                if (isWidth) {                    size = textWidth;                } else {                    size = textHeight;                }                break;        }        return isWidth ?         (size + getPaddingLeft() + getPaddingRight())         : (size + getPaddingTop() + getPaddingBottom());    }

如果设定了确定值(EXACTLY),那就设置之,如果没设,则控件大小就是文本的大小。

好了,核心部分onDraw来了.
本控件其实知识点就一个,怎么实现字体渐变?
答:画布canvas有一个方法canvas.clipRect(),调用了这个方法后接下来只会在这个区域内画内容,超出这个区域的内容就不画了。那么对于我们歌词渐变,我们先用默认颜色画出全部文本,然后呢,根据变量progress(渐变比例,范围[0,1])和方向direction(确定从左到右渐变还是从右到左)计算出要变色的区域,然后用渐变颜色再画一次文本即可。

先放上代码。

 @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        drawText(canvas, direction, progress);    }    private void drawText(Canvas canvas, int direction, float progress) {        int startX;        int endX;        int realWidth = (getMeasuredWidth() - getPaddingLeft() - getPaddingRight());        int realHeight = (getMeasuredHeight() - getPaddingTop() - getPaddingBottom());        int textLeft = getPaddingLeft() + realWidth / 2 - textWidth / 2;   //文本在控件中的起始x位置        int textRight = getPaddingLeft() + realWidth / 2 + textWidth / 2;   // 文本在控件中的结束x位置        int textBottom = getPaddingTop() + realHeight / 2 + textHeight / 2;  //文本在控件中的结束y位置        if(progress < 0 ){progress = 0;}        if(progress > 1 ){progress = 1;}        int changedWidth = (int) (textWidth * progress);        if (direction == LEFT) {            startX = textLeft;            endX = textLeft + changedWidth;        } else {            startX = textRight - changedWidth;            endX = textRight;        }        //画正常的文字内容        paint.setTextSize(textSize);        Paint.FontMetrics fontMetrics = paint.getFontMetrics();        canvas.save();        paint.setColor(defaultColor);        canvas.drawText(text, textLeft, textBottom, paint);        canvas.restore();        //画渐变部分的文字        canvas.save(Canvas.CLIP_SAVE_FLAG);        paint.setColor(changeColor);        canvas.clipRect(startX, 0, endX, getMeasuredHeight());        canvas.drawText(text, textLeft, textBottom, paint);        canvas.restore();    }

哇,贼简单。一些位置的计算,细心读一下应该能懂。如果你没理解,那可以去看看开头的参考文章,咱们的鸿洋大大写的。

小优化

我们先直接看看目前的效果吧:

   <com.example.lyrictextview.LyricTextView        android:id="@+id/lyric"        android:layout_width="match_parent"        android:layout_height="60dp"        android:background="#44000000"        app:changed_color="#ff0000"        app:default_color="#000000"        app:direction="left"        app:progress="0.7"        app:text="按时大大的飒飒的"        app:text_size="26sp" />

这里写图片描述

破费特,效果可以
那我们在用wrap_content看看。

  <com.example.lyrictextview.LyricTextView        android:id="@+id/lyric"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:background="#44000000"        app:changed_color="#ff0000"        app:default_color="#000000"        app:direction="left"        app:progress="0.7"        app:text="按时大大的飒飒的"        app:text_size="26sp" />

这里写图片描述

好了,问题来了,文字下面有一小部分没显示呀。
对于这个问题,看图(图片来自他人博客):
这里写图片描述

这里写图片描述
上图是android中文本绘制的各种线,我们平时绘制一个文本时呢,就是从baseLine最左端开始绘制的(上图中红点)。关于这个文本绘制的细节,已经有很多完整分析的文章了。
看上面这张图,字母g、j、p都是会超出baseLine的,这也就是为什么当我们宽高为wrap_content时文字下面一小部分不显示的原因了。
解决方法1:如果你不需要非常精细,直接给控件设置个默认padding就可以,就几个dp差不多就能把底部显示出来。
解决方法2:
1)修改measureText:

private void measureText() {        Rect r = new Rect();        paint.getTextBounds(text, 0, text.length(), r);//一个坑:rect.width会比实际字长度小一点点//        textHeight = r.bottom - r.top;        Paint.FontMetrics fontMetrics = paint.getFontMetrics();        textHeight = (int) (-fontMetrics.ascent + fontMetrics.descent);        textWidth = (int) paint.measureText(text, 0, text.length());    }

Paint.FontMetric里包含了文本绘制的各种线。因为绘制基线是baseLine,基线为0,坐标轴向下为正,故在其之上的是负数。
2)修改onDraw绘制部分:

//画正常的文字内容        paint.setTextSize(textSize);        Paint.FontMetrics fontMetrics = paint.getFontMetrics();        canvas.save();        paint.setColor(defaultColor);//        canvas.drawText(text, textLeft, textBottom, paint);        canvas.drawText(text, textLeft, textBottom - fontMetrics.descent, paint);        canvas.restore();        //画渐变部分的文字        canvas.save(Canvas.CLIP_SAVE_FLAG);        paint.setColor(changeColor);        canvas.clipRect(startX, 0, endX, getMeasuredHeight());//        canvas.drawText(text, textLeft, textBottom , paint);        canvas.drawText(text, textLeft, textBottom - fontMetrics.descent, paint);        canvas.restore();

好了,就是绘制文字时向上偏移一定距离,即fontMetrics.descent
这样,当控件宽高为wrap_content时文字也能显示完整啦
这里写图片描述

最后再贴上对外公开的setter、getter方法:

/以下settre getter供外部设置属性,别忘记invalidate();    //ps:若要使用属性动画控制progress,前提得有progress的setter getter    public void setProgress(float progress) {        this.progress = progress;        invalidate();    }    public float getProgress() {        return progress;    }    public void setTextSize(float size) {        textSize = size;        initPaint();        measureText();        requestLayout();//wrap_content情况下文字大小改变后需重新onMeausre        invalidate();    }    public float getTextSize() {        return textSize;    }    public int getDirection() {        return direction;    }    public void setDirection(int direction) {        this.direction = direction;        invalidate();    }    public String getText() {        return text;    }    public void setText(String text) {        this.text = text;        requestLayout();  //wrap_content情况下文字长度改变后需重新onMeausre        invalidate();    }    public int getDefaultColor() {        return defaultColor;    }    public void setDefaultColor(int defaultColor) {        this.defaultColor = defaultColor;        invalidate();    }       public void setAll(float progress, String text, float textSize, int defaultColor, int changeColor, int direction) {        this.progress = progress;        this.text = text;        this.textSize = textSize;        this.defaultColor = defaultColor;        this.changeColor = changeColor;        this.direction = direction == LEFT ? LEFT : RIGHT;        initPaint();        measureText();        requestLayout();        invalidate();    }    public int getChangeColor() {        return changeColor;    }    public void setChangeColor(int changeColor) {        this.changeColor = changeColor;        invalidate();    }    //工具    private float dp2px(int dp) {        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());    }    private float sp2px(int sp) {        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());    }

这是一个简单的小控件,写它主要是打算写一个仿今日头条指示器的控件~过些天应该会写吧~~~
源码https://github.com/CCY0122/lyrictextview

原创粉丝点击