滚动弹幕

作者: 有点健忘 | 来源:发表于2018-05-25 18:03 被阅读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,结果就是不动,就是个固定的初始值一直在变化,不知道咋回事。以后有空再研究为啥

    相关文章

      网友评论

        本文标题:滚动弹幕

        本文链接:https://www.haomeiwen.com/subject/jiujjftx.html