先看作用
支撑暂停,康复,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/…