今天小编给大家分享一下Android怎么实现点赞动画效果的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
一、前言
对接下来功能实现的探索。
二、需求拆分
仔细观察点赞交互,看出大概以下几个步骤:
点赞控件需要自定义,对其触摸事件进行处理。
点赞动画的实现。
要有一个存放动画的容器。
三、实现方案
1、点赞控件触摸事件处理
点赞控件是区分长按和点击处理的,另外我们发现在手指按下以后包括手指的移动直到手指的抬起都在执行动画。因为点赞的点击区域可能包括点赞次数,所以这里就自定义了点赞控件,并处理onTouchEvent(event: MotionEvent)
事件,区分长按和单击是使用了点击到手指抬起的间隔时间区分的,伪代码如下:
override fun onTouchEvent(event: MotionEvent): Boolean { var onTouch: Boolean when (event.action) { MotionEvent.ACTION_DOWN -> { isRefreshing = false isDowning = true //点击 lastDownTime = System.currentTimeMillis() postDelayed(autoPollTask, CLICK_INTERVAL_TIME) onTouch = true } MotionEvent.ACTION_UP -> { isDowning = false //抬起 if (System.currentTimeMillis() - lastDownTime < CLICK_INTERVAL_TIME) { //小于间隔时间按照单击处理 onFingerDowningListener?.onDown(this) } else { //大于等于间隔时间按照长按抬起手指处理 onFingerDowningListener?.onUp() } removeCallbacks(autoPollTask) onTouch = true } MotionEvent.ACTION_CANCEL ->{ isDowning = false removeCallbacks(autoPollTask) onTouch = false } else -> onTouch = false } return onTouch}
长按时使用Runnable的postDelayed(Runnable action, long delayMillis)
方法来进行不断的执行动画,伪代码:
private inner class AutoPollTask : Runnable { override fun run() { onFingerDowningListener?.onLongPress(this@LikeView) if(!canLongPress){ removeCallbacks(autoPollTask) }else{ postDelayed(autoPollTask, CLICK_INTERVAL_TIME) } }}
2、点赞动画的实现
点赞效果元素分为:点赞表情图标、点赞次数数字以及点赞文案
2.1、点赞效果图片的获取和存储管理
这里参考了SuperLike的做法,对图片进行了缓存处理,代码如下:
object BitmapProviderFactory { fun getProvider(context: Context): BitmapProvider.Provider { return BitmapProvider.Builder(context) .setDrawableArray( intArrayOf( R.mipmap.emoji_1, R.mipmap.emoji_2, R.mipmap.emoji_3, R.mipmap.emoji_4, R.mipmap.emoji_5, R.mipmap.emoji_6, R.mipmap.emoji_7, R.mipmap.emoji_8, R.mipmap.emoji_9, R.mipmap.emoji_10, R.mipmap.emoji_11, R.mipmap.emoji_12, R.mipmap.emoji_13, R.mipmap.emoji_14 ) ) .setNumberDrawableArray( intArrayOf( R.mipmap.multi_digg_num_0, R.mipmap.multi_digg_num_1, R.mipmap.multi_digg_num_2, R.mipmap.multi_digg_num_3, R.mipmap.multi_digg_num_4, R.mipmap.multi_digg_num_5, R.mipmap.multi_digg_num_6, R.mipmap.multi_digg_num_7, R.mipmap.multi_digg_num_8, R.mipmap.multi_digg_num_9 ) ) .setLevelDrawableArray( intArrayOf( R.mipmap.multi_digg_word_level_1, R.mipmap.multi_digg_word_level_2, R.mipmap.multi_digg_word_level_3 ) ) .build() }}
object BitmapProvider { class Default( private val context: Context, cacheSize: Int, @DrawableRes private val drawableArray: IntArray, @DrawableRes private val numberDrawableArray: IntArray?, @DrawableRes private val levelDrawableArray: IntArray?, private val levelStringArray: Array<String>?, private val textSize: Float ) : Provider { private val bitmapLruCache: LruCache<Int, Bitmap> = LruCache(cacheSize) private val NUMBER_PREFIX = 0x70000000 private val LEVEL_PREFIX = -0x80000000 override fun getNumberBitmap(number: Int): Bitmap? { var bitmap: Bitmap? if (numberDrawableArray != null && numberDrawableArray.isNotEmpty()) { val index = number % numberDrawableArray.size bitmap = bitmapLruCache[NUMBER_PREFIX or numberDrawableArray[index]] if (bitmap == null) { bitmap = BitmapFactory.decodeResource(context.resources, numberDrawableArray[index]) bitmapLruCache.put(NUMBER_PREFIX or numberDrawableArray[index], bitmap) } } else { bitmap = bitmapLruCache[NUMBER_PREFIX or number] if (bitmap == null) { bitmap = createBitmapByText(textSize, number.toString()) bitmapLruCache.put(NUMBER_PREFIX or number, bitmap) } } return bitmap } override fun getLevelBitmap(level: Int): Bitmap? { var bitmap: Bitmap? if (levelDrawableArray != null && levelDrawableArray.isNotEmpty()) { val index = level.coerceAtMost(levelDrawableArray.size) bitmap = bitmapLruCache[LEVEL_PREFIX or levelDrawableArray[index]] if (bitmap == null) { bitmap = BitmapFactory.decodeResource(context.resources, levelDrawableArray[index]) bitmapLruCache.put(LEVEL_PREFIX or levelDrawableArray[index], bitmap) } } else { bitmap = bitmapLruCache[LEVEL_PREFIX or level] if (bitmap == null && !levelStringArray.isNullOrEmpty()) { val index = level.coerceAtMost(levelStringArray.size) bitmap = createBitmapByText(textSize, levelStringArray[index]) bitmapLruCache.put(LEVEL_PREFIX or level, bitmap) } } return bitmap } override val randomBitmap: Bitmap get() { val index = (Math.random() * drawableArray.size).toInt() var bitmap = bitmapLruCache[drawableArray[index]] if (bitmap == null) { bitmap = BitmapFactory.decodeResource(context.resources, drawableArray[index]) bitmapLruCache.put(drawableArray[index], bitmap) } return bitmap } private fun createBitmapByText(textSize: Float, text: String): Bitmap { val textPaint = TextPaint() textPaint.color = Color.BLACK textPaint.textSize = textSize val bitmap = Bitmap.createBitmap( textPaint.measureText(text).toInt(), textSize.toInt(), Bitmap.Config.ARGB_4444 ) val canvas = Canvas(bitmap) canvas.drawColor(Color.TRANSPARENT) canvas.drawText(text, 0f, textSize, textPaint) return bitmap } } class Builder(var context: Context) { private var cacheSize = 0 @DrawableRes private var drawableArray: IntArray? = null @DrawableRes private var numberDrawableArray: IntArray? = null @DrawableRes private var levelDrawableArray: IntArray? = null private var levelStringArray: Array<String>? = null private var textSize = 0f fun setCacheSize(cacheSize: Int): Builder { this.cacheSize = cacheSize return this } fun setDrawableArray(@DrawableRes drawableArray: IntArray?): Builder { this.drawableArray = drawableArray return this } fun setNumberDrawableArray(@DrawableRes numberDrawableArray: IntArray): Builder { this.numberDrawableArray = numberDrawableArray return this } fun setLevelDrawableArray(@DrawableRes levelDrawableArray: IntArray?): Builder { this.levelDrawableArray = levelDrawableArray return this } fun setLevelStringArray(levelStringArray: Array<String>?): Builder { this.levelStringArray = levelStringArray return this } fun setTextSize(textSize: Float): Builder { this.textSize = textSize return this } fun build(): Provider { if (cacheSize == 0) { cacheSize = 32 } if (drawableArray == null || drawableArray?.isEmpty() == true) { drawableArray = intArrayOf(R.mipmap.emoji_1) } if (levelDrawableArray == null && levelStringArray.isNullOrEmpty()) { levelStringArray = arrayOf("次赞!", "太棒了!!", "超赞同!!!") } return Default( context, cacheSize, drawableArray!!, numberDrawableArray, levelDrawableArray, levelStringArray, textSize ) } } interface Provider { val randomBitmap: Bitmap fun getNumberBitmap(number: Int): Bitmap? fun getLevelBitmap(level: Int): Bitmap? }}
2.2、点赞表情图标动画实现
这里的实现参考了toutiaothumb,表情图标的动画大致分为:上升动画的同时执行图标大小变化动画和图标透明度变化,在上升动画完成时进行下降动画。代码如下:
class EmojiAnimationView @JvmOverloads constructor( context: Context, private val provider: BitmapProvider.Provider?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { private var mThumbImage: Bitmap? = null private var mBitmapPaint: Paint? = null private var mAnimatorListener: AnimatorListener? = null private var emojiWith = 0 private var emojiHeight = 0 private fun init() { //初始化图片,取出随机图标 mThumbImage = provider?.randomBitmap } init { //初始化paint mBitmapPaint = Paint() mBitmapPaint?.isAntiAlias = true } private fun showAnimation() { val imageWidth = mThumbImage?.width ?:0 val imageHeight = mThumbImage?.height ?:0 val topX = -1080 + (1400 * Math.random()).toFloat() val topY = -300 + (-700 * Math.random()).toFloat() //上升动画 val translateAnimationX = ObjectAnimator.ofFloat(this, "translationX", 0f, topX) translateAnimationX.duration = DURATION.toLong() translateAnimationX.interpolator = LinearInterpolator() val translateAnimationY = ObjectAnimator.ofFloat(this, "translationY", 0f, topY) translateAnimationY.duration = DURATION.toLong() translateAnimationY.interpolator = DecelerateInterpolator() //表情图片的大小变化 val translateAnimationRightLength = ObjectAnimator.ofInt( this, "emojiWith", 0,imageWidth,imageWidth,imageWidth,imageWidth, imageWidth, imageWidth, imageWidth, imageWidth, imageWidth ) translateAnimationRightLength.duration = DURATION.toLong() val translateAnimationBottomLength = ObjectAnimator.ofInt( this, "emojiHeight", 0,imageHeight,imageHeight,imageHeight,imageHeight,imageHeight, imageHeight, imageHeight, imageHeight, imageHeight ) translateAnimationBottomLength.duration = DURATION.toLong() translateAnimationRightLength.addUpdateListener { invalidate() } //透明度变化 val alphaAnimation = ObjectAnimator.ofFloat( this, "alpha", 0.8f, 1.0f, 1.0f, 1.0f, 0.9f, 0.8f, 0.8f, 0.7f, 0.6f, 0f ) alphaAnimation.duration = DURATION.toLong() //动画集合 val animatorSet = AnimatorSet() animatorSet.play(translateAnimationX).with(translateAnimationY) .with(translateAnimationRightLength).with(translateAnimationBottomLength) .with(alphaAnimation) //下降动画 val translateAnimationXDown = ObjectAnimator.ofFloat(this, "translationX", topX, topX * 1.2f) translateAnimationXDown.duration = (DURATION / 5).toLong() translateAnimationXDown.interpolator = LinearInterpolator() val translateAnimationYDown = ObjectAnimator.ofFloat(this, "translationY", topY, topY * 0.8f) translateAnimationYDown.duration = (DURATION / 5).toLong() translateAnimationYDown.interpolator = AccelerateInterpolator() //设置动画播放顺序 val animatorSetDown = AnimatorSet() animatorSet.start() animatorSet.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) {} override fun onAnimationEnd(animation: Animator) { animatorSetDown.play(translateAnimationXDown).with(translateAnimationYDown) animatorSetDown.start() } override fun onAnimationCancel(animation: Animator) {} override fun onAnimationRepeat(animation: Animator) {} }) animatorSetDown.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) {} override fun onAnimationEnd(animation: Animator) { //动画完成后通知移除动画view mAnimatorListener?.onAnimationEmojiEnd() } override fun onAnimationCancel(animation: Animator) {} override fun onAnimationRepeat(animation: Animator) {} }) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) drawEmojiImage(canvas) } private fun drawEmojiImage(canvas: Canvas) { mThumbImage?.let{ val dst = Rect() dst.left = 0 dst.top = 0 dst.right = emojiWith dst.bottom = emojiHeight canvas.drawBitmap(it, null, dst, mBitmapPaint) } } fun getEmojiWith(): Int { return emojiWith } fun setEmojiWith(emojiWith: Int) { this.emojiWith = emojiWith } fun getEmojiHeight(): Int { return emojiHeight } fun setEmojiHeight(emojiHeight: Int) { this.emojiHeight = emojiHeight } fun setEmojiAnimation() { showAnimation() } fun setAnimatorListener(animatorListener: AnimatorListener?) { mAnimatorListener = animatorListener } interface AnimatorListener { fun onAnimationEmojiEnd() } fun setEmoji() { init() } companion object { //动画时长 const val DURATION = 500 }}
2.3、点赞次数和点赞文案的绘制
这里的点赞次数处理了从1到999,并在不同的点赞次数区间显示不同的点赞文案。代码如下:
class NumberLevelView @JvmOverloads constructor( context: Context, private val provider: BitmapProvider.Provider?, private val x: Int, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { private var textPaint: Paint = Paint() private var mNumber = 0 private var bitmapTalk: Bitmap? = null private var level = 0 private var numberImageWidth = 0 private var offsetX = 0 private var initialValue = 0 private var spacing = 0 init { textPaint.isAntiAlias = true initialValue = x - PublicMethod.dp2px(context, 120f) numberImageWidth = provider?.getNumberBitmap(1)?.width ?: 0 spacing = PublicMethod.dp2px(context, 10f) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val levelBitmap = provider?.getLevelBitmap(level) ?: return //等级图片的宽度 val levelBitmapWidth = levelBitmap.width val dst = Rect() when (mNumber) { in 0..9 -> { initialValue = x - levelBitmapWidth dst.left = initialValue dst.right = initialValue + levelBitmapWidth } in 10..99 -> { initialValue = x - PublicMethod.dp2px(context, 100f) dst.left = initialValue + numberImageWidth + spacing dst.right = initialValue+ numberImageWidth + spacing+ levelBitmapWidth } else -> { initialValue = x - PublicMethod.dp2px(context, 120f) dst.left = initialValue + 2*numberImageWidth + spacing dst.right = initialValue+ 2*numberImageWidth + spacing + levelBitmapWidth } } dst.top = 0 dst.bottom = levelBitmap.height //绘制等级文案图标 canvas.drawBitmap(levelBitmap, null, dst, textPaint) while (mNumber > 0) { val number = mNumber % 10 val bitmap = provider.getNumberBitmap(number)?:continue offsetX += bitmap.width //这里是数字 val rect = Rect() rect.top = 0 when { mNumber/ 10 < 1 -> { rect.left = initialValue - bitmap.width rect.right = initialValue } mNumber/ 10 in 1..9 -> { rect.left = initialValue rect.right = initialValue + bitmap.width } else -> { rect.left = initialValue + bitmap.width rect.right = initialValue +2* bitmap.width } } rect.bottom = bitmap.height //绘制数字 canvas.drawBitmap(bitmap, null, rect, textPaint) mNumber /= 10 } } fun setNumber(number: Int) { this.mNumber = number if (mNumber >999){ mNumber = 999 } level = when (mNumber) { in 1..20 -> { 0 } in 21..80 -> { 1 } else -> { 2 } } //根据等级取出等级文案图标 bitmapTalk = provider?.getLevelBitmap(level) invalidate() }}
3、存放点赞动画的容器
我们需要自定义一个view来存放动画,以及提供开始动画以及回收动画view等工作。代码如下:
class LikeAnimationLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { private var lastClickTime: Long = 0 private var currentNumber = 1 private var mNumberLevelView: NumberLevelView? = null private var hasEruptionAnimation = false private var hasTextAnimation = false private var canLongPress = false private var maxAngle = 0 private var minAngle = 0 private var pointX = 0 private var pointY = 0 var provider: BitmapProvider.Provider? = null get() { if (field == null) { field = BitmapProvider.Builder(context) .build() } return field } private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { val typedArray = context.obtainStyledAttributes( attrs, R.styleable.LikeAnimationLayout, defStyleAttr, 0 ) maxAngle = typedArray.getInteger(R.styleable.LikeAnimationLayout_max_angle, MAX_ANGLE) minAngle = typedArray.getInteger(R.styleable.LikeAnimationLayout_min_angle, MIN_ANGLE) hasEruptionAnimation = typedArray.getBoolean( R.styleable.LikeAnimationLayout_show_emoji, true ) hasTextAnimation = typedArray.getBoolean(R.styleable.LikeAnimationLayout_show_text, true) typedArray.recycle() } private fun addEmojiView( context: Context?, x: Int, y: Int ) { for (i in 0 .. ERUPTION_ELEMENT_AMOUNT) { val layoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) layoutParams.setMargins(x, y, 0, 0) val articleThumb = context?.let { EmojiAnimationView( it, provider ) } articleThumb?.let { it.setEmoji() this.addView(it, -1, layoutParams) it.setAnimatorListener(object : EmojiAnimationView.AnimatorListener { override fun onAnimationEmojiEnd() { removeView(it) val handler = Handler() handler.postDelayed({ if (mNumberLevelView != null && System.currentTimeMillis() - lastClickTime >= SPACING_TIME) { removeView(mNumberLevelView) mNumberLevelView = null } }, SPACING_TIME) } }) it.setEmojiAnimation() } } } fun launch(x: Int, y: Int) { if (System.currentTimeMillis() - lastClickTime >= SPACING_TIME) { pointX = x pointY = y //单次点击 addEmojiView(context, x, y-50) lastClickTime = System.currentTimeMillis() currentNumber = 1 if (mNumberLevelView != null) { removeView(mNumberLevelView) mNumberLevelView = null } } else { //连续点击 if (pointX != x || pointY != y){ return } lastClickTime = System.currentTimeMillis() Log.i(TAG, "当前动画化正在执行") addEmojiView(context, x, y) //添加数字连击view val layoutParams = RelativeLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) layoutParams.setMargins(0, y - PublicMethod.dp2px(context, 60f), 0, 0) if (mNumberLevelView == null) { mNumberLevelView = NumberLevelView(context,provider,x) addView(mNumberLevelView, layoutParams) } currentNumber++ mNumberLevelView?.setNumber(currentNumber) } } companion object { private const val TAG = "LikeAnimationLayout" private const val ERUPTION_ELEMENT_AMOUNT = 8 private const val MAX_ANGLE = 180 private const val MIN_ANGLE = 70 private const val SPACING_TIME = 400L } init { init(context, attrs, defStyleAttr) }}
注意:动画完成之后一定要清除view。
4、启动动画
点赞控件的手势回调,伪代码如下:
holder.likeView.setOnFingerDowningListener(object : OnFingerDowningListener { override fun onLongPress(v: View) { if (!bean.hasLike) { //未点赞 if (!fistLongPress) { //这里同步点赞接口等数据交互 bean.likeNumber++ bean.hasLike = true setLikeStatus(holder, bean) } //显示动画 onLikeAnimationListener?.doLikeAnimation(v) } else { if (System.currentTimeMillis() - lastClickTime <= throttleTime && lastClickTime != 0L) { //处理点击过后为点赞状态的情况 onLikeAnimationListener?.doLikeAnimation(v) lastClickTime = System.currentTimeMillis() } else { //处理长按为点赞状态后的情况 onLikeAnimationListener?.doLikeAnimation(v) } } fistLongPress = true } override fun onUp() { fistLongPress = false } override fun onDown(v: View) { if (System.currentTimeMillis() - lastClickTime > throttleTime || lastClickTime == 0L) { if (!bean.hasLike) { //未点赞情况下,点赞接口和数据交互处理 bean.hasLike = true bean.likeNumber++ setLikeStatus(holder, bean) throttleTime = 1000 onLikeAnimationListener?.doLikeAnimation(v) } else { //点赞状态下,取消点赞接口和数据交互处理 bean.hasLike = false bean.likeNumber-- setLikeStatus(holder, bean) throttleTime = 30 } } else if (lastClickTime != 0L && bean.hasLike) { //在时间范围内,连续点击点赞,显示动画 onLikeAnimationListener?.doLikeAnimation(v) } lastClickTime = System.currentTimeMillis() }})
在显示动画页面初始化工作时初始化动画资源:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_list) likeAnimationLayout?.provider = BitmapProviderFactory.getProvider(this)}
在显示动画的回调中启动动画:
override fun doLikeAnimation(v: View) { val itemPosition = IntArray(2) val superLikePosition = IntArray(2) v.getLocationOnScreen(itemPosition) likeAnimationLayout?.getLocationOnScreen(superLikePosition) val x = itemPosition[0] + v.width / 2 val y = itemPosition[1] - superLikePosition[1] + v.height / 2 likeAnimationLayout?.launch(x, y)}
四、遇到的问题
因为流列表中使用了SmartRefreshLayout下拉刷新控件,如果在列表前几条内容进行点赞动画当手指移动时触摸事件会被SmartRefreshLayout拦截去执行下拉刷新,那么手指抬起时点赞控件得不到响应会一直进行动画操作,目前想到的解决方案是点赞控件在手指按下时查看父布局有无SmartRefreshLayout,如果有通过反射先禁掉下拉刷新功能,手指抬起或者取消进行重置操作。代码如下:
override fun dispatchTouchEvent(event: MotionEvent?): Boolean { parent?.requestDisallowInterceptTouchEvent(true) return super.dispatchTouchEvent(event)}override fun onTouchEvent(event: MotionEvent): Boolean { var onTouch: Boolean when (event.action) { MotionEvent.ACTION_DOWN -> { isRefreshing = false isDowning = true //点击 lastDownTime = System.currentTimeMillis() findSmartRefreshLayout(false) if (isRefreshing) { //如果有下拉控件并且正在刷新直接不响应 return false } postDelayed(autoPollTask, CLICK_INTERVAL_TIME) onTouch = true } MotionEvent.ACTION_UP -> { isDowning = false //抬起 if (System.currentTimeMillis() - lastDownTime < CLICK_INTERVAL_TIME) { //小于间隔时间按照单击处理 onFingerDowningListener?.onDown(this) } else { //大于等于间隔时间按照长按抬起手指处理 onFingerDowningListener?.onUp() } findSmartRefreshLayout(true) removeCallbacks(autoPollTask) onTouch = true } MotionEvent.ACTION_CANCEL ->{ isDowning = false findSmartRefreshLayout(true) removeCallbacks(autoPollTask) onTouch = false } else -> onTouch = false } return onTouch}private fun findSmartRefreshLayout(enable: Boolean) { var parent = parent while (parent != null && parent !is ContentFrameLayout) { if (parent is SmartRefreshLayout) { isRefreshing = parent.state == RefreshState.Refreshing if (isRefreshing){ //如果有下拉控件并且正在刷新直接结束 break } if (!enable && firstClick){ try { firstClick = false val field: Field = parent.javaClass.getDeclaredField("mEnableRefresh") field.isAccessible = true //通过反射获取是否可以先下拉刷新的初始值 enableRefresh = field.getBoolean(parent) }catch (e: Exception) { e.printStackTrace() } } if (enableRefresh){ //如果初始值不可以下拉刷新不要设置下拉刷新状态 parent.setEnableRefresh(enable) } parent.setEnableLoadMore(enable) break } else { parent = parent.parent } }}
五、实现效果
以上就是“Android怎么实现点赞动画效果”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网行业资讯频道。