RecyclerView学习(二)----高仿网易新闻栏目动画效果

  1. 云栖社区>
  2. 博客>
  3. 正文

RecyclerView学习(二)----高仿网易新闻栏目动画效果

tangyangkai 2016-05-20 12:51:00 浏览1267
展开阅读全文

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

之前用TabLayout+RecyclerView实现了CSDN客户端首页搭建与Tabs的排序。今天准备用RecyclerView来实现网易新闻Tabs的动态效果。先看效果图:
这里写图片描述

点击下面的RecyclerView的item,会有一个view的移动的动画;动画完成以后,下面的RecyclerView会有一个item删除的动画,对应上面的RecyclerView有一个item增加的动画;然后拖动上面RecyclerView的item可以进行排序,左右滑动可以进行删除。
整体效果还是和网易新闻的Tabs很像,细节上处理稍微有点不一样。网易上面的item是点击删除,我这里处理成了滑动删除。

项目整体源码后面会给出下载链接,这里就只介绍重点部分的代码以及实现原理。

1.RecyclerView的点击事件
接口回调是处理RecyclerView点击事件很好的方式,分析一下下面RecyclerView的点击事件。点击item的时候,有一个移动的动画,所以我们需要点击item的view;动画完成以后,下面的RecyclerView有一个删除的动画,所以我们需要item的position。综上,我们的接口就出来了:

    public interface onAllTabsListener {
        void allTabsItemClick(View view,int position);
    }

然后就是在点击RecyclerView的item的时候,传递参数:

        viewHolder.txt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                listener.allTabsItemClick(viewHolder.itemView,position);
            }
        });

最后就是让Activity实现这个接口,接受传递的参数,进行逻辑的处理:

public class WangYiActivity extends AppCompatActivity implements AllTabsAdapter.onAllTabsListener {}
public void allTabsItemClick(final View view, final int position) {}

2.RecyclerView的移动动画

RecyclerView的item移动动画,我这里使用的是属性动画加上一阶贝塞尔曲线实现的。

科普时间(引用郭神博客):
ValueAnimator是整个属性动画机制当中最核心的一个类,前面我们已经提到了,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。

其实RecyclerView的item移动动画的实现就是对view的x,y坐标不断赋值,不断更新,达到移动动画效果。

那怎么获取到view的x,y坐标呢,这里我们就需要Android中另外一个非常重要的类来实现了—-Path路径类(封装了贝塞尔曲线)。这里我们使用的是一阶贝塞尔曲线,看一下它的几个重要方法:
(1.)moveTo(float,float)
用于设置移动路径的起始点Point(x,y),对于android系统来说,屏幕的左上角的坐标是 (0,0) , 我们在做一些操作的时候默认基准点也是 (0,0)。Path 的moveTo 方法可以与此进行一个类比,就是为了改变 Path 的起始点。
(2.)lineTo(float x,float y)
上一个点以直线的方式连接到参数里的 (x,y)
(3.)路径测量PathMeasure
起点与终点拿到了,我们需要获取到x,与y的坐标,PathMeasure提供了以下的方法
float getLength() :测量path的距离
getPosTan(float distance, float[] pos, float[] tan) :传入一个距离distance(0<=distance<=getLength()),然后会计算当前距离的坐标点和切线,pos会自动填充上坐标,这个方法很重要。

现在坐标也能够获取到了,怎么实现呢:
1.调用ValueAnimator的ofFloat()方法就可以构建出一个ValueAnimator的实例,ofFloat()方法当中允许传入多个float类型的参数,这里传入0和mPathMeasure.getLength()就表示将值从0平滑过渡到mPathMeasure.getLength()。
2.通过addUpdateListener()方法来添加一个动画的监听器,在动画执行的过程中会不断地进行回调,我们只需要在回调方法当中通过mPathMeasure.getPosTan()方法将当前的值取出并设置给view,就可以达到动画效果了。
3.addListener方法来监听动画完成以后的操作,数据的添加,删除等

    public void allTabsItemClick(final View view, final int position) {


        final PathMeasure mPathMeasure;
        final float[] mCurrentPosition = new float[2];
        int parentLoc[] = new int[2];
        linearLayout.getLocationInWindow(parentLoc);
        int startLoc[] = new int[2];
        view.getLocationInWindow(startLoc);

        final View startView = view;
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(view.getWidth(), view.getHeight());
        allRecycle.removeView(view);
        linearLayout.addView(startView, params);


        final View endView;
        float toX, toY;
        int endLoc[] = new int[2];
        //进行判断
        int i = choseTabs.size();
        if (i == 0) {
            toX = view.getWidth();
            toY = view.getHeight();
        } else if (i % 4 == 0) {
            endView = choseRecycle.getChildAt(i - 4);
            endView.getLocationInWindow(endLoc);
            toX = endLoc[0] - parentLoc[0];
            toY = endLoc[1] + view.getHeight() - parentLoc[1];
        } else {
            endView = choseRecycle.getChildAt(i - 1);
            endView.getLocationInWindow(endLoc);
            toX = endLoc[0] + view.getWidth() - parentLoc[0];
            toY = endLoc[1] - parentLoc[1];
        }


        Log.e("tag", allTabs.size() + "@");
        Log.e("tag", choseTabs.size() + "@@");


        float startX = startLoc[0] - parentLoc[0];
        float startY = startLoc[1] - parentLoc[1];


        Path path = new Path();
        path.moveTo(startX, startY);
        path.lineTo(toX, toY);
        mPathMeasure = new PathMeasure(path, false);


        //属性动画实现
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
        valueAnimator.setDuration(500);
        // 匀速插值器
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                // 获取当前点坐标封装到mCurrentPosition
                mPathMeasure.getPosTan(value, mCurrentPosition, null);
                startView.setTranslationX(mCurrentPosition[0]);
                startView.setTranslationY(mCurrentPosition[1]);
            }
        });
        valueAnimator.start();
    }

起点的坐标就是我们点击RecyclerView的item的坐标,根据传递过来的view进行计算的。终点的坐标我们这里进行了一下判断,根据上面RecyclerView的size进行判断。要是4的倍数,就移动到下一行,不然就添加在后面。大家看示例动态图可以发现不一样。

关于属性动画,贝塞尔曲线,大家可以看我另外一篇博客,里面介绍的很详细:
Android补间动画,属性动画实现购物车添加动画

3.RecyclerView的增加删除动画

        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }
            @Override
            public void onAnimationEnd(Animator animation) {

                //默认recyclerviewe的动画
                allRecycle.setItemAnimator(new DefaultItemAnimator());
                choseRecycle.setItemAnimator(new DefaultItemAnimator());
                choseTabs.add(choseTabs.size(), allTabs.get(position));
                allTabs.remove(position);
                //先更新数据
                allAdapter.notifyDataSetChanged();
                choseAdapter.notifyDataSetChanged();
                //再更新动画
                allAdapter.notifyItemRemoved(position);
                choseAdapter.notifyItemInserted(choseTabs.size());
                linearLayout.removeView(startView);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }
            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

在移动动画完成以后,加上一个监听。这里RecyclerView的增加删除动画使用的是默认的动画。记得先更新数据,在加上动画,不然会出错!!!

4.RecyclerView的拖动排序与滑动删除
这里使用了RecyclerView的ItemTouchHelper类来实现了Item的拖动和删除功能,ItemTouchHelper是v7包下的一个类,专门用来配合RecyclerView实现滑动删除和拖拽功能的类。我们看看怎么使用。

public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
    private onMoveAndSwipedListener mAdapter;


    public SimpleItemTouchHelperCallback(onMoveAndSwipedListener listener) {
        mAdapter = listener;
    }


    /**
     * 这个方法是用来设置我们拖动的方向以及侧滑的方向的
     */
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        //如果是ListView样式的RecyclerView
        //设置拖拽方向为上下左右都可以
        final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN |
                ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        //设置侧滑方向为从左到右和从右到左都可以
        final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
        //将方向参数设置进去
        return makeMovementFlags(dragFlags, swipeFlags);

    }

    /**
     * 当我们拖动item时会回调此方法
     */
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {

        //如果两个item不是一个类型的,我们让他不可以拖拽
        if (viewHolder.getItemViewType() != target.getItemViewType()) {
            return false;
        }
        //回调adapter中的onItemMove方法
        mAdapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    /**
     * 当我们侧滑item时会回调此方法
     */
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
    }

}

(1)自定义一个类继承实现ItemTouchHelper.Callback接口,实现里面的三个方法,分别是设置拖动与侧滑的方向,拖动时回调的方法,侧滑时回调的方法。
然后将参数传递给 makeMovementFlags(dragFlags, swipeFlags)中
(2)如果我们设置了非0的dragFlags 与swipeFlags,那么当item被拖拽与侧滑的时候会不断的回调onMove与onSwiped方法,所以我们需要同时Adapter做出相应的改变,对mItems数据做出交换与删除的操作,因此我们需要一个回调接口来继续回调Adapter中的方法。

public interface onMoveAndSwipedListener {
    boolean onItemMove(int fromPosition , int toPosition);
    void onItemDismiss(int position);
}

我们让TabsAdapter实现此接口,并且重写里面的方法

public class ChoseTabsAdapter extends RecyclerView.Adapter implements onMoveAndSwipedListener {}

重写拖动的方法,其实就是交换集合中指定元素的位置:

    @Override
    public boolean onItemMove(int fromPosition, int toPosition) {
        //交换mItems数据的位置
        Collections.swap(WangYiActivity.choseTabs, fromPosition, toPosition);
        //交换RecyclerView列表中item的位置
        notifyItemMoved(fromPosition, toPosition);
        return true;
    }

滑动删除,就是拿到position进行数组的删除操作:

    @Override
    public void onItemDismiss(int position) {
        //删除mItems数据
        WangYiActivity.choseTabs.remove(position);
        //删除RecyclerView列表对应item
        notifyItemRemoved(position);
    }

再回到我们的SimpleItemTouchHelperCallback,在构造方法中将实现了onMoveAndSwipedListener接口的TabsAdapter 传进来。然后我们就在onMove()方法里获取当前拖拽的item和已经被拖拽到所处位置的item的ViewHolder,有了这2个ViewHolder,我们就可以拿到对应的position,然后调用传递过来的adapter中的onItemMove方法,这样adapter就会进行改变;在onSwiped()方法里面获取侧滑的item的position,然后调用传递过来的adapter中的onItemDismiss方法即可。

然后就是关联我们的ItemTouchHelper和RecyclerView:

        //关联ItemTouchHelper和RecyclerView
        ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(choseAdapter);
        mItemTouchHelper = new ItemTouchHelper(callback);
        mItemTouchHelper.attachToRecyclerView(choseRecycle);

至此,RecyclerView的拖动排序与滑动删除就已经完成,感兴趣的可以参考我另外一篇博客:
仿CSDN客户端首页(二)—-拖拽排序Tabs的实现

另外一些小细节,比如设置RecyclerView的间隔,获取RecyclerView的子item的view等,大家可以下载源码自己看看。

源码地址:

https://github.com/18722527635/MyRecyclerView

欢迎star,fork,提issues,一起进步!

欧了,收工~~

网友评论

登录后评论
0/500
评论
tangyangkai
+ 关注