Android--仿抖音刷新

4 篇文章 0 订阅
3 篇文章 0 订阅
本文详细介绍了如何实现类似抖音APP的刷新动画,通过观察动画效果,确定其涉及到平移和缩放动画,并使用Android的Drawable和AnimatorSet来创建这一动画效果。代码实现部分展示了关键的路径绘制和动画更新过程,最后给出了如何在项目中使用这个自定义动画。
摘要由CSDN通过智能技术生成

前言

抖音作为一个国民级的娱乐短视频应用,小编平常的时候也没少刷,往往一刷一关,半天就过去了。作为一个 Android 开发者,玩自家的应用没有玩抖音的时间长,感觉挺丧的。出于一种莫名其妙的心理,就想着从抖音那里偷学一些东西。但是从哪里开始那?想了想,还是先偷一个刷新动画吧。

观察&思考

先是觉得抖音的刷新动画挺好玩的,然后仔细观察了下。首先它有两个圆,一个红的,一个白的,感官上两个圆像是围着一个大圆在做圆周运动,忽左忽右,忽前忽后的,说明它用到了平移动画。然后两个球的大小也是在先小后大,说明它在做缩放动画。最后,两个球总是互相换来换去,说明它有一定的周期性,且相交的区域用黑色填充。好了,我们观察的情况大致符合咱们上面的描述。那么,你有实现它的思路了吗?不妨用笔画一画,写一写。

代码实现

// 假设有一个圆,沿着该圆心在水平方向上画一条线,然后以该线与圆的两个交点作为圆心画两圆,半径为圆的半径,
// 最后,两个圆沿着该线做平移和缩放动画,两圆相交的地方用黑色填充,直到两圆的圆心互换。
// 此时,走了一个周期,然后再沿着反方向走一遍。
// 注意: 圆心一直在切线上移动。
class DouYinRefresh : Drawable{

    private val redPaint = Paint()
    private val whitePaint = Paint()
    private val blackPaint = Paint()

    private val redPath = Path() // 红球轨迹
    private val whitePath = Path() // 白球轨迹
    private val blackPath = Path() // 中间相交的黑色轨迹

    // 假想圆的圆心和半径
    private var centerX: Float = 0f
    private var centerY: Float = 0f
    private var radius: Float = 30f

    private var offset: Float = 0f
    private var scale: Float = 0f

    // 记录两个圆交换次数
    private var cycleCount: Int = 0

    // 动画集合
    private val animatorSet: AnimatorSet = AnimatorSet()

    constructor() {
        redPaint.color = Color.RED
        redPaint.isAntiAlias = true
        redPaint.style = Paint.Style.FILL

        whitePaint.color = Color.WHITE
        whitePaint.isAntiAlias = true
        whitePaint.style = Paint.Style.FILL

        blackPaint.color = Color.BLACK
        blackPaint.isAntiAlias = true
        blackPaint.style = Paint.Style.FILL
    }

    override fun draw(canvas: Canvas) {
        // 每交换一次,两个圆移动的方向改变
        val redDirection = if (cycleCount and 1 == 0) 1 else -1
        val whiteDirection = if (cycleCount and 1 == 0) -1 else 1

        redPath.reset()
        redPath.addCircle(
            centerX - radius + redDirection * offset,
            centerY,
            radius,
            Path.Direction.CW
        )
        canvas.drawPath(redPath, redPaint)

        whitePath.reset()
        whitePath.addCircle(
            centerX + radius + whiteDirection * offset,
            centerY,
            radius * scale,
            Path.Direction.CW
        )
        canvas.drawPath(whitePath, whitePaint)

        blackPath.reset()
        blackPath.op(redPath, whitePath, Path.Op.INTERSECT)
        canvas.drawPath(blackPath, blackPaint)
    }

    override fun onBoundsChange(bounds: Rect) {
        super.onBoundsChange(bounds)
        centerX = bounds.width() / 2f
        centerY = bounds.height() / 2f
    }

    override fun setAlpha(alpha: Int) {
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
    }

    @SuppressLint("WrongConstant")
    override fun getOpacity(): Int {
        return PixelFormat.RGBA_8888
    }


    fun stopAnimation() {
        if (animatorSet.isRunning) {
            animatorSet.end()
        }
    }

    fun isRunning(): Boolean {
        return animatorSet.isRunning
    }

    fun startAnimation() {
        val translateAnimator = ValueAnimator.ofFloat(0f, 2 * radius)
        translateAnimator.repeatCount = -1
        translateAnimator.startDelay = 60
        translateAnimator.duration = 500
        translateAnimator.interpolator = AccelerateDecelerateInterpolator()
        translateAnimator.addUpdateListener {
            offset = it.animatedValue as Float
            invalidateSelf()
        }

        val scaleAnimator = ValueAnimator.ofFloat(1f, 0.5f, 1f)
        scaleAnimator.startDelay = 60
        scaleAnimator.repeatCount = -1
        scaleAnimator.duration = 500
        scaleAnimator.interpolator = AccelerateDecelerateInterpolator()
        scaleAnimator.addUpdateListener {
            scale = it.animatedValue as Float
            invalidateSelf()
        }

        translateAnimator.addListener(onEnd = {
            cycleCount++
            offset = 0f
        })
        animatorSet.playTogether(translateAnimator, scaleAnimator)
        animatorSet.start()
    }
}

思路想明白了,剩下的就交给系统提供的 API 去实现了。

使用

        val douyin = DouYinRefresh()
        findViewById<ImageView>(R.id.loading).setImageDrawable(douyin)
        douyin.startAnimation()

总结

当我们遇到比较新奇的东西且你有非常冲动的想把握住它,停下手中的事情,画10分钟,为自己加一个 todo 。按照事情的重要与紧急程度,重新分一下时间象限图。在之后的时间内,努力的把握住它。

Android 仿抖音APP下拉刷新功能,首先分析这个效果的实现思路,大致如下:   1、上拉时页面有翻页效果,可以用scrollview的pagingEnabled来实现,也就是说列表页不管你用tableview还是collectionview,只要每个cell是全屏的就可以。   2、下拉:当页面不是停留在第一个cell时,下拉就只是scrollView的滚动效果,不会触发刷新,当页面停留在第一个cell,也就是说scrollView.contentOffset.y = 0的时候,手指下拉才会触发刷新效果,并且下拉时scrollView不动,也就是没有scrollview的弹性效果,因此scrollView.bounces = NO。   3、既然下拉时scrollView不动,就不能使用代理来监听scrollView的滑动实现刷新,于是我想到了用touches的系列方法来监控手指下滑位移。   4、动画分解有五步:   (1)下拉时“推荐、附近”的那个导航条和“下拉刷新内容”的视图有渐隐渐显的效果,位置也随着手指下移,可以通过手指下滑位移计算alpha来实现   (2)下拉时,“下拉刷新内容”的视图右边那个有缺口的小圆环会随着手指滑动转圈,下滑时逆时针旋转   (3)下滑一定距离后如果不松手,又继续上滑,会执行前两步的反效果,圆环顺时针旋转,手指停在屏幕上,圆环就停止转动   (4)下滑到某个临界点,导航条和刷新视图都不再移动(此时导航条已经完全透明),所以可以通过计算起始点和当前点移动距离来计算透明度、位移、旋转角度,这些操作都在touchesMoved中实现   (5)到临界点松手后,导航条和刷新视图都回到原始位置,小圆环一直顺时针转圈,直到刷新结束,停止动画,隐藏刷新视图,显示导航条,如果没达到临界点就松手,不会触发刷新。。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值