先看作用

丝滑体验:Android 循环轮播跑马灯

丝滑体验:Android 循环轮播跑马灯

支撑暂停,康复,view自定义和池化收回复用。使用上,只需求引入xml,并绑定factory即可,内部会在attach时自动开端

 <MarqueeAnimalView
        android:id="@+id/marqueeView"
        android:layout_width="200dp"
        android:layout_height="30dp"
        android:background="@color/color_yellow" />
val list = mutableListOf("我是跑马灯1", "我不是跑马灯", "你猜我是不是跑马灯")
var position = 0
view.marqueeView.setFactory(object : PoolViewFactory {
    override fun makeView(layoutInflater: LayoutInflater, parent: ViewGroup): View {
        val view = TextView(this@ViewActivity)
        view.setPadding(0, 0, 20.dp(), 0)
        view.textSize = 12f
        view.setTextColor(ResourceUtil.getColor(R.color.white))
        return view
    }
    override fun setAnimator(objectAnimator: ObjectAnimator, width: Int, parentWidth: Int) {
        objectAnimator.duration = (parentWidth + width) * 5L
    }
    override fun setView(view: View): Boolean {
        (view as? TextView)?.text = list[position++ % list.size]
        return true
    }
})

池化思路

参考Message的思路,对view进行收回复用,避免内存持续增长,增大GC压力

private fun obtain(): View? {
    synchronized(sPoolSync) {
        if (!isAttachedToWindow) {
            return null
        }
        if (queue.isNotEmpty()) {
            return queue.poll()
        }
    }
    return factory?.makeView(layoutInflater, this@MarqueeAnimalView)?.apply {
        if (it.layoutParams == null) {
            addView(it, LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
        } else {
            addView(it)
        }
    }
}
private fun recycle(view: View) {
    synchronized(sPoolSync) {
        if (queue.size < MAX_POOL_SIZE) {
            queue.offer(view)
        }
    }
}

发明工厂

这儿的思路源于ViewSwitchFactory

interface PoolViewFactory {
    fun makeView(layoutInflater: LayoutInflater, parent: ViewGroup): View
    fun setAnimator(objectAnimator: ObjectAnimator, width: Int, parentWidth: Int)
    /**
     * 返回值,代表view是否需求重新丈量
     */
    fun setView(view: View): Boolean
}

轮询切换

这儿依据对动画进行初始化,并设置合适的监听。此刻需求获取当view和parent的width,以用于标定始末位置,需求注意x轴的正负方向。animators用于存储开端的动画,这也是设计时存在的遗留问题,因为自动取消所有动画,但view->animator是单向绑定联系,所以需求保存发生的动画

这儿遇到个坑,由于容器选用的是FrameLayouut,其在measureChildWithMargins时,会依据child layoutParams的进行丈量,所以setView后依据需求进行手动丈量,并更新layoutParams刚才生效

private val animators = hashMapOf<String, ObjectAnimator>()
private fun next(view: View?) {
    view ?: return
    if (factory?.setView(view) == true) {
        view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
        val lp = view.layoutParams
        lp.width = view.measuredWidth
        lp.height = view.measuredHeight
        view.layoutParams = lp
    }
    if (!enableAnimated) {
        return
    }
    val width = view.measuredWidth
    val parentWidth = measuredWidth
    val targetValue = parentWidth - width
    val animator = ObjectAnimator.ofFloat(view, PROPERTY_NAME, parentWidth.toFloat(), -width.toFloat()).apply {
        // null即为默认线性插值器
        interpolator = null
        addUpdateListener(
            RecyclerAnimatorUpdateListener(targetValue) {
                next(obtain())
                removeUpdateListener(it)
            }
        )
        addListener(this@MarqueeAnimalView)
        factory?.setAnimator(this, width, parentWidth)
    }
    animators["${view.hashCode()}-${animator.hashCode()}"] = animator
    animator.start()
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            // 这儿的childDimension源于 child.getLayoutParams.width\height
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            // 假如为WRAP_CONTENT,就约束了最大尺度
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // ……
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

动画监听

当动画结束时,需求对view进行收回,并对动画移除。取消动画时,需求将view强制归位

同时,为了方便使用,OnAttachStateChangeListener使得全体动画愈加平滑,也避免了view不行见时,动画仍然在持续履行浪费资源。当然如fragment不行见时的监听需求完善

override fun onAnimationEnd(animation: Animator?) {
    (animation as? ObjectAnimator)?.let { animator ->
        (animator.target as? View)?.let { view ->
            animators.remove("${view.hashCode()}-${animator.hashCode()}")
            recycle(view)
        }
        // target释放
        animator.target = null
    }
}
override fun onAnimationCancel(animation: Animator?) {
    (animation as? ObjectAnimator)?.let { animator ->
        (animator.target as? View)?.let { view ->
            view.translationX = measuredWidth.toFloat()
        }
    }
}
override fun onViewAttachedToWindow(v: View?) {
    if (animators.isNotEmpty()) {
        resume()
    } else {
        start()
    }
}
override fun onViewDetachedFromWindow(v: View?) {
    pause()
}

对外能力

var enableAnimated = true
fun start() {
    if (measuredWidth == 0) {
        this.post {
            // 假如丈量还未完结,那就等待post后建议
            next(obtain())
        }
        return
    }
    next(obtain())
}
fun stop() {
    val it = animators.values.iterator()
    while (it.hasNext()) {
        val i = it.next()
        it.remove()
        i.cancel()
    }
}
fun pause() {
    for (i in animators.values) {
        i.pause()
    }
}
fun resume() {
    for (i in animators.values) {
        i.resume()
    }
}

完整代码

欢迎支撑,搜索MarqueeAnimalView即可 github.com/wjf-962464/…