簡(jiǎn)述
在A(yíng)ndroid 開(kāi)發(fā)中昧狮,可能有些小伙伴會(huì)遇到類(lèi)似這種功能:
1.在畫(huà)板上添加一些貼圖明场,從圖片列表中拖曳到畫(huà)板上
2.將某個(gè)新聞條目拖動(dòng)到收藏夾包里保存
對(duì)此寫(xiě)了關(guān)于RecyclerView 簡(jiǎn)單拖曳復(fù)制View的文章 北秽,給小伙伴們提供一下靈感和思路,也算是拋磚引玉,有更好的文章或者想法也希望能在評(píng)論區(qū)留言一下畦韭,共同進(jìn)步断盛。
老規(guī)矩罗洗,先上圖:
解決思路
首先我們把gif圖所展示整體功能拆分成幾個(gè)步驟
1.item長(zhǎng)按點(diǎn)擊時(shí)生成一個(gè)新的View
2.View 的滑動(dòng)處理事件效果編輯
3.解決RecyclerView 與生成的View 在觸摸事件上的沖突
這邊會(huì)通過(guò)代碼和文字把以上這幾個(gè)問(wèn)題如何一一解決呈現(xiàn)給小伙伴們,先提出總提方向是讓大家有個(gè)大致思路钢猛,再往下就比較好理解伙菜。
頁(yè)面布局(只截圖不給代碼,相信小伙伴們能看懂)
適配器布局:
主界面布局:
代碼部分
PicAdapter的實(shí)現(xiàn)非常簡(jiǎn)單命迈,也不是本次討論重點(diǎn)贩绕,這邊主要就是就把長(zhǎng)按點(diǎn)擊通過(guò)接口回調(diào)處理
代碼如下:
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 是一個(gè)擁有名字和顏色屬性的簡(jiǎn)單類(lèi),用在主界面添加循環(huán)生成隨機(jī)顏色的集合對(duì)象,代碼如下:
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的所有代碼(有的小伙伴看不懂就可以往下看我簡(jiǎn)單的解釋?zhuān)?/p>
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) {
//當(dāng)生成復(fù)制View 的時(shí)候壶愤,禁止RecyclerView 滑動(dòng)
return !ryCanScroll;
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//觸發(fā)長(zhǎng)按之后,item的觸摸事件來(lái)到這里了.MotionEvent返回手指移動(dòng)的位置.以及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 產(chǎn)生位移偏差淑倾,好看得出復(fù)制了
startX = location[0]+dp2px(this,30);
startY = location[1]+dp2px(this,60);;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
//獲取移動(dòng)后的坐標(biāo)
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
//拿到手指移動(dòng)距離的大小
int move_bigX = moveX - startX;
int move_bigY = moveY - startY;
//拿到當(dāng)前控件未移動(dòng)的坐標(biāo):只需要計(jì)算該控件離左邊和上邊的距離即可
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://手指抬起來(lái)的同時(shí)判斷是否紅色范圍內(nèi),是則回收該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:
//獲取當(dāng)前按下的坐標(biāo)
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//獲取移動(dòng)后的坐標(biāo)
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
//拿到手指移動(dòng)距離的大小
int move_bigX = moveX - startX;
int move_bigY = moveY - startY;
//拿到當(dāng)前控件未移動(dòng)的坐標(biāo):只需要計(jì)算該控件離左邊和上邊的距離即可
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) {
//長(zhǎng)按
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("復(fù)制:"+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);
}
/**
* 獲取十六進(jìn)制的顏色代碼.例如 "#5A6677"
* 分別取R征椒、G娇哆、B的隨機(jī)值,然后加起來(lái)即可
* 通過(guò)Color.parseColor()轉(zhuǎn)為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 轉(zhuǎn) px
*/
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 用于判斷某個(gè)坐標(biāo)是否在View 范圍內(nèi)
*
* */
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長(zhǎng)按點(diǎn)擊時(shí)生成一個(gè)新的View (看此函數(shù) copyItem(View view,int selectPosition) )
陕靠,是通過(guò)根布局rlView添加到界面中去迂尝,同時(shí)為了方便管理移除,這邊會(huì)把添加copyView 集合list中去剪芥,實(shí)際上這樣寫(xiě)很繁瑣,可以通過(guò)繼承線(xiàn)性布局琴许,將其封裝成一個(gè)類(lèi)這樣比較直觀(guān)税肪。
View 的滑動(dòng)處理事件效果編輯則是通過(guò)onTouchListener,在里面實(shí)現(xiàn)縮放,移動(dòng)榜田,與手指抬起判斷益兄。
注意:這里邊view的移動(dòng)不可通過(guò)view.layout(l,t,r,b) 來(lái)實(shí)現(xiàn),因?yàn)楫?dāng)父組件rlView.addView(View v)時(shí)箭券,都會(huì)將重置子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:
//獲取當(dāng)前按下的坐標(biāo)
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//獲取移動(dòng)后的坐標(biāo)
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
//拿到手指移動(dòng)距離的大小
int move_bigX = moveX - startX;
int move_bigY = moveY - startY;
//拿到當(dāng)前控件未移動(dòng)的坐標(biāo):只需要計(jì)算該控件離左邊和上邊的距離即可
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;
}
}
關(guān)于RecyclerView 與生成的View 在觸摸事件上的沖突净捅,這邊著重要解決的問(wèn)題就是長(zhǎng)按時(shí)(沒(méi)有松開(kāi)),實(shí)際上事件分發(fā)到我們的ryTool辩块,但是滑動(dòng)的時(shí)候卻是移動(dòng)產(chǎn)生在ryTool 上面的view蛔六, 這里我們要做的兩點(diǎn)就是
1.長(zhǎng)按時(shí),將禁止 RecyclerView 的滾動(dòng)
這邊通過(guò)設(shè)置 ryCanScroll ,在長(zhǎng)按時(shí)的回調(diào)設(shè)置為false
2.將RecyclerView上產(chǎn)生的任何觸摸事件傳給最新生成的View 移動(dòng),且當(dāng)手指抬起時(shí)废亭,則允許RecyclerView重新滾動(dòng)国章。
至此,整篇文章講解完畢豆村,這次寫(xiě)的也算比較粗糙液兽,講解的也是一些很表面上的東西,深層次的事件分發(fā)也沒(méi)有展開(kāi)講掌动,只算給小伙伴們提供一些思路四啰,若是哪里有誤宁玫,歡迎留言提出,共同進(jìn)步柑晒。