tyktfj0910 的博客地址:http://blog.csdn.net/tyk0910
今天准备用RecyclerView来实现网易新闻Tabs的动态效果。先看效果图:
点击下面的RecyclerView的item,会有一个view的移动的动画;动画完成以后,下面的RecyclerView会有一个item删除的动画,对应上面的RecyclerView有一个item增加的动画;然后拖动上面RecyclerView的item可以进行排序,左右滑动可以进行删除。整体效果还是和网易新闻的Tabs很像,细节上处理稍微有点不一样。网易上面的item是点击删除,我这里处理成了滑动删除。
接口回调是处理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) {
//逻辑处理
}
}
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会自动填充上坐标,这个方法很重要。
现在坐标也能够获取到了,怎么实现呢:
-
调用ValueAnimator的 ofFloat() 方法就可以构建出一个ValueAnimator的实例, ofFloat() 方法当中允许传入多个float类型的参数,这里传入 0 和 mPathMeasure.getLength() 就表示将值从0平滑过渡到mPathMeasure.getLength()。
-
通过 addUpdateListener() 方法来添加一个动画的监听器,在动画执行的过程中会不断地进行回调,我们只需要在回调方法当中通过 mPathMeasure.getPosTan() 方法将当前的值取出并设置给view,就可以达到动画效果了。
-
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的倍数,就移动到下一行,不然就添加在后面。大家看示例动态图可以发现不一样。
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的增加删除动画使用的是默认的动画。记得先更新数据,在加上动画,不然会出错!!!
这里使用了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());
}
}
自定义一个类继承实现 ItemTouchHelper.Callback 接口,实现里面的三个方法,分别是设置拖动与侧滑的方向,拖动时回调的方法,侧滑时回调的方法。 然后将参数传递给 makeMovementFlags(dragFlags, swipeFlags) 中。
如果我们设置了非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的拖动排序与滑动删除就已经完成。
本篇文章只对核心代码进行了讲解,完整的项目源码大家可以点击最下方的 阅读原文 进行下载。
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: