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