欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Android实现仿今日头条点赞动画效果实例,

来源: 开发者 投稿于  被查看 33167 次 评论:187

Android实现仿今日头条点赞动画效果实例,


目录
  • 一、前言
  • 二、需求拆分
  • 三、实现方案
    • 1、点赞控件触摸事件处理
    • 2、点赞动画的实现
      • 2.1、点赞效果图片的获取和存储管理
      • 2.2、点赞表情图标动画实现
      • 2.3、点赞次数和点赞文案的绘制
    • 3、存放点赞动画的容器
      • 4、启动动画
      • 四、遇到的问题
        • 五、实现效果
          • 六、完整代码获取
            • 七、参考和感谢
              • 总结

                一、前言

                我们在今日头条APP上会看到点赞动画效果,感觉非常不错,正好公司有点赞动画的需求,所以有了接下来的对此功能的实现的探索。

                二、需求拆分

                仔细观察点赞交互,看出大概以下几个步骤:

                1:点赞控件需要自定义,对其触摸事件进行处理。

                2:点赞动画的实现。

                3:要有一个存放动画的容器。

                三、实现方案

                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
                
                        /**
                         * 获取数字图片
                         * @param number
                         * @return
                         */
                        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
                        }
                
                        /**
                         * 获取等级文案图片
                         * @param level
                         * @return
                         */
                        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
                        }
                
                        /**
                         * 获取随机表情图片
                         * @return
                         */
                        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
                        }
                
                        /**
                         * 设置表情图片
                         * @param drawableArray
                         * @return
                         */
                        fun setDrawableArray(@DrawableRes drawableArray: IntArray?): Builder {
                            this.drawableArray = drawableArray
                            return this
                        }
                
                        /**
                         * 设置数字图片
                         * @param numberDrawableArray
                         * @return
                         */
                        fun setNumberDrawableArray(@DrawableRes numberDrawableArray: IntArray): Builder {
                            this.numberDrawableArray = numberDrawableArray
                            return this
                        }
                
                        /**
                         * 设置等级文案图片
                         * @param levelDrawableArray
                         * @return
                         */
                        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
                
                        /**
                         * 获取数字图片
                         * [number] 点击次数
                         */
                        fun getNumberBitmap(number: Int): Bitmap?
                
                        /**
                         * 获取等级文案图片
                         * [level] 等级
                         */
                        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)
                        }
                
                    }
                
                    /**
                     * 这些get\set方法用于表情图标的大小动画
                     * 不能删除
                     */
                    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
                
                    /**
                     * x 初始位置
                     */
                    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()
                
                    }
                
                    /**
                     * 点击表情动画view
                     */
                    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
                }
                
                /**
                 * 如果父布局有SmartRefreshLayout 控件,设置控件是否可用
                 */
                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
                        }
                    }
                }

                五、实现效果

                六、完整代码获取

                点击获取源码

                七、参考和感谢

                再次感谢

                1、SuperLike

                2、toutiaothumb

                总结

                到此这篇关于Android实现仿今日头条点赞动画效果的文章就介绍到这了,更多相关Android今日头条点赞动画内容请搜索3672js教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持3672js教程!

                您可能感兴趣的文章:
                • Android高级UI特效仿直播点赞动画效果
                • android实现直播点赞飘心动画效果
                • Android控件实现直播App点赞飘心动画
                • Android实现点赞动画(27)
                • Android控件FlowLikeView实现点赞动画
                • Android实现简单点赞动画

                用户评论