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,用于复用,减少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 setAdapter(adapter: IFallingAdapter) {

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(@LayoutRes val layoutId: Int) {

var datas: List? = 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(R.layout.item_redpack) {

private val random = Random()

private val animDuration = 6000L//物件动画时长

private val count = 10//一屏显示物件的个数

private val animInterval = ArrayList()

fun setData(data: List) {

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

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">

android:id="@+id/v_falling"

android:layout_width="match_parent"

android:layout_height="match_parent" />

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

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()

}

}

android 红包雨源代码,Android 红包雨效果自定义控件相关推荐

  1. Android直播app源代码超简单气泡效果

    Android直播app源代码超简单气泡效果实现的相关代码 1.1 定义气泡 气泡效果我们关心的属性并不多,主要有这几种:半径.坐标.上升速度.水平平移速度.由于我们只在 View 内部使用,因此直接 ...

  2. android摇骰子源代码,Android实现微信摇骰子游戏

    过年怎么可以不玩红包娱乐一下呢,微信里的摇骰子大小和石头剪刀布就是不错的游戏方式. 使用Java的Random函数很容易实现,以骰子为例: 1.骰子摇动的动画,使用animation-list帧动画实 ...

  3. android 网络调试 源代码,Android源代码调试环境搭建

    我们在调试Android应用程序的时候,有时候遇到一些莫名其妙的问题,因此我们需要查看Android内部是如何调用的.我们都知道Android是一个伟大的开源项目,因此debug的时候肯定是支持源代码 ...

  4. Android 高仿 QQ5.0 侧滑菜单效果 自定义控件来袭

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39257409,本文出自[张鸿洋的博客] 上一篇博客带大家实现了:Android ...

  5. android一键拨号 源代码,android 使用意图(Intent)实现一键拨号实例

    本文打算实现具有一个一键拨号功能的 APP 1.布局文件activity_main.xml xmlns:tools="http://schemas.android.com/tools&quo ...

  6. android人脸识别源代码,Android自带的人脸识别

    1.Android自带的人脸识别Android自带的人脸识别只能识别出人脸在画面中的位置,中点,眼间距,角度等基本特性,提供给拍照性质的应用使用.从基本功能中不能得出明显的特征数据 2.底层库支持ex ...

  7. android北京地铁源代码,Android自定义View实现地铁显示牌效果

    本文实例为大家分享了Android地铁显示牌的具体代码,供大家参考,具体内容如下 预览效果 目录 SubwayBoardView.java 代码 public class SubwayBoardVie ...

  8. android英语字典(源代码),android英语字典(内含源码哦)

    英文词典是手机中经常使用的应用.因此,在本文将结合Android来讨论如何实现一个Android版的英文词典.实现英文词典的方法很多.在本文使用了SQLite数据库来保存英文单词信息.系统通过SQLi ...

  9. android拨号器源代码,android拨号器...

    写完这个拨号器,感慨良多.因刚入门android开发,写的过程,可谓九曲18弯,从搭建开发环境,到完成此拨号器,问题颇多 呵呵,写完这个拨号器,我对android的开发环境算是理解.入门了! ... ...

最新文章

  1. 关于 x-requested-with 请求头 区分ajax请求还是普通请求
  2. SYSCALL_DEFINE含义
  3. 合理的布局,绚丽的样式,谈谈Winform程序的界面设计
  4. Iphone开发之音频101(part 2):转换和录音
  5. 在QEMU硬件环境中启动 kernel 2.6 + busybox as rootfs
  6. 非计算机专业《Python程序设计基础》教学参考大纲
  7. java关闭applet_java – Applet会自动关闭
  8. Machine Learning ——Homework5
  9. AR8035 linux
  10. 路飞学城python开发ftp_路飞学城-Python开发集训-第一章
  11. IPv6 NDP——邻居发现协议
  12. Spring5基础知识
  13. 访问网站时,长时间打不开或无响应
  14. 彻底掌握 Promise-原生Promise的实现(二) Promise的链式调用
  15. 珞石经销商—珞石协作机器人xMate3的标定方法
  16. springboot+视频网站 毕业设计-附源码240925
  17. aiwi最新游戏:黑色洛城
  18. 爬取千库网ppt_初学Python-只需4步,爬取网站图片(附py文件)
  19. 迅为STM32MP157开发板入门教程之外设功能验证
  20. python排名分析_如何通过 Python 分析中国演员排名?

热门文章

  1. [L][ML]Adaboost
  2. 生信人实现“SCI自由”的秘密竟然是…
  3. 生信人的linux考试20题解析
  4. $‘\r‘: command not found的解决方法
  5. 微信小程序被判定诱导分享处理方案
  6. 九宫格的认识以及如何运用九宫格原理
  7. Android 超简单音乐播放器(十)歌词的实现
  8. Linux 之旅 5:磁盘与文件系统管理
  9. Python3解决问题:编写词云代码,总是报错
  10. TextView AutoLink, ClikSpan 与长按事件冲突的解决,Android面试真题解析火爆全网