简述
在Android 开发中,可能有些小伙伴会遇到类似这种功能:
1.在画板上添加一些贴图,从图片列表中拖曳到画板上
2.将某个新闻条目拖动到收藏夹包里保存
对此写了关于RecyclerView 简单拖曳复制View的文章 ,给小伙伴们提供一下灵感和思路,也算是抛砖引玉,有更好的文章或者想法也希望能在评论区留言一下,共同进步。
老规矩,先上图:
解决思路
首先我们把gif图所展示整体功能拆分成几个步骤
1.item长按点击时生成一个新的View
2.View 的滑动处理事件效果编辑
3.解决RecyclerView 与生成的View 在触摸事件上的冲突
这边会通过代码和文字把以上这几个问题如何一一解决呈现给小伙伴们,先提出总提方向是让大家有个大致思路,再往下就比较好理解。
页面布局(只截图不给代码,相信小伙伴们能看懂)
适配器布局:
主界面布局:
代码部分
PicAdapter的实现非常简单,也不是本次讨论重点,这边主要就是就把长按点击通过接口回调处理
代码如下:
public class PicAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context context;
private List<DataEntity> dataEntityList;
private OnItemLongClickListener onItemLongClickListener;
public PicAdapter(Context context, List<DataEntity> dataEntityList, OnItemLongClickListener onItemLongClickListener) {
this.context=context;
this.dataEntityList = dataEntityList;
this.onItemLongClickListener=onItemLongClickListener;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view= LayoutInflater.from(context).inflate(R.layout.item_pic,null);
ViewHolder viewHolder=new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
((ViewHolder) holder).lyItem.setBackgroundColor(Color.parseColor(dataEntityList.get(position).getColor()));
((ViewHolder) holder).tvView.setText(dataEntityList.get(position).getName());
((ViewHolder) holder).lyItem.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if(onItemLongClickListener!=null)onItemLongClickListener.onItemClickEvent(v,position);
return true;
}
});
}
@Override
public int getItemCount() {
return dataEntityList==null? 0: dataEntityList.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
LinearLayout lyItem;
TextView tvView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
lyItem=itemView.findViewById(R.id.lyItem);
tvView=itemView.findViewById(R.id.tvView);
}
}
public interface OnItemLongClickListener {
void onItemClickEvent(View view, int selectPosition);
}
}
DataEntity 是一个拥有名字和颜色属性的简单类,用在主界面添加循环生成随机颜色的集合对象,代码如下:
public class DataEntity {
private String name;
private String color;
public DataEntity(String name, String color) {
this.name = name;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
MainActivity的所有代码(有的小伙伴看不懂就可以往下看我简单的解释)
public class MainActivity extends AppCompatActivity {
private Button btnTool;
private RecyclerView ryTool;
private LinearLayout lyTool,deleteView;
private RelativeLayout rlView;
private boolean showToolView ,itemPress;
private PicAdapter picAdapter;
private List<DataEntity>dataEntityList=new ArrayList<>();
private List<View> copyView=new ArrayList<>();
private View.OnTouchListener onTouchListener;
private int startY;
private int startX;
private boolean ryCanScroll=true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initAdapter();
initListener();
}
private void initListener() {
btnTool.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showRy(showToolView=!showToolView);
}
});
onTouchListener=new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
moveViewEvent( v, event);
return true;
}
};
ryTool.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//当生成复制View 的时候,禁止RecyclerView 滑动
return !ryCanScroll;
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//触发长按之后,item的触摸事件来到这里了.MotionEvent返回手指移动的位置.以及up事件
if(copyView.size()>0 && itemPress){
ryMoveEvent(copyView.get(copyView.size()-1),e);
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
}
private void ryMoveEvent(View v, MotionEvent event) {
if(v==null)return;
v.setScaleX(1.3f);
v.setScaleY(1.3f);
int[] location = new int[2];
v.getLocationOnScreen(location);
//手指按下后View 产生位移偏差,好看得出复制了
startX = location[0]+dp2px(this,30);
startY = location[1]+dp2px(this,60);;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
//获取移动后的坐标
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
//拿到手指移动距离的大小
int move_bigX = moveX - startX;
int move_bigY = moveY - startY;
//拿到当前控件未移动的坐标:只需要计算该控件离左边和上边的距离即可
int left = v.getLeft();
int top = v.getTop();
left += move_bigX;
top += move_bigY;
RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
params.setMargins(left, top, 0, 0);
v.setLayoutParams(params);
startX = moveX;
startY = moveY;
break;
case MotionEvent.ACTION_CANCEL://手指抬起来的同时判断是否红色范围内,是则回收该View
case MotionEvent.ACTION_UP:
v.setScaleX(1.0f);
v.setScaleY(1.0f);
itemPress=false;
ryCanScroll=true;
if(isInChangeImageZone(deleteView,(int)event.getRawX(),(int)event.getRawY())){
copyView.remove(v);
rlView.removeView(v);
}
break;
}
}
private void moveViewEvent(View v, MotionEvent event) {
if(v==null)return;
v.setScaleX(1.3f);
v.setScaleY(1.3f);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取当前按下的坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//获取移动后的坐标
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
//拿到手指移动距离的大小
int move_bigX = moveX - startX;
int move_bigY = moveY - startY;
//拿到当前控件未移动的坐标:只需要计算该控件离左边和上边的距离即可
int left = v.getLeft();
int top = v.getTop();
left += move_bigX;
top += move_bigY;
RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
params.setMargins(left, top, 0, 0);
v.setLayoutParams(params);
startX = moveX;
startY = moveY;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
v.setScaleX(1.0f);
v.setScaleY(1.0f);
if(isInChangeImageZone(deleteView,(int)event.getRawX(),(int)event.getRawY())){
copyView.remove(v);
rlView.removeView(v);
}
break;
}
}
private void initAdapter() {
for (int i=0;i<18;i++){
DataEntity dataEntity=new DataEntity("图片"+i,getRandColor());
dataEntityList.add(dataEntity);
}
picAdapter=new PicAdapter(this, dataEntityList, new PicAdapter.OnItemLongClickListener() {
@Override
public void onItemClickEvent(View view, int selectPosition) {
//长按
copyItem(view ,selectPosition);
itemPress=true;
ryCanScroll=false;
}
});
ryTool.setLayoutManager(new GridLayoutManager(this,3));
ryTool.setAdapter(picAdapter);
}
private void copyItem(View view,int selectPosition) {
int[] location = new int[2];
view.getLocationOnScreen(location);
LinearLayout linearLayout=new LinearLayout(this);
linearLayout.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams layoutParams=new LinearLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
linearLayout.setLayoutParams(layoutParams);
layoutParams.setMargins(location[0],location[1],0,0);
linearLayout.setOnTouchListener(onTouchListener);
linearLayout.setBackgroundColor(Color.parseColor(dataEntityList.get(selectPosition).getColor()));
TextView textView=new TextView(this);
textView.setText("复制:"+dataEntityList.get(selectPosition).getName());
ViewGroup.LayoutParams params=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,dp2px(this,80));
textView.setLayoutParams(params);
textView.setTextColor(Color.WHITE);
textView.setGravity(Gravity.CENTER);
linearLayout.addView(textView);
copyView.add(linearLayout);
rlView.addView(linearLayout);
}
private void showRy(boolean b) {
lyTool.setVisibility(b? View.VISIBLE:View.GONE);
}
private void initView() {
btnTool=findViewById(R.id.btnTool);
deleteView=findViewById(R.id.deleteView);
ryTool =findViewById(R.id.ry);
lyTool=findViewById(R.id.lyTool);
rlView=findViewById(R.id.rlView);
}
/**
* 获取十六进制的颜色代码.例如 "#5A6677"
* 分别取R、G、B的随机值,然后加起来即可
* 通过Color.parseColor()转为color值即可使用
* @return String
*/
public static String getRandColor() {
String R, G, B;
Random random = new Random();
R = Integer.toHexString(random.nextInt(256)).toUpperCase();
G = Integer.toHexString(random.nextInt(256)).toUpperCase();
B = Integer.toHexString(random.nextInt(256)).toUpperCase();
R = R.length() == 1 ? "0" + R : R;
G = G.length() == 1 ? "0" + G : G;
B = B.length() == 1 ? "0" + B : B;
return "#" + R + G + B;
}
/**
* dp 转 px
*/
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 用于判断某个坐标是否在View 范围内
*
* */
private Rect mChangeImageBackgroundRect = null;
private boolean isInChangeImageZone(View view, int x, int y) {
if (null == mChangeImageBackgroundRect) {
mChangeImageBackgroundRect = new Rect();
}
view.getDrawingRect(mChangeImageBackgroundRect);
int[] location = new int[2];
view.getLocationOnScreen(location);
mChangeImageBackgroundRect.left = location[0];
mChangeImageBackgroundRect.top = location[1];
mChangeImageBackgroundRect.right = mChangeImageBackgroundRect.right + location[0];
mChangeImageBackgroundRect.bottom = mChangeImageBackgroundRect.bottom + location[1];
return mChangeImageBackgroundRect.contains(x, y);
}
}
item长按点击时生成一个新的View (看此函数 copyItem(View view,int selectPosition) )
,是通过根布局rlView添加到界面中去,同时为了方便管理移除,这边会把添加copyView 集合list中去,实际上这样写很繁琐,可以通过继承线性布局,将其封装成一个类这样比较直观。
View 的滑动处理事件效果编辑则是通过onTouchListener,在里面实现缩放,移动,与手指抬起判断。
注意:这里边view的移动不可通过view.layout(l,t,r,b) 来实现,因为当父组件rlView.addView(View v)时,都会将重置子View位置
onTouchListener=new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
moveViewEvent( v, event);
return true;
}
};
private void moveViewEvent(View v, MotionEvent event) {
if(v==null)return;
v.setScaleX(1.3f);
v.setScaleY(1.3f);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取当前按下的坐标
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//获取移动后的坐标
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
//拿到手指移动距离的大小
int move_bigX = moveX - startX;
int move_bigY = moveY - startY;
//拿到当前控件未移动的坐标:只需要计算该控件离左边和上边的距离即可
int left = v.getLeft();
int top = v.getTop();
left += move_bigX;
top += move_bigY;
RelativeLayout.LayoutParams params= new RelativeLayout.LayoutParams(dp2px(this,100),dp2px(this,80));
params.setMargins(left, top, 0, 0);
v.setLayoutParams(params);
startX = moveX;
startY = moveY;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
v.setScaleX(1.0f);
v.setScaleY(1.0f);
if(isInChangeImageZone(deleteView,(int)event.getRawX(),(int)event.getRawY())){
copyView.remove(v);
rlView.removeView(v);
}
break;
}
}
关于RecyclerView 与生成的View 在触摸事件上的冲突,这边着重要解决的问题就是长按时(没有松开),实际上事件分发到我们的ryTool,但是滑动的时候却是移动产生在ryTool 上面的view, 这里我们要做的两点就是
1.长按时,将禁止 RecyclerView 的滚动
这边通过设置 ryCanScroll ,在长按时的回调设置为false
2.将RecyclerView上产生的任何触摸事件传给最新生成的View 移动,且当手指抬起时,则允许RecyclerView重新滚动。
至此,整篇文章讲解完毕,这次写的也算比较粗糙,讲解的也是一些很表面上的东西,深层次的事件分发也没有展开讲,只算给小伙伴们提供一些思路,若是哪里有误,欢迎留言提出,共同进步。