这篇博客主要讲解如何代码实现拖动图标功能。阅读本篇博客之前请先阅读上一篇博客,了解一下事件分发机制的一些要点。
Android新体会(一)通过仿桌面实现图标拖动了解事件分发机制
然后是demo地址https://github.com/huangwanjie/TableImitate/tree/develop
效果图
实现拖动的actvtiy包含一个ViewPager和一个CellDragLayout。ViewPager包含两个fragment,fragment中的布局有一个RecyclerView。CellDragLayout是实现图标移动的类。首先看一下布局activity布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.MainActivity">
<FrameLayout
android:id="@+id/fl_under"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="@+id/vp_cells"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="100dp"/>
</FrameLayout>
<com.hwj.test.tabledrag.celldrag.CellDragLayout
android:id="@+id/cdl_drag"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
然后看fragment布局
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_cells"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
最后RecyclerView的item的布局:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp">
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/iv_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:scaleType="fitXY"/>
</FrameLayout>
说一下实现的大概思路:
1.长按RecyclerView中的item,然后在CellDragLayout上新建一个ImageView,把item的bitmap给到ImageView,同时将item设为View.INVISIBLE.
2.让ImageView随着CellDragLayout接收到的Move事件而移动。
3.松开手后remove掉ImageView,同时item设为View.VISIBLE。
这里的重点主要是第一步和第二步的衔接。上一篇博客中提到,在ViewGroup事件分发中,Down事件是整个事件的开始,消费了Down时间的view,ViewGroup在不拦截的情况下会把后续的事件都交给该view去处理,不再访问分发到其它子view,也就是在事件分发的流程中消费事件都是同一个View。所以在正常事件分发流程中,item获取了并消费事件之后,RelativeLayout不可能在本次触摸事件结束之后将事件在分发给CellDraglayout。因此实际上我们需要解决的问题是,CellDragLayout和item都必须能接收和消费到相同的事件。由于CellDragLayout布局中的最上层,所以它能很容易的获取到事件,并进行消费,但是消费过后RelativeLayout不会把事件间接地分发给item了。所以按下的时候,先判断该位置是否在ViewPager所在的区域中,如果是,CellDragLayout在onTouchEvent(MotionEvent event)中消费事件,同时需要主动地将事件分发给activity_main.xml中的FrameLayout,调用FrameLayout的dispatchTouchEvent(MotionEvent event)方法,把CellDragLayout接收到的event传进去,item自然就可以获取到事件了。
下面把CellDragLayout中重写的onTouchEvent()方法贴出来
public boolean onTouchEvent(MotionEvent event) {
Log.i("onTouchEvent", event.toString());
if (actionDownRect == null){ //1
return false;
}
currentTouchX = event.getX();
currentTouchY = event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN){ //2
if (!actionDownRect.contains((int) event.getRawX(), (int) event.getRawY())) {
return false;
}
}
if (moveView == null){ //3
deleteCellHelper.transferTouchEvent(event);
return true;
}
switch (event.getAction()){
case MotionEvent.ACTION_MOVE: //4
moveView.setTranslationX(currentTouchX - (moveView.getX() + moveView.getWidth() / 2) + moveView.getTranslationX());
moveView.setTranslationY(currentTouchY - (moveView.getY() + moveView.getHeight() / 2) + moveView.getTranslationY());
onMoveState((int)(event.getX()), (int)(event.getY()));
break;
case MotionEvent.ACTION_UP:
onUpState((int)(event.getX()), (int)(event.getY()));
break;
default:
break;
}
return true;
}
注释1中的actionDownRect是ViewPager相对于CellDragLayout的区域,要是没有设置该区域就不消费事件。注释2中判断触点是否在actionDownRect之中,不在就不消费事件。注释3中moveView是在CellDragLayout上移动的控件,在item触发长按是创建,为空表示还没有触发,侧通过一个辅助类DeleteCellHelper调用transferTouchEvent(MotionEvent event),把事件传递出去。注释4是在长按时候moveView创建之后执行,使moveView根据触点移动。下面看看DeleteCellHelper类部分代码:
public class DeleteCellHelper<T> {
private CellDragLayout dragLayout;
private View underLayout;
private T content;
View view;
public DeleteCellHelper(CellDragLayout dragLayout,View underLayout){
this.dragLayout = dragLayout;
this.underLayout = underLayout;
dragLayout.setDeleteCellHelper(this);
}
public void setDownActionRect(Rect rect){
dragLayout.setActionDownRect(rect);
}
public void onAction(View view, T content){
this.view = view;
this.content = content;
viewCopy(view);
view.setVisibility(View.INVISIBLE);
}
private void viewCopy(View view){
Bitmap bitmap = ViewUtil.getViewBitmap(view);
ImageView ivCopy = new ImageView(view.getContext());
ivCopy.setClickable(false);
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
LayoutParams lp = new LayoutParams(view.getWidth(), view.getHeight());
lp.leftMargin = rect.left;
lp.topMargin = rect.top - (int) dragLayout.getX();
ivCopy.setLayoutParams(lp);
ivCopy.setImageBitmap(bitmap);
dragLayout.addMoveView(ivCopy, bitmap);
}
public void cancle(){
view.setVisibility(View.VISIBLE);
if (deleteListenter != null){
deleteListenter.onCancle();
}
}
public void deleted(){
if (deleteListenter != null){
deleteListenter.onDeteled(content);
}
}
private DeleteListenter deleteListenter;
public void setDeleteListenter(DeleteListenter deleteListenter){
this.deleteListenter = deleteListenter;
}
public void transferTouchEvent(MotionEvent event){
underLayout.dispatchTouchEvent(event);
}
public interface DeleteListenter<T>{
void onCancle();
void onDeteled(T content);
}
}
underLayout是demo中的activity_main.xml的FrameLayout,创建DeleteCellHelper时把该对象传进去。item在长按之后调用onAtion(View v, T content)把item中的view和相关内容content传进来(content现在还没有用,下一篇将介绍它的用处),进在draglayout中新建用于移动的ImageView。DeleteCellHelper类主要的作用是让CellDragLayout和其它的控件建立联系,充当桥梁的作用。
还需要注意的一点是demo中的underLayout和draglayout在布局中的大小和位置是一样的,因为draglayout分发给underLayout的event是相对于underLayout的,里面包含的位置信息event.getX()和getY()也是相对于dragLayout的,要是underLayout和draglayout在布局中位置不一致需要则经过转换,不然触点在underLayout会表现的不准确。
如果需要转换要怎么写呢?我们可以参考一下ViewGroup中的dispatchTransformedTouchEvent方法,里面有这样一段转换得代码:
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
计算完偏移量之后通过event.offsetLocation进行校正,然后在传给子view,最后再更正回来。
好了本篇博客到此结束,下篇分享删除的动画特效。