1.效果图.
2.代码
1.动画视图view
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Path
import android.util.AttributeSet
import android.util.Log
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.util.Pools
/**
* Just :
* @author by Znan
* @date on 2020/12/12 15
*/
class PriseAnimView : FrameLayout {
private val TAG = "PriseAnimView"
//表情对象池
private val imageViewPool = Pools.SynchronizedPool<AppCompatImageView>(50);
private val emojiArray = arrayOf(
R.mipmap.ic_emoji_1,
R.mipmap.ic_emoji_2,
R.mipmap.ic_emoji_3,
R.mipmap.ic_emoji_4,
R.mipmap.ic_emoji_5
)
//阈值 影响动画宽度
private val minThresholdValue = 700
private val maxThresholdValue = 900
//每次點擊出現圖標隨機個數的最大值
private val MIN_COUNT = 3
private val MAX_COUNT = 6
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(
context,
attributeSet,
defStyleAttr
)
fun loadAnim(x: Float, y: Float) {
Log.w(TAG, "load anim x->" + x + " y->" + y)
imageViewPool.acquire()
val animatorSet = AnimatorSet()
repeat(getEmojiCount()) {
val thresholdValue = getRandomThresholdValue()
//二阶贝塞尔
val path = Path()
//起点坐标 阈值偏移10%的随机数
val startX =
((x - thresholdValue / 10).toInt()..(x + thresholdValue / 10).toInt()).random()
.toFloat()
val startY =
((y - thresholdValue / 10).toInt()..(y + thresholdValue / 10).toInt()).random()
.toFloat()
//终点坐标
val endX =
((x - thresholdValue).toInt()..(x + thresholdValue).toInt()).random().toFloat()
val endY = y + (thresholdValue * 0.8.toInt()..thresholdValue * 1.2.toInt()).random()
//控制点
val controlX = if (endX < x) {
(endX.toInt()..x.toInt()).random().toFloat()
} else {
(x.toInt()..endX.toInt()).random().toFloat()
}
val controlY = ((y - thresholdValue / 2).toInt()..y.toInt()).random().toFloat()
//路径
path.moveTo(x, y)
//path.moveTo(startX, startY)
path.quadTo(controlX, controlY, endX, endY)
//添加view 动画
val imageView = obtainImageView()
val res = getRandomEmoji()
imageView.setImageResource(res)
addView(imageView)
val alphaAnimator =
ObjectAnimator.ofFloat(
imageView,
"alpha",
1f,
0.9f,
0.8f,
0.7f,
0.6f,
0.4f,
0.2f,
0.0f
)
val translateAnimator = ObjectAnimator.ofFloat(imageView, "x", "y", path)
animatorSet.playTogether(translateAnimator, alphaAnimator)
animatorSet.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) {
}
override fun onAnimationEnd(animation: Animator?) {
recycleImageView(imageView)
}
override fun onAnimationCancel(animation: Animator?) {
}
override fun onAnimationRepeat(animation: Animator?) {
}
})
animatorSet.duration = 500
animatorSet.start()
}
}
fun removeAnimView() {
removeAllViews()
}
//获取一个随机阈值
private fun getRandomThresholdValue(): Int {
return (minThresholdValue..maxThresholdValue).random()
}
private fun getRandomEmoji(): Int {
return emojiArray[(emojiArray.indices).random()]
}
private fun getEmojiCount(): Int {
return (MIN_COUNT..MAX_COUNT).random()
}
/**
* 獲取imageView對象
*/
private fun obtainImageView(): AppCompatImageView {
val instance = imageViewPool.acquire()
return if (instance == null) {
val imageView = AppCompatImageView(context)
val layoutParams = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
imageView.layoutParams = layoutParams
imageView
} else {
instance
}
}
/**
* 释放imageView对象
*/
private fun recycleImageView(imageView: AppCompatImageView) {
removeView(imageView)
imageView.clearAnimation()
imageView.setImageResource(0)
imageViewPool.release(imageView)
}
}
2.布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_emoji_1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.znan.androidtest.PriseAnimView
android:id="@+id/view_prise_anim"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.触摸事件
屏幕坐标可以通过 getLocationOnScreen
//触摸事件 100ms延迟 iv_emoji.setOnTouchListener
RxView.touches(iv_emoji)
.throttleFirst(100, TimeUnit.MILLISECONDS)
.subscribe {
//执行动画
view_prise_anim.loadAnim(iv_emoji.x, iv_emoji.y)
when (it.action) {
MotionEvent.ACTION_DOWN -> {
logw("事件")
}
}
}
var pollDisposable :Disposable?= null
iv_emoji.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
pollingAnim(iv_emoji.x, iv_emoji.y)
logw("ACTION_DOWN")
}
MotionEvent.ACTION_UP -> {
logw("ACTION_UP")
pollDisposable?.dispose()
}
MotionEvent.ACTION_CANCEL-> {
logw("ACTION_UP")
pollDisposable?.dispose()
}
}
false
}
private fun pollingAnim(x:Float,y: Float) {
Observable.interval(0, 100, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object :Observer<Long>{
override fun onSubscribe(d: Disposable) {
pollDisposable = d
}
override fun onNext(t: Long) {
view_prise_anim.loadAnim(x,y)
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
}
})
}