滚动弹幕
偷张图,免得自己还得录像转换
来源:https://www.jianshu.com/p/e55b73f3e913
第一眼想到的就是recyclerview的动画,不停的移除第一个item,那么不就是这种效果吗?
然后开始写。哎,没写过动画了还。
首先先用系统的默认动画,默认动画是个透明度从0到1的动画,duration是120毫秒。
实际,我们这里需要2种操作,移除第一个item,以及在最后一个位置上添加一个item。
弄一个runnable添加数据,或者删除数据。
截张图留念,最后一个正在从小到大变化。
image.png
首先布局,我item高度固定的,这样的话我们如果只要显示4个,那么recyclerview的高度就能算出来,可以让最后一个刚好在页面最底部。这些看实际需求。
我这里item高度是50,外加itemDecoration的间隔10,所以4个的话高度就是240.
我平板是1dp=1px
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/include_toolbar"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_toast"
android:layout_gravity="bottom"
android:layout_width="wrap_content"
android:layout_height="240dp"/>
</FrameLayout>
代码如下:中间的adapter用自己的就行,我这里就简单封装了一下。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_anima)
defaultSetTitle("toast")
initTestDatas()
rv_toast.apply {
layoutManager=LinearLayoutManager(this@ActivitySimpleAnima)
addItemDecoration(object :RecyclerView.ItemDecoration(){
override fun getItemOffsets(outRect: Rect, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
outRect.bottom=10
outRect.left=10
}
})
itemAnimator=ItemAnimatorScaleLB2RT().apply {
removeDuration=1000
addDuration=1222
}
adapterRV=object :BaseRvAdapter<String>(){
override fun getLayoutID(viewType: Int): Int {
return R.layout.item_simple_anima_text
}
override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
holder.setText(R.id.tv_toast,getItemData(position))
}
}
adapter= adapterRV
}
startSimulateRefresh()
}
lateinit var adapterRV:BaseRvAdapter<String>
var messages= arrayListOf<String>()
var colors= arrayListOf<Int>()
private fun initTestDatas(){
messages.add("今天是个好日子。")
messages.add("机箱的光阴不能忘。")
messages.add("明天有出去玩的吗。")
messages.add("有啊,要一起吗?")
messages.add("我刚才看到某某拉。")
messages.add("你们在说啥了")
messages.add("听说周末有雨额,哪也去不了")
messages.add("我们这里不下雨,没事")
messages.add("累了,去睡了,再见。")
messages.add("今天是个好日子。")
messages.add("机箱的光阴不能忘。")
messages.add("明天有出去玩的吗。")
messages.add("有啊,要一起吗?")
messages.add("我刚才看到某某拉。")
messages.add("你们在说啥了")
messages.add("听说周末有雨额,哪也去不了")
messages.add("我们这里不下雨,没事")
messages.add("累了,去睡了,再见。")
colors.add(Color.RED)
colors.add(Color.GREEN)
colors.add(Color.BLUE)
colors.add(Color.YELLOW)
colors.add(Color.LTGRAY)
}
var handler=Handler()
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
private fun startSimulateRefresh(){
handler.postDelayed(viewRunnble,100)
}
var index=0;
val viewRunnble= object :Runnable {
override fun run() {
adapterRV.apply {
if (adapterRV.itemCount < 4) {
this.datas.add(messages[index % messages.size])//最后一个位置添加数据并notify
index++
notifyItemInserted(datas.size - 1)
} else {
this.datas.removeAt(0)//删除第一条数据
rv_toast.adapter.notifyItemRemoved(0)
}
}
handler.postDelayed(this, 2000)
}
}
上边有个ItemAnimatorScaleLB2RT ,我本来想写个从左下角到右上角的放大动画的,可惜不会,
所以我就复制了下系统默认提供的那个DefaultItemAnimator,然后,在add里稍微修改了下
add方法的修改如下,照着他那透明度写就行了。以后如果有别的动画需求也在这里改就行。
额,这2个方法就是添加一个新的item的动画拉
image.png
目前还有个bug,每条对话长度不一样,后边长的消失的时候感觉瞬间少了一部分。。还在研究原因。
bug分析
周一比较有感觉,我给rv加了个背景,然后就很快发现问题了。问题在于我的rv宽度是wrap_content的。
要rvmove的item长度是最长的,比如100,其他都是80.在remove这个item的时候,动画还没开始,rv的宽度因为是wrap就变成了80了,所以就看到一个不正常的现象了。
修复这个bug,需要将宽度固定。使用match_parent或者给个固定的宽度。
最后再学习下LayoutTransition
上代码,就用开头的帖子弄的,现在学习kotlin,所以用这个写了,原作者弄个pools也没用,这里也都加上了。
//布局就是个线性布局
<LinearLayout
android:id="@+id/layout_container"
android:orientation="vertical"
android:showDividers="middle"
android:divider="@drawable/shape_space10"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
//divider也简单就是个空白间隔
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="10dp" android:width="10dp"/>
</shape>
//完整的代码如下
private fun useTransition(){
//初始化add动画,拉伸动画,从小到大
var scaleAnimaX=PropertyValuesHolder.ofFloat("scaleX",0f,1f)
var scaleAnimaY=PropertyValuesHolder.ofFloat("scaleY",0f,1f)
var scale=ObjectAnimator.ofPropertyValuesHolder(null as Any?,scaleAnimaX,scaleAnimaY)
//这里kotlin的null,半天不知道咋写,后来还是看源码里有这样的(Object)null ,才改对了kotlin的写法。
scale.addListener(object :AnimatorListenerAdapter(){
override fun onAnimationStart(animation: Animator?) {
super.onAnimationStart(animation)
var target=(animation as ObjectAnimator).target as View
target.pivotY=target.height.toFloat()//修改下中心点,默认是控件的正中心,我们这里是从左下角开始变大的
}
})
var transition=LayoutTransition().apply {
setAnimator(LayoutTransition.APPEARING,scale) //这里只修改出现的动画,其他的用默认的,默认的消失动画就是一个透明度从1到0的动画。
setDuration(500)
}
layout_container.layoutTransition=transition
handler.post(addViewRunnable) //开始添加删除控件
}
var i=0;
var addViewRunnable= object :Runnable {
override fun run() {
if(layout_container.childCount>=4){//控件已经有4的话,执行remove操作
var tv=layout_container.getChildAt(0) as TextView
pools.release(tv) //回收的控件留着复用,存起来
layout_container.removeViewAt(0)
}else{
var tv=getView().apply {
text=messages[i%messages.size]
setBackgroundColor(colors[i%colors.size])
i++
}
layout_container.addView(tv)
}
handler.postDelayed(this,1000)
}
}
var pools=Pools.SimplePool<TextView>(4)
fun getView():TextView{
//先从pools 里看有没有回收的view,有的话就复用,没有就new一个
var view=pools.acquire()?:TextView(this).apply {
layoutParams= ViewGroup.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT)
setPadding(20,10,20,10)
setTextSize(22f)
setTextColor(Color.WHITE)
pivotX=0f
}
return view
}
另外一点知识ofMultiFloat
看objectAnimator的时候看到这个,然后看下方法注释,应该是为一个set方法有2个float参数用的
如下
public static ObjectAnimator ofMultiFloat(Object target, String propertyName, Path path) {
PropertyValuesHolder pvh = PropertyValuesHolder.ofMultiFloat(propertyName, path);
return ofPropertyValuesHolder(target, pvh);
}
//上边的方法看不出什么,我们看具体的holder解释
//The setter must take exactly two <code>float</code> parameters. 这句
/**
* Constructs and returns a PropertyValuesHolder with a given property name to use
* as a multi-float setter. The values are animated along the path, with the first
* parameter of the setter set to the x coordinate and the second set to the y coordinate.
*
* @param propertyName The name of the property being animated. Can also be the
* case-sensitive name of the entire setter method. Should not be null.
* The setter must take exactly two <code>float</code> parameters.
* @param path The Path along which the values should be animated.
* @return PropertyValuesHolder The constructed PropertyValuesHolder object.
* @see ObjectAnimator#ofPropertyValuesHolder(Object, PropertyValuesHolder...)
*/
public static PropertyValuesHolder ofMultiFloat(String propertyName, Path path) {
Keyframes keyframes = KeyframeSet.ofPath(path);
PointFToFloatArray converter = new PointFToFloatArray();
//这个类里边有个private float[] mCoordinates = new float[2];后边获取animator的value的时候会需要
return new MultiFloatValuesHolder(propertyName, converter, null, keyframes);
}
就简单写了个
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
class WidgetXy :View{
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
private var pX=0f;
private var pY=0f;
//ObjectAnimator里的name就是这个“XandY”,animator数值改变的时候会自动调用这个方法的
fun setXandY(pX:Float,pY:Float){
this.pX=pX
this.pY=pY
postInvalidate()
}
var paint=Paint(Paint.ANTI_ALIAS_FLAG)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if(width==0){
return
}
canvas.save()
canvas.translate(width/2f,height/2f)
paint.color=Color.BLACK
paint.style=Paint.Style.STROKE
paint.strokeWidth=10f
canvas.drawCircle(0f,0f,width/3f,paint)
if(pX!=0f){
paint.style=Paint.Style.FILL
paint.color=Color.RED
canvas.drawCircle(pX,pY,5f,paint)
}
canvas.restore()
}
}
使用如下
<com.charliesong.demo0327.layoutmanager.WidgetXy
android:id="@+id/wxy"
android:layout_gravity="right"
android:layout_marginTop="100dp"
android:layout_width="200dp"
android:layout_height="200dp" />
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun wxytest(){
var path=Path()
path.moveTo(0f,50f)
path.lineTo(50f,0f)
path.lineTo(0f,-50f)
path.lineTo(-50f,0f)
path.close()
ObjectAnimator.ofMultiFloat(wxy,"XandY",path).setDuration(5000)
.apply {
addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
override fun onAnimationUpdate(animation: ValueAnimator) {
var valuesHolder=animation.animatedValue as FloatArray//这里就是个长度为2的float类型的数组
println("===========${Arrays.toString(valuesHolder)}")
}
})
repeatCount=10
interpolator=LinearInterpolator()
}
.start()
}
override fun onPause() {
super.onPause()
wxy.animation?.cancel()
}
使用中有个问题,上边的Path,我只能addline,我尝试用了addcircle或者addarc,结果就是不动,就是个固定的初始值一直在变化,不知道咋回事。以后有空再研究为啥