Android开发Android技术知识Android开发经验谈

Android自定义View(14) 《手写一个MIUI的相机快

2021-09-13  本文已影响0人  非典型程序猿

概述

之前就一直觉得MIUI的设计团队和开发团队很牛逼,看着手里的K30 pro,觉得相机的快门键也是不错的练习素材,今天就手写一个MIUI的相机快门键吧~

先看效果

shutter_view.gif

效果就是这样啦,轻按一下是拍照,长按是进行录像,看起来几乎是完美还原了,那么接下来我们开始分析这个控件如何实现

分析控件状态

根据我们的观察,未做操作时,按钮是一个圆圈,当点击按钮时,圆圈开始缩小,当缩小状态维持一段时间后,圆圈开始扩大直到大于初始状态的圆圈,同时不透明度不断变低,最后开始录像则开始绘制2条路径。最后恢复到初始状态,那么简单来看我们就可以分为3步了

核心代码

参数定义

 // 定义当前的操作
    companion object{
        const val unknownOp = 0
        const val takePhotoOp = 1
        const val takeVideoOp = 2
    }
    var option = unknownOp

    var paint = Paint()
    var listener : ShutterTouchEventListener
    init {
        paint.style = Paint.Style.STROKE
        paint.isAntiAlias = true
        paint.strokeJoin = Paint.Join.ROUND
        paint.strokeWidth = 20f
        listener = this
    }
    // 开始按下去的动画
    lateinit var pictureAnimator : ValueAnimator
    var currentPictureValue = 0f
    var pictureDuration = 1000L
    // 长按执行到Video录制的动画
    lateinit var videoAnimator : ValueAnimator
    var currentVideoValue = 0f
    var videoDuration = 15000L
    // 圆心x坐标
    var centerX = 0f
    // 圆心y坐标
    var centerY = 0f
    // 初始半径
    var radius = 0f
    // 绘制的半径
    var drawRadius = 0f
    // 缩小的半径的最小值
    var minRadius = 0f
    // 缩小的半径的最大值
    var maxRadius = 0f
    // 画笔的不透明度
    var paintAlpha = 255

三个关键动画

第一个动画(拍照动画的初始化),这里半径和画笔的不透明度都是按动画值计算的,我们把动画值分为了3部分,前1/4执行缩小动画,中间的1/2是保持缩小状态,而最后的1/4是放大半径且画笔不透明度逐渐降低。

    private fun initPictureAnim(){
        pictureAnimator = ValueAnimator.ofFloat(0F, 100F)
        pictureAnimator.duration = pictureDuration
        pictureAnimator.addUpdateListener { valueAnimator ->
            currentPictureValue = valueAnimator.animatedValue as Float
           if (currentPictureValue<100F/4){
               drawRadius =  radius-(radius-minRadius)*(currentPictureValue/(100f/4))
               paintAlpha =255
            }else if (currentPictureValue>100F/4 && currentPictureValue<(100F)/4*3){
               drawRadius =  minRadius
               paintAlpha = 255
            }else{
               drawRadius =  minRadius + (maxRadius-minRadius)*((currentPictureValue-(100f/4*3))/(100f/4))
               paintAlpha = (255-205*(currentPictureValue-100f/4*3)/(100f/4)).toInt()
            }
            postInvalidate()
        }
        pictureAnimator.addListener(object  : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {
                option = takePhotoOp
            }

            override fun onAnimationEnd(p0: Animator?) {
                if (option != unknownOp){
                    videoAnimator.start()
                }
            }

            override fun onAnimationCancel(p0: Animator?) {
                drawRadius = radius
                if (listener!=null){
                    listener.takePicture()
                }
                option = unknownOp
                postInvalidate()
            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }

第二个动画(video录制的动画)这里其实就是获取一个当前的动画值,在绘制时利用这个动画值来绘制已经进行的进度和未执行完的进度,动画结束时保持初始状态

  private fun initVideoAnim(){
        videoAnimator = ValueAnimator.ofFloat(0F,100F)
        videoAnimator.duration = videoDuration
        videoAnimator.addUpdateListener { valueAnimator ->
            currentVideoValue = valueAnimator.animatedValue as Float
            postInvalidate()
        }
        videoAnimator.addListener(object  : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {
                option = takeVideoOp
                if (listener!=null){
                    listener.videoStart()
                }
            }

            override fun onAnimationEnd(p0: Animator?) {
                if (listener!=null){
                    listener.videoEnd()
                }
                option = unknownOp
                postInvalidate()
            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }

第三个动画,就是恢复到初始状态的动画,为了让最后的按钮看起来丝滑

    private fun initCancelAnim(){
        cancelAnimator = ValueAnimator.ofFloat(0f,100f)
        cancelAnimator.duration = cancelDuration
        cancelAnimator.addUpdateListener { valueAnimator ->
            currentCancelValue = valueAnimator.animatedValue as Float
            drawRadius = if (animEndRadius>radius){
                animEndRadius - (animEndRadius - radius)*(currentCancelValue/100f)
            }else {
                animEndRadius + (animEndRadius - radius)*(currentCancelValue/100f)
            }
            postInvalidate()
        }
        cancelAnimator.addListener(object  : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {

            }

            override fun onAnimationEnd(p0: Animator?) {

            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }

绘制函数

初始状态

 private fun drawUnknownOp(canvas: Canvas){
        paint.color = Color.WHITE
        paint.alpha = 255
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
    }

拍照的动画

private fun drawTakePicture(canvas: Canvas){
        paint.color = Color.WHITE
        paint.alpha = paintAlpha
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
    }

录像的动画

    private fun drawTakeVideo(canvas: Canvas){
        var path = Path()
        path.addCircle(centerX,centerY,maxRadius,Path.Direction.CW)
        var pathMeasure  = PathMeasure()
        pathMeasure.setPath(path,true)
        var currentPath = Path()
        var leftPath = Path()
        pathMeasure.getSegment(0F,currentVideoValue/100*pathMeasure.length,currentPath,true)
        pathMeasure.getSegment(currentVideoValue/100*pathMeasure.length,pathMeasure.length,leftPath,true)
        paint.color = Color.WHITE
        paint.alpha = paintAlpha
        canvas.drawPath(leftPath,paint)
        paint.color = Color.WHITE
        paint.alpha = 255
        canvas.drawPath(currentPath,paint)
    }

其实就是当按下去时我们开始播放拍照的动画,如果中途抬起手指,我们则取消这个动画,同时反馈拍照事件,如果未抬起,动画执行完毕后,则在onAnimationEnd()方法中开启video录制的动画,同理,当手指抬起时我们终止video的录制动画,在onAnimationStart()中回调录制开始事件,在onAnimationEnd()中回调录制结束事件,当两个动画结束时开始执行恢复状态的动画。

完整源码

View部分源码

package com.tx.txcustomview.view

import android.animation.Animator
import android.animation.Animator.AnimatorListener
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.Toast

/**
 * create by xu.tian
 * @date 2021/9/9
 */
class ShutterView : View ,ShutterTouchEventListener{
    // 定义当前的操作
    companion object{
        const val unknownOp = 0
        const val takePhotoOp = 1
        const val takeVideoOp = 2
    }
    var option = unknownOp

    var paint = Paint()
    var listener : ShutterTouchEventListener
    init {
        paint.style = Paint.Style.STROKE
        paint.isAntiAlias = true
        paint.strokeJoin = Paint.Join.ROUND
        paint.strokeWidth = 20f
        listener = this
    }
    // 开始按下去的动画
    lateinit var pictureAnimator : ValueAnimator
    var currentPictureValue = 0f
    var pictureDuration = 1000L

    // 长按执行到Video录制的动画
    lateinit var videoAnimator : ValueAnimator
    var currentVideoValue = 0f
    var videoDuration = 15000L

    // 取消操作时的动画
    lateinit var cancelAnimator : ValueAnimator
    var currentCancelValue = 0f
    var cancelDuration = 200L

    // 圆心x坐标
    var centerX = 0f
    // 圆心y坐标
    var centerY = 0f
    // 初始半径
    var radius = 0f
    // 绘制的半径
    var drawRadius = 0f
    // 缩小的半径的最小值
    var minRadius = 0f
    // 缩小的半径的最大值
    var maxRadius = 0f
    // 画笔的不透明度
    var paintAlpha = 255
    // 拍照或者录像动画结束时的半径
    var animEndRadius = 0f

    constructor(context: Context): super(context)

    constructor(context: Context,attributeSet: AttributeSet): super(context,attributeSet){
        initPictureAnim()
        initVideoAnim()
        initCancelAnim()
        setLayerType(LAYER_TYPE_SOFTWARE,null)
        rotation = -90f
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        when(option) {
            unknownOp -> drawUnknownOp(canvas)
            takePhotoOp -> drawTakePicture(canvas)
            takeVideoOp -> drawTakeVideo(canvas)
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerX = (w/2).toFloat()
        centerY = (h/2).toFloat()
        radius = if (centerX<centerY){
            centerX/10*6
        }else{
            centerY/10*6
        }
        drawRadius = radius
        minRadius = centerX/10*5
        maxRadius = centerX/10*8
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> actionDown()
            MotionEvent.ACTION_UP -> actionUp()
        }
        return true
    }

    private fun initPictureAnim(){
        pictureAnimator = ValueAnimator.ofFloat(0F, 100F)
        pictureAnimator.duration = pictureDuration
        pictureAnimator.addUpdateListener { valueAnimator ->
            currentPictureValue = valueAnimator.animatedValue as Float
           if (currentPictureValue<100F/4){
               drawRadius =  radius-(radius-minRadius)*(currentPictureValue/(100f/4))
               paintAlpha =255
            }else if (currentPictureValue>100F/4 && currentPictureValue<(100F)/4*3){
               drawRadius =  minRadius
               paintAlpha = 255
            }else{
               drawRadius =  minRadius + (maxRadius-minRadius)*((currentPictureValue-(100f/4*3))/(100f/4))
               paintAlpha = (255-205*(currentPictureValue-100f/4*3)/(100f/4)).toInt()
            }
            postInvalidate()
        }
        pictureAnimator.addListener(object  : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {
                option = takePhotoOp
            }

            override fun onAnimationEnd(p0: Animator?) {
                animEndRadius = drawRadius
                if (option != unknownOp){
                    videoAnimator.start()
                }else{
                    cancelAnimator.start()
                }

            }

            override fun onAnimationCancel(p0: Animator?) {
                drawRadius = radius
                if (listener!=null){
                    listener.takePicture()
                }
                option = unknownOp
            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }



    private fun initVideoAnim(){
        videoAnimator = ValueAnimator.ofFloat(0F,100F)
        videoAnimator.duration = videoDuration
        videoAnimator.addUpdateListener { valueAnimator ->
            currentVideoValue = valueAnimator.animatedValue as Float
            postInvalidate()
        }
        videoAnimator.addListener(object  : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {
                option = takeVideoOp
                if (listener!=null){
                    listener.videoStart()
                }
            }

            override fun onAnimationEnd(p0: Animator?) {
                if (listener!=null){
                    listener.videoEnd()
                }
                option = unknownOp
                animEndRadius = drawRadius
                cancelAnimator.start()
            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }
    private fun initCancelAnim(){
        cancelAnimator = ValueAnimator.ofFloat(0f,100f)
        cancelAnimator.duration = cancelDuration
        cancelAnimator.addUpdateListener { valueAnimator ->
            currentCancelValue = valueAnimator.animatedValue as Float
            drawRadius = if (animEndRadius>radius){
                animEndRadius - (animEndRadius - radius)*(currentCancelValue/100f)
            }else {
                animEndRadius + (animEndRadius - radius)*(currentCancelValue/100f)
            }
            postInvalidate()
        }
        cancelAnimator.addListener(object  : Animator.AnimatorListener{
            override fun onAnimationStart(p0: Animator?) {

            }

            override fun onAnimationEnd(p0: Animator?) {

            }

            override fun onAnimationCancel(p0: Animator?) {

            }

            override fun onAnimationRepeat(p0: Animator?) {

            }
        })
    }

    private fun actionDown(){
        pictureAnimator.start()
    }

    private fun actionUp(){
        if(option == takePhotoOp){
            pictureAnimator.cancel()
        }else{
            videoAnimator.cancel()
        }
    }

    private fun drawUnknownOp(canvas: Canvas){
        paint.color = Color.WHITE
        paint.alpha = 255
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
    }

    private fun drawTakePicture(canvas: Canvas){
        paint.color = Color.WHITE
        paint.alpha = paintAlpha
        canvas.drawCircle(centerX,centerY,drawRadius,paint)
    }

    private fun drawTakeVideo(canvas: Canvas){
        var path = Path()
        path.addCircle(centerX,centerY,maxRadius,Path.Direction.CW)
        var pathMeasure  = PathMeasure()
        pathMeasure.setPath(path,true)
        var currentPath = Path()
        var leftPath = Path()
        pathMeasure.getSegment(0F,currentVideoValue/100*pathMeasure.length,currentPath,true)
        pathMeasure.getSegment(currentVideoValue/100*pathMeasure.length,pathMeasure.length,leftPath,true)
        paint.color = Color.WHITE
        paint.alpha = paintAlpha
        canvas.drawPath(leftPath,paint)
        paint.color = Color.WHITE
        paint.alpha = 255
        canvas.drawPath(currentPath,paint)
    }

    override fun takePicture() {
        Toast.makeText(context,"takePicture",Toast.LENGTH_SHORT).show()
    }

    override fun videoStart() {
        Toast.makeText(context,"videoStart",Toast.LENGTH_SHORT).show()
    }

    override fun videoEnd() {
        Toast.makeText(context,"videoEnd",Toast.LENGTH_SHORT).show()
    }

}

事件定义接口文件

package com.tx.txcustomview.view

/**
 * create by xu.tian
 * @date 2021/9/13
 */
interface ShutterTouchEventListener {
    fun takePicture()
    fun videoStart()
    fun videoEnd()
}

总结

今天又是台风天,刚刚人都差点被吹没了.今天就写到这里吧~see you

上一篇下一篇

猜你喜欢

热点阅读