美文网首页Android开发Android技术知识Android开发经验谈
Android自定义View(14) 《手写一个MIUI的相机快

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

作者: 非典型程序猿 | 来源:发表于2021-09-13 21:09 被阅读0次

    概述

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

    先看效果

    shutter_view.gif

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

    分析控件状态

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

    • 1.第一个动画是先缩小,然后维持缩小状态,再不断扩大并同时降低透明度,到动画最大值时停止绘制,用给拍照使用
    • 2.第二个动画是绘制一个圆中的两条路径,第一条是已经进行的动画值对应的长度,第二条是剩余的动画值所对应的总长度
    • 3.第三个动画就是恢复到最初的状态,也就是一个圆圈半径不断缩小的过程
      我们用第一个动画是否执行完来判断当前是否是录像操作,如果执行到第一个动画,中途动画被调用cancel后,我们认为是拍照操作,如果在第一个动画结束后执行到第二个动画,那就是开始录像,第二个动画调用cancel后就是录像结束的操作。第三个动画就是当前两个动画结束时根据当时的状态来进行第三个动画,恢复到最初的状态

    核心代码

    参数定义

     // 定义当前的操作
        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

    相关文章

      网友评论

        本文标题:Android自定义View(14) 《手写一个MIUI的相机快

        本文链接:https://www.haomeiwen.com/subject/bwafgltx.html