Android怎么实现循环轮播跑马灯效果
支持暂停,恢复,view自定义和池化回收复用。使用上,只需要引入xml,并绑定factory即可,内部会在attach时自动开始
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 {
addView(this, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
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是单向绑定关系,所以需要保存发生的动画
private val animators = hashMapOf() 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 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() }
动画监听
当动画结束时,需要对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()
}对外能力
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()
}
}