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

概述

之前就一直觉得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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341