广告
返回顶部
首页 > 资讯 > 移动开发 >Android实现仿今日头条点赞动画效果实例
  • 349
分享到

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

2024-04-02 19:04:59 349人浏览 薄情痞子
摘要

目录一、前言二、需求拆分三、实现方案1、点赞控件触摸事件处理2、点赞动画的实现2.1、点赞效果图片的获取和存储管理2.2、点赞表情图标动画实现2.3、点赞次数和点赞文案的绘制3、存放

一、前言

我们在今日头条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

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

五、实现效果

六、完整代码获取

点击获取源码

七、参考和感谢

再次感谢

1、SuperLike

2、toutiaothumb

总结

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

--结束END--

本文标题: Android实现仿今日头条点赞动画效果实例

本文链接: https://www.lsjlt.com/news/138182.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Android实现仿今日头条点赞动画效果实例
    目录一、前言二、需求拆分三、实现方案1、点赞控件触摸事件处理2、点赞动画的实现2.1、点赞效果图片的获取和存储管理2.2、点赞表情图标动画实现2.3、点赞次数和点赞文案的绘制3、存放...
    99+
    2022-11-13
  • Android 仿今日头条简单的刷新效果实例代码
    点击按钮,先自动进行下拉刷新,也可以手动刷新,刷新完后,最后就多一行数据。有四个选项卡。 前两天导师要求做一个给本科学生预定机房座位的app,出发点来自这里。做着做着遇到很多...
    99+
    2022-06-06
    Android
  • Android仿今日头条APP实现下拉导航选择菜单效果
    本文实例为大家分享了在Android中如何实现下拉导航选择菜单效果的全过程,供大家参考,具体内容如下 关于下拉导航选择菜单效果在新闻客户端中用的比较多,当然也可以用在其他的项目...
    99+
    2022-06-06
    菜单 选择 app Android
  • Android怎么实现点赞动画效果
    今天小编给大家分享一下Android怎么实现点赞动画效果的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。一、前言对接下来功能实...
    99+
    2023-06-29
  • Android Studio初学者实例:RecyclerView学习--模仿今日头条
    本案例来自于学校的一个简单的课程实验 先看效果图,可以显然的看到,一些item是不同的布局,而其他布局就是简单的布局嵌套 看一下xml代码: ...
    99+
    2023-10-21
    android studio 学习 android
  • Android仿今日头条多个fragment懒加载的实现
    前言最近有时间,所以我又双叒叕推新一篇文章了,fragment懒加载实现虽然是个小模块,但做过的人都有体会,通常并不会轻易就成功了的,让你辗转反侧,彻夜难眠,绵绵无绝期。我就按照今日头条的样式做了一个懒加载功能。文章到一半会解释大家可能遇到...
    99+
    2023-05-30
  • Android如何仿今日头条评论时键盘自动弹出的效果
    这篇文章主要介绍Android如何仿今日头条评论时键盘自动弹出的效果,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!效果图:对这个对话框设置一个style效果:<style name="inp...
    99+
    2023-05-31
    android
  • Vue transition实现点赞动画效果的示例
    目录效果一览爱心效果 数字滚动动画 点赞动画 效果一览 爱心效果 材料:爱心图标两个,没有我这种 icon 组件的用 png 图片代替 <transition :nam...
    99+
    2022-11-12
  • iOS实现抖音点赞动画效果
    本文实例为大家分享了iOS实现抖音点赞动画的具体代码,供大家参考,具体内容如下 1. 概述 最近看到抖音点赞爱心的动画效果比较好,出于好奇,自己也研究仿照动画效果写了一个,不喜欢的朋...
    99+
    2022-05-31
    iOS 抖音 点赞
  • 使用Vue transition 实现点赞动画效果
    要实现点赞动画效果,你可以使用Vue的过渡(transition)组件。下面是一个简单的示例代码:```html<templa...
    99+
    2023-09-21
    Vue
  • 通过JetpackCompose实现双击点赞动画效果
    目录实现步骤先红色画个爱心点击事件加动画完整代码效果图实现步骤 先红色画个爱心 Icon( Icons.Filled.Favorite, "爱心", Modi...
    99+
    2022-11-13
  • 利用Android实现一种点赞动画效果的全过程
    目录前言点击后的缩放效果拇指的散开效果示例总结前言 最近有个需求,需要仿照公司的H5实现一个游戏助手,其中一个点赞的按钮有动画效果,如下图: 分析一下这个动画,点击按钮后,拇指首先...
    99+
    2022-12-08
    android 点赞动画 android点赞效果 android点击动画
  • 微信小程序如何实现今日头条导航栏滚动效果
    这篇文章给大家分享的是有关微信小程序如何实现今日头条导航栏滚动效果的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。项目需要,做一个和今日头条一样的导航栏,可以横行滚动,幸好再weu...
    99+
    2022-10-19
  • Android仿淘宝头条基于TextView实现上下滚动通知效果
    最近有个项目需要实现通知栏的上下滚动效果,仿淘宝头条的那种。我从网上看了一些代码,把完整的效果做了出来。如图所示:具体代码片段如下:1.在res文件夹下新建anmin文件夹,在这个文件夹里创建两个文件(1).anim_marquee_in....
    99+
    2023-05-31
    android textview 滚动
  • Android Flutter怎么实现仿闲鱼动画效果
    这篇文章主要讲解了“Android Flutter怎么实现仿闲鱼动画效果”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Android Flutter怎么实现仿闲鱼动画效果...
    99+
    2023-07-05
  • 如何通过Jetpack Compose实现双击点赞动画效果
    这篇文章主要介绍如何通过Jetpack Compose实现双击点赞动画效果,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!实现步骤先红色画个爱心Icon(    Ico...
    99+
    2023-06-28
  • Android实现仿iOS菊花加载圈动画效果
    目录常见的实现方式效果图:完整代码布局代码 常见的实现方式 切图,做旋转动画 自定义View,绘制效果 gif图 1、切图会增加体积,但相对简单,不过在换...
    99+
    2022-11-12
  • Android实现仿微软系统加载动画效果
    目录效果图:实现步骤:具体代码实现:1、创建Circle对象2、自定义MinSoftLoadingView实现代码3、布局文件中使用效果图: 实现步骤: 初始化五个圆球分...
    99+
    2022-11-12
  • Android编程实现仿心跳动画效果的方法
    本文实例讲述了Android编程实现仿心跳动画效果的方法。分享给大家供大家参考,具体如下: // 按钮模拟心脏跳动 private void playHeartbeatAni...
    99+
    2022-06-06
    方法 动画 Android
  • Android实现音频条形图效果(仿音频动画无监听音频输入)
    音频条形图 如下图所示就是这次的音频条形图: 由于只是自定义View的用法,我们就不去真实地监听音频输入了,随机模拟一些数字即可。 如果要实现一个如上图的静态音频条形图,相信...
    99+
    2022-06-06
    输入 监听 条形图 动画 Android
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作