Android 红包雨效果自定义控件

作者: lucasDev | 来源:发表于2021-01-16 10:49 被阅读0次
    WX20201231-181616@2x.png

    思路:利用Path绘制动画轨迹,再使用PathMeasure获取轨迹中的坐标位置实时改变view的坐标完成红包动画。

    封装一个红包容器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 飘落物件控件
     *              规则:通过适配器实现
     */
    class FallingView : FrameLayout, Runnable {
        private val TAG = FallingView::class.java.simpleName
        private var handlerTask = Handler()
        private var iFallingAdapter: IFallingAdapter<*>? = null
        private var position = 0//当前item
        private var fallingListener: OnFallingListener? = null
        private var lastStartTime = 0L//最后一个item开始显示的延迟时间
    
        private val cacheHolder = HashSet<Holder>()//缓存holder,用于复用,减少item view创建的个数
    
        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加入缓存以复用
                    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后准备显示下一个item
            handlerTask.postDelayed(this, holder.config.startTime - lastStartTime)
            lastStartTime = holder.config.startTime
            position++
        }
    
        //设置适配器
        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
    
            //复用
            abstract fun convert(parent: ViewGroup, holder: Holder)
    
            //创建动画轨迹
            abstract fun convertAnim(parent: ViewGroup, holder: Holder): Animation
    
        }
    
        //初始化配置
        class Config {
            var startTime = 0L//开始发射时间
            var anim: Animation? = null
            var path: Path? = null
        }
    
        fun setOnFallingListener(onFallingListener: OnFallingListener) {
            fallingListener = onFallingListener
        }
    
        interface OnFallingListener {
            fun onStart()
    
            fun onStop()
        }
    
    }
    

    单个红包view动画轨迹设置

    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的样式、轨迹路线、动画属性、数据

    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//一屏显示物件的个数
    
        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轴摆动范围
                //限制动画区间使物件分布均匀
                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 {//点中红包回调
    //            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
            //旋转方向
            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
        }
    
        //区间
        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>
    

    在界面中定义适配器,添加红包数据

    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)部分

    相关文章

      网友评论

        本文标题:Android 红包雨效果自定义控件

        本文链接:https://www.haomeiwen.com/subject/mpjroktx.html