1、最近接到一个送礼需求,需要用户用礼物图片画一个效果,然后保存送出去,重新绘制出来
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.animation.LinearInterpolator
import androidx.core.animation.doOnEnd
import kotlin.math.hypot
class BrushSignatureView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
/** 所有笔画 */
private val strokes = mutableListOf<MutableList<StrokePoint>>()
private var currentStroke: MutableList<StrokePoint>? = null
/** 是否允许用户手绘 */
var enableDrawing: Boolean = true
/** 回放速度(ms/点) */
var mSpeed: Long = 20L
/** 笔刷图片 */
private var brushBitmap: Bitmap? = null
private var originalBrushBitmap: Bitmap? = null
/** 点间隔(外部 dp) */
var stampDistanceDp: Float = 2f
set(value) {
field = value
stampDistancePx = dp2px(value)
}
private var stampDistancePx: Float = dp2px(2f)
/** 图片大小(外部 dp) */
var brushSizeDp: Float = 24f
set(value) {
field = value
brushSizePx = dp2px(value).toInt()
resizeBrush()
}
private var brushSizePx: Int = dp2px(24f).toInt()
/** 动画相关 */
private var replayAnimator: ValueAnimator? = null
private var replayTempStrokes: MutableList<MutableList<StrokePoint>>? = null
private var isReplaying = false
private var blinkingAnimator: ValueAnimator? = null
private var isFlashing = false
private var flashAlpha = 255
/** 回放结束回调 */
var onReplayEnd: (() -> Unit)? = null
private var accumulatedDistance = 0f
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
isClickable = true
}
/** 设置笔刷图片 */
fun setBrushBitmap(bitmap: Bitmap) {
originalBrushBitmap?.takeIf { !it.isRecycled }?.recycle()
brushBitmap?.takeIf { !it.isRecycled }?.recycle()
originalBrushBitmap = bitmap.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, true)
resizeBrush()
}
/** 调整笔刷大小 */
private fun resizeBrush() {
val src = originalBrushBitmap ?: return
brushBitmap?.takeIf { !it.isRecycled }?.recycle()
brushBitmap = Bitmap.createScaledBitmap(src, brushSizePx, brushSizePx, true)
invalidate()
}
/** ========================
* 用户手绘
* ======================== */
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!enableDrawing || isReplaying || isFlashing) return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
currentStroke = mutableListOf()
strokes.add(currentStroke!!)
addPoint(event.x, event.y)
accumulatedDistance = 0f
}
MotionEvent.ACTION_MOVE -> {
addInterpolatedPoints(event.x, event.y)
}
MotionEvent.ACTION_UP -> {}
}
return true
}
/** 插值补点保证均匀间隔 */
private fun addInterpolatedPoints(x: Float, y: Float) {
val last = currentStroke?.lastOrNull() ?: run {
addPoint(x, y)
return
}
var dx = x - last.x
var dy = y - last.y
var dist = hypot(dx, dy)
if (dist < 0.1f) return
var offsetX = last.x
var offsetY = last.y
var lastPoint = last
accumulatedDistance += dist
while (accumulatedDistance >= stampDistancePx) {
val ratio = stampDistancePx / dist
offsetX = lastPoint.x + dx * ratio
offsetY = lastPoint.y + dy * ratio
val newPoint = StrokePoint(offsetX, offsetY)
addPoint(newPoint.x, newPoint.y)
lastPoint = newPoint
dx = x - lastPoint.x
dy = y - lastPoint.y
dist = hypot(dx, dy)
accumulatedDistance -= stampDistancePx
}
}
private fun addPoint(x: Float, y: Float) {
currentStroke?.add(StrokePoint(x, y))
invalidate()
}
/** ========================
* 绘制
* ======================== */
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val toDraw = if (isReplaying) replayTempStrokes else strokes
toDraw?.forEach { drawStroke(canvas, it) }
}
private fun drawStroke(canvas: Canvas, stroke: List<StrokePoint>) {
val brush = brushBitmap ?: return
val half = brushSizePx / 2f
if (stroke.isEmpty()) return
val oldAlpha = paint.alpha
if (isFlashing) paint.alpha = flashAlpha
for (p in stroke) {
canvas.drawBitmap(brush, p.x - half, p.y - half, paint)
}
paint.alpha = oldAlpha
}
/** ========================
* 导出 / 导入
* ======================== */
fun exportStrokes(): MutableList<MutableList<StrokePoint>> =
strokes.map { it.toMutableList() }.toMutableList()
fun importStrokes(list: MutableList<MutableList<StrokePoint>>, autoReplay: Boolean = false, speed: Long = mSpeed) {
strokes.clear()
list.forEach { strokes.add(it.toMutableList()) }
invalidate()
if (autoReplay) replay(speed)
}
/** ========================
* 撤回 / 清空
* ======================== */
fun undo() {
if (strokes.isNotEmpty()) {
strokes.removeAt(strokes.size - 1)
invalidate()
}
}
fun clearAll() {
strokes.clear()
currentStroke = null
invalidate()
}
/** ========================
* 回放动画
* ======================== */
fun replay(speed: Long = mSpeed) {
if (strokes.isEmpty()) { onReplayEnd?.invoke(); return }
replayAnimator?.cancel()
replayAnimator = null
isReplaying = true
val full = strokes.map { it.toMutableList() }.toMutableList()
replayTempStrokes = mutableListOf()
val totalPoints = full.sumOf { it.size }
replayAnimator = ValueAnimator.ofInt(0, totalPoints - 1).apply {
duration = totalPoints * speed
interpolator = LinearInterpolator()
addUpdateListener { animator ->
val index = animator.animatedValue as Int
updateReplay(full, index)
}
doOnEnd {
isReplaying = false
replayTempStrokes = null
startFlash() // 回放结束后闪烁
}
start()
}
}
private fun updateReplay(full: MutableList<MutableList<StrokePoint>>, index: Int) {
var count = 0
replayTempStrokes?.clear()
for (stroke in full) {
val newStroke = mutableListOf<StrokePoint>()
for (p in stroke) {
if (count <= index) newStroke.add(p)
count++
}
if (newStroke.isNotEmpty()) replayTempStrokes?.add(newStroke)
}
invalidate()
}
/** ========================
* 闪烁效果
* ======================== */
private fun startFlash(times: Int = 2, duration: Long = 500L) {
if (strokes.isEmpty()) return
blinkingAnimator?.cancel()
blinkingAnimator = null
isFlashing = true
// 每次闪烁包含一次亮->暗->亮
val values = mutableListOf<Int>()
for (i in 0 until times) {
values.addAll(listOf(255, 0))
}
values.add(255) // 最后回到全亮
blinkingAnimator = ValueAnimator.ofInt(*values.toIntArray()).apply {
this.duration = duration * values.size / 2 // 每个过渡时间为 duration
interpolator = LinearInterpolator()
addUpdateListener {
flashAlpha = it.animatedValue as Int
invalidate()
}
doOnEnd {
isFlashing = false
flashAlpha = 255
invalidate()
onReplayEnd?.invoke()
}
start()
}
}
private fun dp2px(dp: Float): Float = dp * resources.displayMetrics.density
/** ========================
* 内存清理
* ======================== */
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
release()
}
fun release() {
// 取消动画
replayAnimator?.cancel()
blinkingAnimator?.cancel()
replayAnimator = null
blinkingAnimator = null
// 回收 Bitmap
brushBitmap?.takeIf { !it.isRecycled }?.recycle()
brushBitmap = null
originalBrushBitmap?.takeIf { !it.isRecycled }?.recycle()
originalBrushBitmap = null
// 清理临时数据
currentStroke = null
replayTempStrokes = null
strokes.clear()
}
}
data class StrokePoint(val x: Float, val y: Float)
使用
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.bg_tlp_game_queen)
binding.brushSignatureView.setBrushBitmap(bitmap)
binding.brushSignatureView.stampDistanceDp = 12f
binding.brushSignatureView.onReplayEnd = {
//播放完成回调
binding.brushSignatureView.release()
}
binding.tvSave.setOnClickListener {
//保存轨迹
list = binding.brushSignatureView.exportStrokes()
}
binding.tCancel.setOnClickListener {
//撤销
binding.brushSignatureView.undo()
}
binding.tvClear.setOnClickListener {
//清空
binding.brushSignatureView.enableDrawing = true
binding.brushSignatureView.clearAll()
}
binding.tvPlay.setOnClickListener {
//播放轨迹
if (!list.isNullOrEmpty()){
binding.brushSignatureView.enableDrawing = false
binding.brushSignatureView.importStrokes(list!!, true, 30)
}
}
注意只有在onDetachedFromWindow里面做了资源释放,具体请根据自己的实际情况调用 release()
最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。