先看效果
支持暂停,恢复,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 { 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<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 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() } }
完整代码
欢迎支持,搜索MarqueeAnimalView即可 github.com/wjf-962464/Self_Demo.git