一. 文字绘制坐标问题
在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…
注意:这里计算包含的条件如下图:圆的直径 = 巨型的高
