
WX20201231-181616@2x.png
思路:利用Path繪制動畫軌跡,再使用PathMeasure獲取軌跡中的坐標(biāo)位置實時改變view的坐標(biāo)完成紅包動畫。
封裝一個紅包容器view用于管理大量紅包view的顯示、動畫、消失、回收利用
package com.cj.customwidget.widget
import android.content.Context
import android.graphics.*
import android.os.Handler
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.widget.FrameLayout
import androidx.annotation.LayoutRes
import androidx.core.view.children
/**
* File FallingView.kt
* Date 12/25/20
* Author lucas
* Introduction 飄落物件控件
* 規(guī)則:通過適配器實現(xiàn)
*/
class FallingView : FrameLayout, Runnable {
private val TAG = FallingView::class.java.simpleName
private var handlerTask = Handler()
private var iFallingAdapter: IFallingAdapter<*>? = null
private var position = 0//當(dāng)前item
private var fallingListener: OnFallingListener? = null
private var lastStartTime = 0L//最后一個item開始顯示的延遲時間
private val cacheHolder = HashSet<Holder>()//緩存holder,用于復(fù)用,減少item view創(chuàng)建的個數(shù)
constructor(context: Context) : super(context) {
initView(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initView(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initView(context, attrs)
}
private fun initView(context: Context, attrs: AttributeSet?) {
// setWillNotDraw(false)//放開注釋可顯示輔助線
}
//開始飄落
fun startFalling() {
if (iFallingAdapter == null) {
Log.e(TAG, "iFallingAdapter not be null.")
return
}
position = 0
handlerTask.post(this)
}
//停止飄落
fun stopFalling() {
handlerTask.removeCallbacks(this)
//停止所有動畫
children.forEach {
it.clearAnimation()
}
removeAllViews()
}
override fun run() {
iFallingAdapter?.also { adapter ->
if (adapter.datas.isNullOrEmpty() || position > adapter.datas!!.size - 1) return
// "position:$position".p()
showItem(adapter)
invalidate()
}
}
private fun showItem(adapter: IFallingAdapter<*>) {
if (position == 0) {
fallingListener?.onStart()
}
var holder: Holder
if (cacheHolder.isEmpty()) {
val inflate = LayoutInflater.from(context).inflate(adapter.layoutId, this, false)
holder = Holder(inflate)
} else {//從緩存中獲取holder
val iterator = cacheHolder.iterator()
holder = iterator.next()
iterator.remove()
}
holder.position = position
addView(holder.view)
adapter.convert(this, holder)
holder.config.anim = adapter.convertAnim(this, holder)
holder.config.anim?.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
//將item加入緩存以復(fù)用
cacheHolder.add(holder)
removeView(holder.view)
if (childCount == 0 && adapter.datas?.size == position + 1) {
fallingListener?.onStop()
}
// "cacheHolder:${cacheHolder.size}".p()
}
override fun onAnimationStart(animation: Animation?) {
}
})
holder.view.startAnimation(holder.config.anim)
//顯示完一個item后準(zhǔn)備顯示下一個item
handlerTask.postDelayed(this, holder.config.startTime - lastStartTime)
lastStartTime = holder.config.startTime
position++
}
//設(shè)置適配器
fun <T> setAdapter(adapter: IFallingAdapter<T>) {
iFallingAdapter = adapter
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//輔助線
cacheHolder.forEach { enty ->
enty.config.path?.also { assistLine(it, canvas) }
}
}
private val paint = Paint().apply {
style = Paint.Style.STROKE
color = Color.RED
strokeWidth = 4f
}
//輔助線
private fun assistLine(path: Path, canvas: Canvas) {
canvas.drawPath(path, paint)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopFalling()
}
class Holder(val view: View) {
var config: Config = Config()
var position: Int = 0
}
//適配器
abstract class IFallingAdapter<T>(@LayoutRes val layoutId: Int) {
var datas: List<T>? = null
//復(fù)用
abstract fun convert(parent: ViewGroup, holder: Holder)
//創(chuàng)建動畫軌跡
abstract fun convertAnim(parent: ViewGroup, holder: Holder): Animation
}
//初始化配置
class Config {
var startTime = 0L//開始發(fā)射時間
var anim: Animation? = null
var path: Path? = null
}
fun setOnFallingListener(onFallingListener: OnFallingListener) {
fallingListener = onFallingListener
}
interface OnFallingListener {
fun onStart()
fun onStop()
}
}
單個紅包view動畫軌跡設(shè)置
package com.cj.customwidget.page.falling
import android.graphics.Path
import android.graphics.PathMeasure
import android.view.View
import android.view.animation.Animation
import android.view.animation.Transformation
import java.util.*
class RedPackAnim(val path: Path, val rotation: Float, val view: View) : Animation() {
val pathMeasure = PathMeasure(path, false)
val point = FloatArray(2)
val tan = FloatArray(2)
override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
pathMeasure.getPosTan(pathMeasure.length * interpolatedTime, point, tan)
view.x = point[0] - view.measuredWidth / 2
view.y = point[1]
view.rotation = rotation * interpolatedTime
// "point:${point.toList()}".p()
}
}
適配器:用于定義紅包view的樣式、軌跡路線、動畫屬性、數(shù)據(jù)
package com.cj.customwidget.page.falling
import android.graphics.Path
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.widget.ImageView
import com.cj.customwidget.R
import com.cj.customwidget.widget.FallingView
import java.util.*
import kotlin.collections.ArrayList
class FallingAdapter : FallingView.IFallingAdapter<Int>(R.layout.item_redpack) {
private val random = Random()
private val animDuration = 6000L//物件動畫時長
private val count = 10//一屏顯示物件的個數(shù)
private val animInterval = ArrayList<Interval>()
fun setData(data: List<Int>) {
datas = data
}
private fun createPath(parent: ViewGroup, position: Int, view: View): Path =
Path().apply {
view.measure(0, 0)
val width = parent.width - view.measuredWidth
val height = parent.height
val swing = width / 3f//x軸擺動范圍
//限制動畫區(qū)間使物件分布均勻
if (animInterval.isEmpty()) {
animInterval.add(Interval(view.measuredWidth / 2f, swing))
animInterval.add(Interval(swing, swing * 2))
animInterval.add(Interval(swing * 2, parent.width - view.measuredWidth / 2f))
}
// "animInterval:${animInterval.size}".p()
val interval: Interval
if (animInterval.size == 1) {
interval = animInterval[0]
} else {
interval = animInterval[random.nextInt(animInterval.size)]
}
animInterval.remove(interval)
val startPointX = random.nextInt(width).toFloat()
moveTo(startPointX, -view.measuredHeight.toFloat())
//控制點
var point1X = random.nextInt(interval.getLength().toInt()) + interval.start
val point1Y = random.nextInt(height / 2).toFloat()
var point2X = random.nextInt(interval.getLength().toInt()) +interval.start
val point2Y = random.nextInt(height / 2).toFloat() + height / 2
var point3X = random.nextInt(interval.getLength().toInt()) + interval.start
cubicTo(point1X, point1Y, point2X, point2Y, point3X, height.toFloat())
}
override fun convert(parent: ViewGroup, holder: FallingView.Holder) {
if (holder.position%20==0){
(holder.view as ImageView).setImageResource(R.mipmap.ic_readpack2)
}else{
(holder.view as ImageView).setImageResource(R.mipmap.ic_readpack)
}
holder.config.startTime = holder.position * (animDuration / count)
holder.view.setOnClickListener {//點中紅包回調(diào)
// holder.view.clearAnimation()
// holder.view.visibility = View.GONE
}
}
override fun convertAnim(parent: ViewGroup, holder: FallingView.Holder): Animation {
val path = createPath(parent, holder.position, holder.view)
holder.config.path = path
//旋轉(zhuǎn)方向
val rotation:Float
if (random.nextInt(2)==0){
rotation = 30f*random.nextFloat()
}else{
rotation = -30f*random.nextFloat()
}
val redPackAnim = RedPackAnim(path, rotation, holder.view)
//動畫時長-下落速度
redPackAnim.duration = (animDuration*(0.6+random.nextInt(4)*0.1)).toLong()
return redPackAnim
}
//區(qū)間
class Interval(val start: Float, val end: Float) {
fun getLength() = end - start
}
}
使用方式,在布局中添加view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
tools:context=".page.falling.FallingActivity">
<com.cj.customwidget.widget.FallingView
android:id="@+id/v_falling"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
在界面中定義適配器,添加紅包數(shù)據(jù)
package com.cj.customwidget.page.falling
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.animation.Animation
import android.view.animation.Transformation
import com.cj.customwidget.R
import com.cj.customwidget.ext.p
import kotlinx.android.synthetic.main.activity_falling.*
/**
* File FallingActivity.kt
* Date 12/25/20
* Author lucas
* Introduction 紅包雨
*/
class FallingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_falling)
v_falling.setAdapter(FallingAdapter().apply { setData(List(100){it}) })
v_falling.startFalling()
}
}
源碼地址:https://github.com/LucasDevelop/CustomView。中的(Falling)部分