Android开发(19)——自定义炫酷进度

本节内容

1.准备工作

2.绘制矩形区域

3.进度动画分析和高阶函数做回调

4.改变动画因子驱动动画

5.两端形变为半圆形

6.两端向中间靠拢形成圆

7.绘制勾勾或者叉叉

8.实现裁剪效果

效果展示
  • 由于不能放视频,所以我会截一些中间片段。
  • 首先就是一个长矩形
1.png
  • 点击该矩形之后,会有红色的矩形覆盖上来
2.png
  • 等红色矩形完全将绿色矩形覆盖之后,红色矩形先变为圆角,然后不断缩小
3.png
  • 最后缩成一个圆,然后中间出现一个勾
4.png
一、准备工作
  • 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.启动程序,点击之后,最终可以得到如下效果。
image.png
六、两端向中间靠拢形成圆
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.最后得到如下效果
image.png
七、绘制勾勾或者叉叉
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)

你可能感兴趣的:(Android开发(19)——自定义炫酷进度)