自我绘制一

282 阅读8分钟

一. 文字绘制坐标问题

       在view基础里边已经说过:View的位置有父类直接决定,大小由开发者和父布局和view自身商量决定(商量的源码也分析过了)。那么在自定义View的时候我们仅仅不需要考虑整个View在父布局的位置的。但是View内部元素之间的位置还需要开发者去设计的。所以当我们自定义View的时候都要考虑内部元素的位置和大小,以及整体的大小。而内部之间的位置安排都是以自定义View的左上角为坐标原点,然后确定每个元素的坐上角坐标。但是有一个例外,那就是绘制文字的时候并不是依据的坐上角的坐标!!!

绘制文字的方法:canvas.drawText(text_content, x, baseline, text_Paint)

x位置:可以用画笔去控制,如:left。center , right。

baseline:是基线。基线就是0。它之上的top就是负数,它之下的bottom就是正数。那么我们怎么通过这些距离算出来基线的y坐标呢?先祭上一张图:


思路:我们先计算出top到bottom之间的中间点,他们和整个View的Y方向中间点重合,从这里突破得到公式:基线坐标 = 控件Y/2+(bottom-top)/2-bottom

二 动画绘制线

绘制多条线时候配合Path去一起绘制:

val path = Path()
path.moveTo(30F,30F)
path.rLineTo(30F,30F)
canvas.drawPath(path,checkPaint)

这里moveTo是移动到开始位置坐标,rLineTo,表示是相对位置,lineTo是相对于自定义控件原点坐标,然后去drawPath去绘制去一条线。

这里提供的绘制的起点坐标和终点坐标,就可以绘制出来一条线。如果想要结合动画,我们只要找到一个设置线的长度的方法,就可以结合属性动画就可以绘制带有动画的曲线。遗憾的是google并没有给我们提供这样的一个属性。


办法绝对是有的,只是我们需要使用DashPathEffect这个类。这个类又是什么呢?

这个类可以使paint画出类似虚线的样子,并且可以任意指定虚实的排列方式。举个例子:

Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Style.STROKE);
p.setColor(Color.WHITE);
p.setStrokeWidth(1);
PathEffect effects = new DashPathEffect(new float[] { 1, 2, 4, 8}, 0);
p.setPathEffect(effects);
canvas.drawLine(0, 40, 300, 40, p);

代码中的float数组,必须是偶数长度,且>=2,指定了多少长度的实线之后再画多少长度的空白。

如本代码中,绘制长度1的实线,再绘制长度2的空白,再绘制长度4的实线,再绘制长度8的空白,依次重复。1是起始位置的偏移量。

我们可以把DashPathEffect的第一个参数(float数组)只填入两个值,都是path的总长度length,那么按照上面对DashPathEffect的解释,第一次绘制一条实线就已经完全绘制完了,间隔的空白区间得不到绘制的机会。事实上这样绘制完全不能产生虚线效果,跟不设置PathEffect是一样的。

但是我们注意第三个参数即起始位置的偏移量现在是为0的。如果我们不为0呢?

比如为100,那么第一次绘制实线就会跳过100的距离,第一次的实线就只能绘制length-100的长度,那么空白区域就可以绘制100的长度,但是你看不见空白,所以我们只会感觉到绘制了一条length-100的路径。

根据这个特性,我们就可以绘制出来一个动态变换的效果。

类是这样的代码设置给画笔:

    new DashPathEffect(new float[] { length, length },length - phase * length);

只需要通过属性动画去改变phase的值,我们就能看到绘制线的过程。

并且google给我们提供了计算path长度的方法:

PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();

由此我们就可以动态去绘制一个线的过程。

package view.local.zz.customviews.views

import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View

/**
 * Created by Administrator on 2018/7/9.
 */

class Test : View, View.OnClickListener {
    //画笔
    val checkPaint: Paint
    //路径
    var checkPath: Path
    //实现需要数组
    val inf: FloatArray
    //自定义变换率属性
    var bianhuan = 0.0F
        set(value) {
            field = value
            checkPaint.setPathEffect(DashPathEffect(inf, inf.get(0) - inf.get(0) * value))
            invalidate()
        }
    //是否绘制
    var isDraw = false

    //构造方法
    constructor(context: Context?) : this(context, null)

    //构造方法
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)

    //构造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)

    //构造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
        //创建画笔
        checkPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        checkPaint.setStyle(Paint.Style.STROKE)//设checkPath置画线模式
        checkPaint.setStrokeWidth(8F) // 线条宽度为 8F 像素
        //创建画的路径
        checkPath = Path()
        checkPath.lineTo(100F, 100F)
        checkPath.lineTo(300F, 150F)
        //计算路径的长度
        val Measure = PathMeasure(checkPath, false)
        inf = floatArrayOf(Measure.length, Measure.length)
        //设置点击事件
        setOnClickListener(this)
    }

    override fun onDraw(canvas: Canvas) {
        if (isDraw) {//绘制路径
            canvas.drawPath(checkPath, checkPaint)
        }
    }

    override fun onClick(v: View?) {
        isDraw = true
        //创建动画
        val ofFloat = ObjectAnimator.ofFloat(this, "bianhuan", 0.0F, 1.0F)
        //开始动画
        ofFloat.start()
    }


}

出来的效果图:


三自定义一个动画按钮

动画按钮效果图如下


分析:

1.需要绘制文字,并且改变文字的透明度(绘制文字确定基线位置,上边说过方法)

2.矩形变成圆角巨型

3.圆角巨型变成圆

4.绘制对号,线的绘制过程(上边也说过)

开始自定义:

1.首先要确定View的大小(上一篇分析的源码,我们这里可以直接去重写onMeasure),确定自定义View的大小

2.背景的变换可以通过 canvas.drawRoundRect去绘制

3.对号就是绘制线,这个已经说过,最后完整代码如下:

package MyViews

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator


/**
 * Created by Administrator on 2018/7/9.
 */
class AnimationButton : View, View.OnClickListener {
    //动画集合
    val animatorset: AnimatorSet
    //背景颜色
    val bg_color = "#88880000"
    //控件的默认宽
    val bg_width = DisplayUtils.dip2px(context, 140F)
    //控件的默认高
    val bg_height = DisplayUtils.dip2px(context, 50F)
    //文字画笔
    val text_Paint: Paint
    //文字默认的大小
    val text_size = DisplayUtils.dip2px(context, 20F)
    //默认文字内容
    val text_content = "确认完成"
    //画背景矩形的画笔
    val bg_Paint: Paint
    //矩形长度默认变化
    var change: Float = 0F
        set(value) {
            field = value
            invalidate()
        }
    //背景默认角度,重写set方法,去自定义属性动画
    var default_Round = 0.0F
        set(value) {
            field = value
            invalidate()
        }
    //是否画上对钩
    var check: Boolean = false
    //对号画笔
    val checkPaint: Paint
    //对号的路径
    var checkPath: Path
    //对号的虚线绘制数组
    var intervals: FloatArray
    //路径变化的百分比
    var percent = 0F
        set(value) {
            //打开绘制的开关
            check = true
            field = value
            checkPaint.setPathEffect(getDashPathEffect())//设置画笔的偏移量
            //重新绘制
            invalidate()
        }

    //绘制线的变换
    fun getDashPathEffect(): PathEffect = DashPathEffect(intervals, intervals.get(0) - percent * intervals.get(0))

    //构造方法
    constructor(context: Context?) : this(context, null)

    //构造方法
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)

    //构造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)

    //构造方法
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
        //创建文字画笔
        text_Paint = Paint(Paint.ANTI_ALIAS_FLAG)
        //设置画笔颜色
        text_Paint.color = Color.WHITE
        //计算文字开始大小
        text_Paint.textSize = text_size
        //设置中心位置
        text_Paint.textAlign = Paint.Align.CENTER

        //创建画背景的画笔
        bg_Paint = Paint(Paint.ANTI_ALIAS_FLAG)
        //设置画笔颜色
        bg_Paint.color = Color.parseColor(bg_color)

        //创建对号画笔
        checkPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        checkPaint.color = Color.WHITE
        checkPaint.setStyle(Paint.Style.STROKE)//设checkPath置画线模式
        checkPaint.setStrokeWidth(8F) // 线条宽度为 20 像素
        //绘制对号的路径
        checkPath = Path()
        //计算对号关键点绝对坐标
        val startX = (bg_width - bg_height) / 2 + bg_height / 4
        val startY = bg_height / 3
        val guaiX = bg_width / 2
        val guaiY = bg_height * 2 / 3
        val endX = (bg_width - bg_height) / 2 + 5 * bg_height / 6
        val endY = bg_height / 4
        checkPath.moveTo(startX, startY)
        checkPath.lineTo(guaiX, guaiY)
        checkPath.lineTo(endX, endY)
        //计算出路径的长度
        val measure = PathMeasure(checkPath, false)
        intervals = floatArrayOf(measure.length, measure.length)
        //动画集合
        animatorset = AnimatorSet()

        //设置点击事件
        setOnClickListener(this)
        //设置动画完成监听,
        animatorset.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                // 调用完这个之后最后会再去调用一次绘制方法,我们可以在这里边去恢复之前的动画
                postDelayed({
                    check = false
                    default_Round = 0F
                    percent = 0F
                    change = 0F
                    text_Paint.alpha = 255
                    invalidate()
                }, 500)

            }
        })

    }


    //告诉父布局,我需要的尺寸
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        setMeasuredDimension(resolveSize(bg_width.toInt(), widthMeasureSpec), resolveSize(bg_height.toInt(), heightMeasureSpec))
    }

    //开始绘制
    override fun onDraw(canvas: Canvas) {
        drawRectBg(canvas)
        drawText(canvas)
        if (check) {
            drawCheck(canvas)
        }
    }

    //绘制背景
    fun drawRectBg(canvas: Canvas) {
        canvas.drawRoundRect(RectF(0F + change, 0F, bg_width - change, bg_height), default_Round, default_Round, bg_Paint)
    }

    //绘制文字
    fun drawText(canvas: Canvas) {
        //因为上边设置了x是中心点坐标,所以直接指定控件的一半就是x的中心位置
        var x = (bg_width / 2)
        val fontMetrics = text_Paint.getFontMetrics()
        //计算公式:baseline=getHeight()/2+(fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom
        var baseline = bg_height / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
        canvas.drawText(text_content, x, baseline, text_Paint)
    }

    //绘制对号
    fun drawCheck(canvas: Canvas) {
        canvas.drawPath(checkPath, checkPaint)
    }

    //触摸之后的属性动画
    override fun onClick(v: View) {
        //直角矩形变成圆角巨型的动画
        val Round = ObjectAnimator.ofFloat(this, "default_Round", 0F, 90F)
        //文字的透明度的变化动画
        val text_Alpha = ObjectAnimator.ofInt(text_Paint, "alpha", 255, 0)
        //距型变成圆动画
        val kuan = ObjectAnimator.ofFloat(this, "change", 0F, (bg_width - bg_height) / 2)
        //偏移量控制线的绘制过程
        val duihao = ObjectAnimator.ofFloat(this, "percent", 0.0F, 1.0F)
        //添加到集合,然后开始动画
        animatorset.play(Round).with(text_Alpha).with(kuan).before(duihao)
        animatorset.duration = 800
        animatorset.interpolator = LinearInterpolator()
        animatorset.start()
    }

}

这个时候使用者使用上边自定义控件设置包裹内容是没有问题的,但是如果使用者设置的宽度和高度比我们默认的还要小,那么就会出现问题,怎么解决这个问题呢?

只要我们能获取最后控件可使用的大小区域,然后改变我们的默认大小解决这个问题。

获取控件最后真正可以使用的大小是在onChangeSize方法中回调的,我们吧所有计算的逻辑放到onChangeSize方法中,然后根据比例去缩放字体的大小,就可以解决这个问题。

当然我们还有另外解决方式:就是在测量大小的时候就去缩放,绘制的时候也去缩放绘制!

代码不再贴,具体可以查看源码地址:github.com/XiFanYin/Cu…

注意:这里计算包含的条件如下图:圆的直径 = 巨型的高