data class AnimationItem( val pathMeasure: PathMeasure, val animator: ValueAnimator, var currentX: Float, var currentY: Float, var currentAlpha: Int, var totalDistance: Float, val bitmap: Bitmap )
class RandomCurveView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) {
private val IMAGE_IDS = listOf( R.drawable.cat1, R.drawable.cat2, R.drawable.cat3, R.drawable.cat4, R.drawable.cat5, R.drawable.cat6, R.drawable.cat7, R.drawable.cat8, R.drawable.cat9, R.drawable.cat10, R.drawable.cat11, R.drawable.cat12, R.drawable.cat13, R.drawable.cat14, R.drawable.cat15 )
private val activeItems = CopyOnWriteArrayList<AnimationItem>()
private val ANIMATION_DURATION_MS = 800L private val TRAVEL_DISTANCE_DP = 100f private val RANDOM_OFFSET_DP = 30f
private val itemPaint = Paint(Paint.ANTI_ALIAS_FLAG) private lateinit var imageMap: Map<Int, Bitmap> private var itemSize: Int = 0
private val travelDistancePx: Float private val randomOffsetPx: Float
init { isClickable = true
travelDistancePx = context.dpToPx(TRAVEL_DISTANCE_DP) randomOffsetPx = context.dpToPx(RANDOM_OFFSET_DP)
itemSize = context.dpToPx(24f).roundToInt() val targetSize = itemSize
val loadedImages = mutableMapOf<Int, Bitmap>()
IMAGE_IDS.forEach { id -> try { val resource = BitmapFactory.decodeResource(resources, id)
resource?.let { r -> loadedImages[id] = Bitmap.createScaledBitmap(r, targetSize, targetSize, true) } } catch (e: Exception) { Log.e( "RandomCurveView", "Resource loading failed for ID: $id. Please check if this R.drawable.xxx exists.", e ) } }
imageMap = loadedImages.toMap()
if (imageMap.isEmpty()) { throw IllegalStateException("RandomCurveView failed to initialize. No valid images were loaded from IMAGE_IDS list. Please update IMAGE_IDS with at least one existing R.drawable.xxx resource.") } }
override fun onDraw(canvas: Canvas) { super.onDraw(canvas)
val halfItemSize = itemSize / 2f
activeItems.forEach { item -> itemPaint.alpha = item.currentAlpha
canvas.drawBitmap( item.bitmap, item.currentX - halfItemSize, item.currentY - halfItemSize, itemPaint ) } }
override fun performClick(): Boolean { startCurveAnimation() startCurveAnimation() startCurveAnimation() return super.performClick() }
private fun startCurveAnimation() { if (width == 0 || height == 0 || imageMap.isEmpty()) return
val startPoint = PointF(width / 2f, height / 2f)
val randomBitmap = imageMap.values.random()
val randomAngle = Random.nextFloat() * 360f val endPoint = calculateEndPoint(startPoint, travelDistancePx, randomAngle)
val pMid1 = lerp(startPoint, endPoint, 0.33f) val pMid2 = lerp(startPoint, endPoint, 0.66f)
val controlPoint1 = offsetPointRandomly(pMid1, randomAngle, randomOffsetPx) val controlPoint2 = offsetPointRandomly(pMid2, randomAngle, randomOffsetPx)
val path = Path().apply { moveTo(startPoint.x, startPoint.y) cubicTo( controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endPoint.x, endPoint.y ) }
val pathMeasure = PathMeasure(path, false) val totalDistance = pathMeasure.length val pos = FloatArray(2) val item = AnimationItem( pathMeasure, ValueAnimator.ofFloat(0f, 1f), startPoint.x, startPoint.y, 255, totalDistance, randomBitmap ) activeItems.add(item)
item.animator.apply { duration = ANIMATION_DURATION_MS interpolator = LinearInterpolator()
addUpdateListener { animator -> val progress = animator.animatedValue as Float
pathMeasure.getPosTan(totalDistance * progress, pos, null) item.currentX = pos[0] item.currentY = pos[1]
item.currentAlpha = if (progress >= 0.8f) { val fadeProgress = (progress - 0.8f) / 0.2f (255 * (1f - fadeProgress)).roundToInt().coerceIn(0, 255) } else { 255 }
invalidate() }
addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animator: Animator) {} override fun onAnimationCancel(animator: Animator) {} override fun onAnimationRepeat(animator: Animator) {}
override fun onAnimationEnd(animator: Animator) { activeItems.remove(item) } }) start() } } }
|