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

自定义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

猜你喜欢

转载自blog.csdn.net/ccy0122/article/details/72870580