Android自定义控件-仿keep结束按钮,带结束动效


属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TerminateButton">
<attr name="iconColor" format="color" />
<attr name="iconSize" format="dimension" />
<attr name="backgroundColor" format="color" />
<attr name="progressBackgroundColor" format="color" />
<attr name="progressColor" format="color" />
<attr name="isAnimation" format="boolean" />
<attr name="topLeftRadius_terminate" format="dimension" />
<attr name="bottomLeftRadius_terminate" format="dimension" />
<attr name="topRightRadius_terminate" format="dimension" />
<attr name="bottomRightRadius_terminate" format="dimension" />
<attr name="shape_terminate" format="enum">
<enum name="circle" value="0" />
<enum name="square" value="1" />
</attr>
</declare-styleable>
</resources>
实现
package com.wuleizhenshang.fitness.mod_sport_record_detail
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PathMeasure
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.core.animation.addListener
class TerminateButton(context: Context, attrs: AttributeSet) : View(context, attrs) {
companion object {
const val STYLE_CIRCLE = 0
const val STYLE_ROUND_RECT = 1
}
private var _iconColor = Color.WHITE
private var _iconSize = 0f
private var _halfIconSize = 0f
private var _backgroundColor = Color.RED
private var _progressBackgroundColor = Color.GRAY
private var _progressColor = Color.BLUE
private var _topLeftRadius = 0f
private var _topRightRadius = 0f
private var _bottomLeftRadius = 0f
private var _bottomRightRadius = 0f
private var _style = STYLE_CIRCLE
private var _isAnimation = true
private var _width = 0f
private var _halfWidth = 0f
private var _height = 0f
private var _halfHeight = 0f
private var _minSize = 0f
private val _paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
}
private val _progressPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
}
private val _backgroundPath = Path()
private val _progressBackgroundPath = Path()
private val _progressPath = Path()
private var _progressAnimator: ValueAnimator? = null
init {
val typedArray =
context.theme.obtainStyledAttributes(attrs, R.styleable.TerminateButton, 0, 0)
try {
_iconSize = typedArray.getDimension(R.styleable.TerminateButton_iconSize, 0f)
_halfIconSize = _iconSize / 2
_iconColor = typedArray.getColor(R.styleable.TerminateButton_iconColor, Color.WHITE)
_backgroundColor =
typedArray.getColor(R.styleable.TerminateButton_backgroundColor, Color.RED)
_progressBackgroundColor =
typedArray.getColor(R.styleable.TerminateButton_progressBackgroundColor, Color.GRAY)
_progressColor =
typedArray.getColor(R.styleable.TerminateButton_progressColor, Color.BLUE)
_topLeftRadius =
typedArray.getDimension(R.styleable.TerminateButton_topLeftRadius_terminate, 0f)
if (_topLeftRadius < 0) {
_topLeftRadius = 0f
}
_topRightRadius =
typedArray.getDimension(R.styleable.TerminateButton_topRightRadius_terminate, 0f)
if (_topRightRadius < 0) {
_topRightRadius = 0f
}
_bottomLeftRadius =
typedArray.getDimension(R.styleable.TerminateButton_bottomLeftRadius_terminate, 0f)
if (_bottomLeftRadius < 0) {
_bottomLeftRadius = 0f
}
_bottomRightRadius =
typedArray.getDimension(R.styleable.TerminateButton_bottomRightRadius_terminate, 0f)
if (_bottomRightRadius < 0) {
_bottomRightRadius = 0f
}
_style = typedArray.getInt(R.styleable.TerminateButton_shape_terminate, STYLE_CIRCLE)
_isAnimation = typedArray.getBoolean(R.styleable.TerminateButton_isAnimation, true)
} finally {
typedArray.recycle()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
_width = w.toFloat()
_height = h.toFloat()
_halfWidth = _width / 2
_halfHeight = _height / 2
_minSize = if (_width > _height) _height else _width
if (_topLeftRadius > _minSize) {
_topLeftRadius = _minSize
}
if (_topRightRadius > _minSize) {
_topRightRadius = _minSize
}
if (_bottomLeftRadius > _minSize) {
_bottomLeftRadius = _minSize
}
if (_bottomRightRadius > _minSize) {
_bottomRightRadius = _minSize
}
initBackgroundPath()
initProgressBackgroundPath()
initProgressPath()
}
private fun initBackgroundPath() {
if (_style == STYLE_ROUND_RECT) {
_backgroundPath.reset()
val balance = _minSize / 2
_backgroundPath.addRoundRect(
_halfWidth - balance,
_halfHeight - balance,
_halfWidth + balance,
_halfHeight + balance,
floatArrayOf(
_topLeftRadius,
_topLeftRadius,
_topRightRadius,
_topRightRadius,
_bottomRightRadius,
_bottomRightRadius,
_bottomLeftRadius,
_bottomLeftRadius
),
Path.Direction.CW
)
}
}
private fun initProgressBackgroundPath() {
if (_style == STYLE_ROUND_RECT) {
_progressBackgroundPath.reset()
val balance = _minSize * 8 / 24
_progressBackgroundPath.addRoundRect(
_halfWidth - balance,
_halfHeight - balance,
_halfWidth + balance,
_halfHeight + balance,
floatArrayOf(
_topLeftRadius,
_topLeftRadius,
_topRightRadius,
_topRightRadius,
_bottomRightRadius,
_bottomRightRadius,
_bottomLeftRadius,
_bottomLeftRadius
),
Path.Direction.CW
)
}
}
private fun initProgressPath() {
if (_style == STYLE_ROUND_RECT) {
_progressPath.reset()
val pathMeasure = PathMeasure(_progressBackgroundPath, false)
val progressPathLength = pathMeasure.length
val progressLength = progressPathLength * _progress
pathMeasure.getSegment(0f, progressLength, _progressPath, true)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
_paint.color = _backgroundColor
if (_style == STYLE_CIRCLE) {
canvas.drawCircle(_halfWidth, _halfHeight, _minSize / 2, _paint)
}
else if (_style == STYLE_ROUND_RECT) {
canvas.drawPath(_backgroundPath, _paint)
}
_paint.color = _iconColor
canvas.drawRect(
_halfWidth - _halfIconSize,
_halfHeight - _halfIconSize,
_halfWidth + _halfIconSize,
_halfHeight + _halfIconSize,
_paint
)
if (_isProgressAnimating) {
val per = _minSize / 24
val balance = _minSize / 3
if (_style == STYLE_CIRCLE) {
_progressPaint.apply {
color = _progressBackgroundColor
strokeWidth = per
}
canvas.drawCircle(_halfWidth, _halfHeight, balance, _progressPaint)
_progressPaint.apply {
color = _progressColor
strokeWidth = per
}
canvas.drawArc(
_halfWidth - balance,
_halfHeight - balance,
_halfWidth + balance,
_halfHeight + balance,
-90f,
360 * _progress,
false,
_progressPaint
)
} else if (_style == STYLE_ROUND_RECT) {
_progressPaint.apply {
color = _progressBackgroundColor
strokeWidth = per
}
canvas.drawPath(_progressBackgroundPath, _progressPaint)
_progressPaint.apply {
color = _progressColor
strokeWidth = per
}
canvas.drawPath(_progressPath, _progressPaint)
}
}
}
private var _scaleFactor = 1f
private var _progress = 0f
private var _isProgressAnimating = false
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (!_isAnimation) {
_onTerminateListener?.invoke(true)
return super.onTouchEvent(event)
}
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
startAmplifyAnimation()
_progressAnimator = startProgressAnimation {
_onTerminateListener?.invoke(it)
startReduceAnimation(onEnd = {
invalidate()
})
}
}
MotionEvent.ACTION_MOVE -> {
}
MotionEvent.ACTION_UP -> {
if (_isProgressAnimating) {
_progressAnimator?.cancel()
}
}
MotionEvent.ACTION_CANCEL -> {
if (_isProgressAnimating) {
_progressAnimator?.cancel()
}
}
}
return true
}
private fun startAmplifyAnimation(onEnd: (() -> Unit)? = null) {
val scaleX = ObjectAnimator.ofFloat(this, "scaleX", _scaleFactor, 1.2f)
val scaleY = ObjectAnimator.ofFloat(this, "scaleY", _scaleFactor, 1.2f)
val animatorSet = AnimatorSet()
animatorSet.playTogether(scaleX, scaleY)
animatorSet.duration = 300
animatorSet.addListener(onEnd = {
_scaleFactor = 1.2f
_width = width.toFloat()
_height = height.toFloat()
_halfWidth = _width / 2
_halfHeight = _height / 2
_minSize = if (_width > _height) _height else _width
initBackgroundPath()
initProgressBackgroundPath()
initProgressPath()
Log.d("TerminateButton放大", "width: $_width, height: $_height")
onEnd?.invoke()
})
animatorSet.start()
}
private fun startReduceAnimation(onEnd: (() -> Unit)? = null) {
val scaleX = ObjectAnimator.ofFloat(this, "scaleX", _scaleFactor, 1.0f)
val scaleY = ObjectAnimator.ofFloat(this, "scaleY", _scaleFactor, 1.0f)
val animatorSet = AnimatorSet()
animatorSet.playTogether(scaleX, scaleY)
animatorSet.duration = 300
animatorSet.addListener(onEnd = {
_scaleFactor = 1.0f
_width = width.toFloat()
_height = height.toFloat()
_halfWidth = _width / 2
_halfHeight = _height / 2
_minSize = if (_width > _height) _height else _width
initBackgroundPath()
initProgressBackgroundPath()
initProgressPath()
Log.d("TerminateButton缩小", "width: $_width, height: $_height")
onEnd?.invoke()
})
animatorSet.start()
}
private fun startProgressAnimation(onCompleted: ((Boolean) -> Unit)? = null): ValueAnimator {
val progressAnimator = ValueAnimator.ofFloat(0f, 1f)
progressAnimator.duration = 1000
var isCallback = false
progressAnimator.addUpdateListener { animation ->
_progress = animation.animatedValue as Float
initProgressPath()
invalidate()
}
progressAnimator.addListener(onStart = {
_isProgressAnimating = true
}, onEnd = {
_isProgressAnimating = false
if (!isCallback) {
if (_progress == 1f) {
onCompleted?.invoke(true)
} else {
onCompleted?.invoke(false)
}
isCallback = true
}
_progress = 0f
progressAnimator.removeAllListeners()
}, onCancel = {
_isProgressAnimating = false
if (!isCallback) {
if (_progress == 1f) {
onCompleted?.invoke(true)
} else {
onCompleted?.invoke(false)
}
isCallback = true
}
_progress = 0f
progressAnimator.removeAllListeners()
})
progressAnimator.start()
return progressAnimator
}
private var _onTerminateListener: ((Boolean) -> Unit)? = null
fun setOnTerminateListener(listener: ((Boolean) -> Unit)) {
_onTerminateListener = listener
}
}
简单使用
<com.wuleizhenshang.fitness.mod_sport_record_detail.TerminateButton
android:id="@+id/terminateButton"
android:layout_width="200dp"
android:layout_height="200dp"
app:bottomRightRadius_terminate="50dp"
app:iconSize="40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shape_terminate="square"
app:topLeftRadius_terminate="20dp"
app:topRightRadius_terminate="30dp" />
binding.terminateButton.setOnTerminateListener { bool ->
Toast.makeText(this, "bool = $bool", Toast.LENGTH_SHORT).show()
}