本节内容
1.准备工作
2.绘制矩形区域
3.进度动画分析和高阶函数做回调
4.改变动画因子驱动动画
5.两端形变为半圆形
6.两端向中间靠拢形成圆
7.绘制勾勾或者叉叉
8.实现裁剪效果
效果展示
-
由于不能放视频,所以我会截一些中间片段。
-
首先就是一个长矩形
-
等红色矩形完全将绿色矩形覆盖之后,红色矩形先变为圆角,然后不断缩小
一、准备工作
-
1.绘制一个圆角矩形区域
-
2.进度变化
-
3.两端缩成一个半圆形
-
4.两端向中间靠拢,形成一个完整的圆
-
5.绘制勾勾 或者 叉叉
-
6.实现展开效果。勾勾并不是突然出现,而是一点点逐渐出现,有一个循序渐进的效果
1.创建一个类继承自View,并实现对应的构造方法
class ProgressView : View{
constructor(context: Context):super(context){}
constructor(context: Context,attrs: AttributeSet?):super(context,attrs){}
constructor(context: Context,attrs: AttributeSet?,style:Int):super(context,attrs,style){}
}
二、绘制矩形区域
1.首先,定义两个变量来记录矩形的宽和高,并在onSizeChanged方法里面给它赋值。再定义两个两边来记录中心点的坐标。
private var mWidth = 0f
private var mHeight = 0f
private var cx =0f
private var cy =0f
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
mWidth = width.toFloat()
mHeight = height.toFloat()
}
2.在onDraw里面调用drawRoundRect方法来画一个带有圆角的矩形框。里面有七个参数。
-
前四个代表矩形框左上右下的位置。左上为0,那么右下就为它们的宽度和高度,因为矩形框的画布也是一个矩形。
-
rx,ry 表示圆角的半径,如果rx,ry相同,那么就是一个扇形。如果不同,那么就有点像三角形的斜边,只不过是一条斜的弧线而已。(一般都让它们相等)
-
最后一个参数是画笔。
mRectPaint.color =Color.MAGENTA
canvas?.drawRoundRect(
0f, 0f, mWidth,mHeight,
cornerRadius, cornerRadius, RoundRectPaint
)
private var cornerRadius = 0f
private val mRectPaint = Paint().apply {
color = Color.MAGENTA
style = Paint.Style.FILL
}
三、进度动画分析和高阶函数做回调
1.进度变化效果相当于,新的矩形的宽度不断增加,我们的视觉感受就是有进度变化。
var progress = 0f
2.在绘制新的圆角矩形之前,我们要修改一下画笔的颜色。同理,在绘制之前的矩形时,我们也要修改一下画笔的颜色。不然下次启动动画时,画笔的颜色就都一样了。(见前面的代码)
mRectPaint.color = Color.BLUE
canvas?.drawRoundRect(0f,0f,progress*mWidth,mHeight,
cornerRadius,cornerRadius,mRectPaint)
3.绘制进度条分析:首先外部要先知道目前的状态,是在下载还是暂停。如果是下载的话,那么就启动下载,并获取下载数据,然后根据当前进度来绘制矩形框。
4.要实现这些效果,必须做到以下事情:
-
自定义的View需要监听点击事件
-
自定义的View需要将点击事件传递给外部
-
自定义的View可以接收外部的数据,调节进度
5.先监听一下点击事件,callBack一个函数回调
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN){
//将当前点击事件传递给外部
callBack?.let {back->
back()
}
}
return true
}
定义一个名为callBack的变量
var callBack:((Int)-> Unit)? = null
6.在外部设置回调函数的具体实现,mProgress是矩形的id。外部就是MainActivity里面
mProgress.callBack ={state->
}
7.设置几个表示状态的半生对象
companion object{
const val ON = 0
const val OFF = 1
}
8.定义一个变量来记录当前的状态,当我们点击的时候,在callBack里面就把state作为参数传递过去
private var state = ON
9.当我们点击之后,就要改变一下状态,也就是让state取反,为下一次点击做准备
callBack?.let {back->
back(state)
state = if(state== ON) OFF else ON
}
10.接上面第6点,如果在外部接收到的回调是ON,那么就开始下载,否则就暂停
mProgress.callBack ={state->
if (state == ProgressView.ON){
//下载
}else{
//暂停
}
}
四、改变动画因子驱动动画
1.在MainActivity里面写一个函数,模拟下载数据。ValueAnimator就是记录一个数据变到另一个数据的过程,主要是监听动画因子。
private fun downLoadAnim(){
ValueAnimator.ofFloat(0f,1.0f).apply {
duration = 2000
addUpdateListener {
( it.animatedValue as Float ).also {value->
mProgress.progress = value
}
}
}
}
2.在ProgressView里面调用set方法,接收从MainActivity里面传递过来的数据。
set(value) {
//记录外部传递过来的值
field = value
//刷新
invalidate()
}
3.在MainActivity里面添加一个动画变量,并让downLoadAnim动画赋值给它,以便控制它的暂停和开始。如果是暂停的状态,就让它resume,重新启动可以保存前面已有的状态,不会再从头来过。
private var mDownloadAnim:ValueAnimator? = null
downLoadAnim()
//设置回调函数的具体实现
mProgress.callBack ={state->
if (state == ProgressView.ON){
//下载
if (mDownloadAnim?.isPaused!!){
mDownloadAnim?.resume()
}else {
mDownloadAnim?.start()
}
}else{
//暂停
mDownloadAnim?.pause()
}
}
五、两端形变为半圆
1.当进度加载完成之后,就要开启两端形变为半圆的动画。所以在set方法里面,我们要判断一下进度是不是为1.0,如果是就调用实现动画的函数。
if(value==1.0f){
startFinishAnim()
}
2.实现这个函数
private fun startFinishAnim(){
//变成半圆
val changeIntoCircle= ValueAnimator.ofFloat(0f,mHeight/2f).apply {
duration = 1000
addUpdateListener {anim->
cornerRadius= anim.animatedValue as Float
invalidate()
}
}
}
3.启动程序,点击之后,最终可以得到如下效果。
六、两端向中间靠拢形成圆
1.要实现这个效果,就是在drawRoundRec的时候,修改一下left和right这两个参数,左边往右靠(让transX从0→width/2-radius),右边往左靠(让right从mWidth→mWidth-2*transX)
2.定义一个变化因子,作为左边向中间靠的因子
//定义中间靠拢的动画因子
private var transX = 0f
3.修改一下前面绘制圆角矩形的参数。因为往中间靠的时候,可绘制视图也在不断变化,所有右侧的距离就为视图的宽度减去间距,右边的间距和左边的间距一样,所以就为mWidth-transX
canvas?.drawRoundRect(
transX, 0f, mWidth-transX,mHeight,
cornerRadius, cornerRadius, mRectPaint
)
canvas?.drawRoundRect(
transX, 0f, progress * mWidth-transX, mHeight,
cornerRadius, cornerRadius, mRectPaint
)
4.实现向中间靠拢的动画,由于半径 = mHeight/2。所以transX由0→(mWidth-mHeight)/2)
val moveToCenterAnim= ValueAnimator.ofFloat(0f,(mWidth-mHeight)/2).apply {
duration = 1000
addUpdateListener {anim->
transX= anim.animatedValue as Float
invalidate()
}
}
5.然后让这两个动画先后运行起来
AnimatorSet().apply {
playSequentially(changeIntoCircle,moveToCenterAnim)
start()
}
6.最后得到如下效果
七、绘制勾勾或者叉叉
1.在绘制勾勾之前,我们要先建模,确定绘制区域的宽高以及勾勾的坐标。在下图中,我们以中心点的坐标为cx,cy。
然后我们再把勾勾所在的矩形区域进行划分。我们令这个勾勾所在的正方形的边长为gWidth,那么我们就可以写除各个点的坐标
-
x1(cx - gWidth/2,cy)
-
x2(cx - gWidth/8,cy+gWidth/2)
-
x3(cx + gWidth/2,cy - gWidth/4)
2.绘制勾勾相当于画两条线,只要能确定起点与终点的坐标即可,也就是三个顶点坐标。
3.下面来确定叉叉的四个坐标
-
x1(cx -gWidth/4,cy -gWidth/4)
-
x2(cx -gWidth/4,cy +gWidth/4)
-
x3(cx +gWidth/4,cy -gWidth/4)
-
x4(cx +gWidth/4,cy +gWidth/4)
4.当矩形完全向中间靠拢聚成一个圆之后,我们才开始绘制勾勾或者叉叉。那么就要来记录一下加载成功还是失败。先定义两个静态变量来记录成功和失败。
companion object{
const val ON = 0
const val OFF = 1
const val SUCCESS = 2
const val FAILURE = 3
}
5.然后用一个变量来记录下载的结果
var resultStatus = SUCCESS
6.下载的结果一般由外部返回给我们,由于我们前面是用动画模拟了一下下载过程,所以不是真的下载,那我们在外部也就自己设置一下返回结果。
mProgress.resultStatus = ProgressView.SUCCESS
7.在绘制之前,我们要提供一下绘制的画笔,并用一个变量来记录绘制的路径,还要一个变量来记录绘制勾勾的矩形边长
//勾勾叉叉的画笔
private val markPaint = Paint().apply {
color=Color.WHITE
strokeWidth = 10f
style = Paint.Style.STROKE
}
//绘制勾勾或者叉叉的路径Path
private var markPath = Path()
private var markSize = 0f
8.在onSizeChanged方法里面,可以确定中心点的坐标以及勾勾或者叉叉的矩形边长。绘制勾勾叉叉可以在onSizeChanged方法里面进行。
markSize = height/3f
//中心点坐标
cx = width/2f
cy = height/2f
if(resultStatus== SUCCESS ){
//绘制勾勾
markPath.apply {
//markPath有相应的函数可以移动点的位置,然后再画线
moveTo(cx-markSize/2,cy)
lineTo(cx-markSize/8,cy+markSize/2)
lineTo(cx+markSize/2,cy-markSize/4)
}
}else{
//绘制叉叉
markPath.apply {
moveTo(cx-markSize/2,cy-markSize/2)
lineTo(cx+markSize/2,cy+markSize/2)
moveTo(cx-markSize/2,cy+markSize/2)
lineTo(cx+markSize/2,cy-markSize/2)
}
}
9.在onDraw方法里面,先判断圆角矩形是否已经聚拢成了一个圆,如果是的话再绘制叉叉或勾勾
if( transX ==( width - height)/2f){
canvas?.drawPath(markPath,markPaint)
}
10.运行程序之后,可以得到如下效果
八、实现裁剪效果
1.如果仅仅只是绘制还不行,因为我们还需要一个展开的效果,让这个勾勾或者叉叉缓慢出现,而不是突然出现。
2.想要实现这样的效果,我们可以绘制一个遮罩层,也就是一个矩形区域,所以我们用一个变量来记录该矩形区域
private var coverRect = Rect()
3.在onDraw方法里面绘制遮罩层,遮罩层的画笔和背景一样
coverRect.set((cx-markSize/2).toInt(),
(cy-markSize/2).toInt(),
(cx+markSize/2).toInt(),
(cy+markSize/2).toInt()
)
canvas?.drawRect(coverRect,mRectPaint)
4.把遮罩层添加好了之后,我们只需要让遮罩层不断向右边移动,就有逐渐出现的动画效果了。所以我们要定义一个动画因子,然后把它加入到遮罩层的横坐标里面
private var clipWidth = 0
coverRect.set((cx-markSize/2).toInt()+clipWidth.toInt(),
(cy-markSize/2).toInt(),
(cx+markSize/2).toInt(),
(cy+markSize/2).toInt()
)
5.我们再添加一个裁剪动画
val clipAnim = ValueAnimator.ofFloat(0f,markSize+20).apply {
duration = 1000
addUpdateListener {anim->
clipWidth= anim.animatedValue as Float
invalidate()
}
}
6.最后,把它添加到AnimatorSet里面,让它们三个先后运行
AnimatorSet().apply {
playSequentially(changeIntoCircle,moveToCenterAnim,clipAnim)
start()
}
7.运行之后,我们发现绘制的时候多出来了一点,主要是因为我们忽略了画笔的宽度,所以会露一点出来。那我们绘制遮罩层时就得加上画笔的宽度10
coverRect.set(
((cx-markSize/2).toInt()+clipWidth).toInt(),
(cy-markSize/2).toInt(),
(cx+markSize/2).toInt()+10,
(cy+markSize/2).toInt()+10
)
canvas?.drawRect(coverRect,mRectPaint)