效果如下

Android 头像轮博动画 头像轮换_kotlin


即以下5张图循环播放:

Android 头像轮博动画 头像轮换_android_02

实现思路

  仔细看动效会发现,同时出现的最多可有四张图。我们用一个RelativeLayout来盛放这四张图,分别放在最右边(第0张图)、中间(第1张图)和最左边(第2张图),而第3张图在动效开始时再添加在左边,并设置translationX = -scrollLength,使其只显示一部分在画板内。

  动效开始时,四张图同时调用translationXBy,向右滚动scrollLength距离,这时第3张图即可出现在画板内,第0张图滚出画板。第0、3张图可根据需要再设置滚出缩小逐渐透明或者放大逐渐清晰的动效。

  第0张图片再向右滚动一次就会完全移出屏幕,所以在滚出屏幕动效结束后,将这个ImageView从RelativeLayout移除并放进缓存池子(一个List列表)。第3张图片要移入屏幕出现时,复用缓存池子里的ImageView设置图片然后addView到RelativeLayout中显示并移动。

  一个ImageView从缓存中取出,然后在屏幕的位置由3→2→1→0后,再放入缓存等待取出,如此循环。

Android 头像轮博动画 头像轮换_Android 头像轮博动画_03

滚动距离计算:

scrollLength(滚动距离)在onSizeChanged方法中动态计算,公式为:(width - avatarSize) / 2F

Android 头像轮博动画 头像轮换_kotlin_04

代码实现

const val START_AVATAR_LOOP = 111

class LoopScrollAvatar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
    //动画播放时长
    private val animDuration = 700L

    //动画间隔播放时间
    private val animIntervalTime = 1000L

    //两边头像的缩放程度
    private val scaleFrom = 0.7F

    //头像大小
    private val avatarSize = SizeUtils.dp2px(60F)

    //从当前位置滚动到下一位置需要移动的距离
    private var scrollLength = 0F

    //下次要显示的图片角标
    private var index = 0
    
    private val res =
        arrayOf(
            R.drawable.avatar_1,
            R.drawable.avatar_2,
            R.drawable.avatar_3,
            R.drawable.avatar_4,
            R.drawable.avatar_5
        )

    //缓存复用ImageView
    private val ivCache = mutableListOf(
        createImageView(),
        createImageView(),
        createImageView(),
        createImageView()
    )
    private val handler by lazy {
        LoopHandler(this)
    }

    init {
        //前三位的头像先addView显示出来
        //放最右
        addImageView(ALIGN_PARENT_RIGHT)
        //放中间
        addImageView(CENTER_HORIZONTAL)
        //默认放左边
        addImageView()
    }

    class LoopHandler() : Handler() {
        private var lWeak: WeakReference<LoopScrollAvatar>? = null

        constructor(loopScrollAvatar: LoopScrollAvatar) : this() {
            lWeak = WeakReference(loopScrollAvatar)
        }

        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)

            lWeak?.get()?.apply {
                if ((context as? Activity)?.isDestroyed == false) {
                    startAnimMove()
                    sendLoopMsg()
                }
            }
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        scrollLength = (width - avatarSize) / 2F
    }

    /**
     * 创建圆形头像ImageView
     */
    private fun createImageView(): ImageView {
        return QMUIRadiusImageView(context).apply {
            //这里用了一个三方ImageView,可以设置圆角、border宽度和颜色
            isCircle = true
            borderWidth = SizeUtils.dp2px(1F)
            borderColor = Color.WHITE
        }
    }

    /**
     * 摆放头像ImageView
     */
    private fun addImageView(rule: Int = ALIGN_PARENT_LEFT) {
        //复用缓存
        val iv = if (ivCache.size > 0) {
            ivCache[0]
        } else {
            createImageView()
        }
        //当前已在屏幕显示的控件不要复用,防止params混乱
        ivCache.remove(iv)

        iv.setImageResource(res[index])
        iv.scaleType = ImageView.ScaleType.FIT_XY

        //图片资源全部播放完之后要从头重播
        index = (index + 1) % res.size

        //设置在RelativeLayout中的显示位置
        val lp = LayoutParams(avatarSize, avatarSize)
        lp.addRule(rule)
        iv.layoutParams = lp

        addView(iv)
    }

    /**
     * 轮播滚动动效
     */
    private fun startAnimMove() {
        //添加一个即将从左边移进屏幕的ImageView
        addImageView()
        //上行代码刚添加进来的最左边头像(此时RelativeLayout的mChildrenCount=4)
        getChildAt(3)?.apply {
            //设置起始的低透明度 和 小size
            alpha = 0.6F
            scaleX = scaleFrom
            scaleY = scaleFrom
            //先设置左边部分向左移出控件,即挡住左边不显示(然后才能translationXBy移进屏幕)
            translationX = -scrollLength

            //translationXBy指的是从当前位置开始移动多少距离(区别于translationX)
            //alpha是从当前透明度(即0.6F)变为设置的透明度(即1F)
            //scaleX是从当前宽度比例(即scaleFrom)变为设置的宽度比例(即1F)
            animate().translationXBy(scrollLength).alpha(1F).scaleX(1F).scaleY(1F)
                .setDuration(animDuration).start()
        }
        //中间俩头像只需设置平移的距离即可
        getChildAt(1)?.apply {
            animate().translationXBy(scrollLength).setDuration(animDuration).start()
        }
        //设置平移的距离
        getChildAt(2)?.apply {
            animate().translationXBy(scrollLength).setDuration(animDuration).start()
        }
        //最右边的头像(从完整显示 到 透明度和大小都变小,并且右移出屏幕)(因为是最先add进来的View,所以index=0)
        getChildAt(0)?.let { iv ->
            iv.animate().translationXBy(scrollLength).alpha(0F).scaleX(scaleFrom).scaleY(scaleFrom)
                .setDuration(animDuration)
                .setListener(object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator?) {
                        super.onAnimationEnd(animation)
                        //清除ImageView已有属性,并添加进ivCache缓存
                        iv.animate().setListener(null)
                        iv.clearAnimation()
                        iv.translationX = 0F
                        iv.scaleX = 1.0F
                        iv.scaleY = 1.0F
                        iv.alpha = 1F
                        //从RelativeLayout移出
                        removeView(iv)
                        ivCache.add(iv as ImageView)
                    }
                }).start()
        }
    }

    private fun sendLoopMsg() {
        handler.sendEmptyMessageDelayed(START_AVATAR_LOOP, animIntervalTime + animDuration)
    }

    private var looping = false

    /**
     * 开始轮播
     */
    fun startLoop() {
        if (looping) {
            throw Exception("startLoop cannot be called twice")
        }
        looping = true
        sendLoopMsg()
    }

    /**
     * 停止轮播
     */
    fun stopLoop() {
        looping = false
        handler.removeCallbacksAndMessages(null)
    }
}

使用

<com.hl.sun.ui.widget.LoopScrollAvatar
            android:id="@+id/loop_avatar"
            android:layout_width="150dp"
            android:layout_height="wrap_content"/>
loop_avatar.startLoop()
        loop_avatar.stopLoop()