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今日头条点赞动画内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android实现点赞动画(27)

    本文实例为大家分享了Android使用入门第二十七篇点赞动画的具体代码,供大家参考,具体内容如下 MainActivity.java代码: package siso.likeanimation; import android.graphics.Bitmap; import android.graphics.PointF; import android.graphics.drawable.BitmapDrawable; import android.support.v4.content.res.R

  • Android控件实现直播App特效之点赞飘心动画

    现在市面上直播类的应用可以说是一抓一大把,随随便便就以什么主题来开发个直播App,说白了就想在这领域分杯羹.在使用这些应用过程中其实不难发现,在所有的直播界面,少不了的就是各种打赏.各种点赞.今天自己就针对点赞功能敲了一下,代码不多,主要是涉及到动画运动轨迹运算,这里需借助 贝塞尔曲线 相关知识,我使用三阶贝塞尔曲线来实现轨迹动画. 运行效果 一.具体实现流程 仔细分析整个点赞过程可以发现,首先是"爱心"的出现动画,然后是"爱心"以类似气泡的形式向上运动. &quo

  • Android高级UI特效仿直播点赞动画效果

    本文给大家分享高级UI特效仿直播点赞效果-一个优美炫酷的点赞动画,具体实现代码大家参考本文. 效果图如下: 攻克难点: 心形图片的路径等走向 心形图片的控制范围 部分代码如下: 通过AbstractPathAnimator定义飘心动画控制器 @Override public void start(final View child, final ViewGroup parent) { parent.addView(child, new ViewGroup.LayoutParams(mConfig.

  • Android控件FlowLikeView实现点赞动画

    现在市面上直播类的应用可以说是一抓一大把,随随便便就以什么主题来开发个直播App,说白了就想在这领域分杯羹.在使用这些应用过程中其实不难发现,在所有的直播界面,少不了的就是各种打赏.各种点赞.今天自己就针对点赞功能敲了一下,代码不多,主要是涉及到动画运动轨迹运算,这里需借助 贝塞尔曲线 相关知识,我使用三阶贝塞尔曲线来实现轨迹动画. 运行效果 一.具体实现流程 仔细分析整个点赞过程可以发现,首先是"爱心"的出现动画,然后是"爱心"以类似气泡的形式向上运动. &quo

  • android实现直播点赞飘心动画效果

    前段时间在写直播的时候,需要观众在看直播的时候点赞的效果,在此参照了腾讯大神写的点赞(飘心动画效果).下面是效果图: 1.自定义飘心动画的属性 在attrs.xml 中增加自定义的属性 <!-- 飘心动画自定义的属性 --> <declare-styleable name="HeartLayout"> <attr name="initX" format="dimension"/> <attr name=&

  • Android实现简单点赞动画

    思路 找到Activity中DecorView的RootView 确定点赞控件位于屏幕中的坐标值 将点赞效果View加入到RootView中, 给效果View添加自己想要的动画效果. 重复点击时候, 需要将效果View先移除掉再重新加入到RootView中. 代码 /**  * 普通点赞效果, 点击控件后出现一个View上浮  */ public class ViewLikeUtils {     public interface ViewLikeClickListener {        

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

    目录 一.前言 二.需求拆分 三.实现方案 1.点赞控件触摸事件处理 2.点赞动画的实现 2.1.点赞效果图片的获取和存储管理 2.2.点赞表情图标动画实现 2.3.点赞次数和点赞文案的绘制 3.存放点赞动画的容器 4.启动动画 四.遇到的问题 五.实现效果 六.完整代码获取 七.参考和感谢 总结 一.前言 我们在今日头条APP上会看到点赞动画效果,感觉非常不错,正好公司有点赞动画的需求,所以有了接下来的对此功能的实现的探索. 二.需求拆分 仔细观察点赞交互,看出大概以下几个步骤: 1:点赞控件

  • Android使用ListView实现滚轮的动画效果实例

    之前收到一个需求,需要把一个数据展示列表页面做成像滚轮那样的动画效果:中间最大然后向上下两端逐渐缩小.我想了想iOS那边自带滚轮组件,安卓得自己去实现,目前网上仿ios的滚轮组件的也有一些,但是感觉不适合我,我的要求没那么复杂,于是决定自己动手去实现一下. 动手前先分析一下应该怎么做,归根到底只是要实现缩放效果,由中间向两边变小,当一个item越接近中间就放大,越远离中间就缩小.那么可以通过先获取ListView的中点,然后获取当前可视的所有item跟ListView的中点的垂直距离计算出一个比

  • Android直播app送礼物连击动画效果(实例代码)

    最近在做公司的直播项目,需要实现一个观看端连击送礼物的控件: 直接上代码: /** * @author yangyinglong on 2017/7/11 16:52. * @Description: todo(这里用一句话描述这个类的作用) * @Copyright Copyright (c) 2017 Tuandai Inc. All Rights Reserved. */ public class CustomGiftView extends LinearLayout { private

  • Android 仿今日头条简单的刷新效果实例代码

    点击按钮,先自动进行下拉刷新,也可以手动刷新,刷新完后,最后就多一行数据.有四个选项卡. 前两天导师要求做一个给本科学生预定机房座位的app,出发点来自这里.做着做着遇到很多问题,都解决了.这个效果感觉还不错,整理一下. MainActivity package com.example.fragmentmytest; import android.content.DialogInterface; import android.graphics.Color; import android.os.B

  • Android 仿今日头条评论时键盘自动弹出的效果(推荐)

    Android 仿今日头条评论时键盘自动弹出的效果:当点击评论时,弹出对话框,同时弹出软键盘,当点击返回键时,将对话框关闭,不只是关闭软键盘. 效果图: 对这个对话框设置一个style效果: <style name="inputDialog" parent="@android:style/Theme.Holo.Light.Dialog"> <item name="android:windowBackground">@col

  • Android实现今日头条订阅频道效果

    本文实例为大家分享了Android仿今日头条订阅频道,供大家参考,具体内容如下 源码:Android实现今日头条订阅频道 布局文件 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.andro

  • Android 仿余额宝数字跳动动画效果完整代码

    一:想都不用想的,有图有真相,看着爽了,在看下面源码 二:实例源码分析 ①:首先定义接口 package com.demo.tools.view; /** * 数字动画自定义 * * @author zengtao 2015年7月17日 上午11:48:27 * */ public interface RiseNumberBase { public void start(); public RiseNumberTextView withNumber(float number); public R

  • Android仿打开微信红包动画效果实现代码

    首先看下效果: 实现原理: 准备3张不同角度的图片,通过AnimationDrawable帧动画进行播放即可 代码实现: 1.编写动画xml文件: <?xml version="1.0" encoding="utf-8"?> <animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false&

  • 利用Android实现一种点赞动画效果的全过程

    目录 前言 点击后的缩放效果 拇指的散开效果 示例 总结 前言 最近有个需求,需要仿照公司的H5实现一个游戏助手,其中一个点赞的按钮有动画效果,如下图: 分析一下这个动画,点击按钮后,拇指首先有个缩放的效果,然后有5个拇指朝不同的方向移动,其中部分有放大的效果. 点击后的缩放效果 本文通过ScaleAnimation 实现缩放效果,代码如下: private fun playThumbUpScaleAnimator() { // x.y轴方向都从1倍放大到2倍,以控件的中心为原点进行缩放 Sca

随机推荐