实现一个《头像循环轮播控件》

效果如下

在这里插入图片描述
即以下5张图循环播放:
在这里插入图片描述

实现思路

  仔细看动效会发现,同时出现的最多可有四张图。我们用一个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后,再放入缓存等待取出,如此循环。
在这里插入图片描述

滚动距离计算:
scrollLength(滚动距离)在onSizeChanged方法中动态计算,公式为:(width - avatarSize) / 2F
在这里插入图片描述

代码实现

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()
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值