Android开发

滚动弹幕

2018-05-25  本文已影响17人  有点健忘

偷张图,免得自己还得录像转换
来源:https://www.jianshu.com/p/e55b73f3e913

image.png

第一眼想到的就是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,结果就是不动,就是个固定的初始值一直在变化,不知道咋回事。以后有空再研究为啥

上一篇下一篇

猜你喜欢

热点阅读