背景
需求:需要做一个仿支付宝“我的应用页面的功能”,编辑状态下支持跨两栏拖拽。但由于Android中RecyclerView无法支持跨控件拖拽,所以就想到自定义控件来解决问题。
分析
首先,需要将两栏的视图“首页展示”,中间的分割视图,置顶应用放在一个可拖拽的控件中。
该控件需要支持的功能
- 不满首页数目时,自动添加到首页展示中
- 当首页数目满时,如果下一个刚好添加到置顶应用中,将先添加分割视图,然后再添加置顶应用
- 当置顶应用已有数目时,自动添加到置顶应用中
- 移除首页展示的数据时,后面自动向前补齐,并会将置顶应用中首个自动补齐
- 移除置顶应用时,后面的自动补齐
- 移除后,如果数目刚好等于首页数目,将隐藏分割视图
- 长按触发拖拽
- 点击触发删除
- 向前拖拽时,数据自动向后补齐,当前拖拽的数据插入到触发交换数据的位置
- 向后拖拽时,数据自动向前补齐,当前拖拽的数据插入到触发交换数据的位置
实现
1.前提
- 拖拽控件的父视图有且只有一个ScrollView
- 悬浮视图在ScrollView上方
- 悬浮视图的父控件必须是FrameLayout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--悬浮视图的父布局时FrameLayout-->
<FrameLayout
android:id="@+id/drag_layout_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<!--父视图有且只有一个ScrollView-->
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#DDDDDD">
<com.hhsjtest.testjump2.impl.CustomFrameLayout
android:id="@+id/custom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
<!--悬浮视图在ScrollView的上方-->
<com.hhsjtest.testjump2.impl.DefaultDragItemView
android:id="@+id/float_view_2"
android:layout_width="270px"
android:layout_height="250px"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/mode_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="编辑" />
<Button
android:id="@+id/mode_normal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="正常" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/add_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="添加1条数据" />
<Button
android:id="@+id/add_15"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="添加15条数据" />
<Button
android:id="@+id/remove_one"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="移除1条数据" />
</LinearLayout>
</LinearLayout>
2.触发拖拽的延时任务
当任务被执行时:
1. 标记正在拖拽
2. 展示悬浮窗
3.dispatchTouchEvent
1. ACTION_DOWN,记录按下的坐标,计算按下时的视图位置,记录按下的时间,发送长按触发拖拽的延时任务,请求父视图不要拦截时间
2. ACTION_MOVE,如果时间内超过阈值时,将移除长按拖拽的延时任务,并标记已经取消长按,并请求父视图拦截
3. ACTION_MOVE,小于阈值时,不做任何处理
4. ACTION_UP 还未触发长按,将移除长按事件,并标记长按事件已经取消;
5. ACTION_UP 长按事件没有取消时,将取消长按拖拽的任务,并标记长按事件已经取消;
6. 最终所有的事件走super.dispathTouchEvent() 并返回值
4.onInterceptTouchEvent
1. ACTION_MOVE 滑动超过阈值直接拦截(一般ScrollView会先拦截,如果ScrollView被告诉不拦截,它将能成功拦截) return true
2. ACTION_MOVE 正在拖拽时直接拦截 return true
3. ACTION_UP 正在拖拽视直接拦截(为了保证事件能走进onTouchEvent,在onTouchEvent的UP时候停止拖拽任务),return true
4. ACTION_UP 点击事件的时候不能拦截(点击事件是无法触发长按,也无法把状态标记为正在拖拽,所以不拦截)
5. 其他走super.onInterceptTouchEvent() 并返回值
5.onTouchEvent
1. ACTION_DOWN, return true
2. ACTION_MOVE, 正在拖拽时将根据点位 计算悬浮窗的位置,并更新
3. ACTION_MOVE, (当ScrollView超屏时)滑动触发父视图ScrollView的自动滚动(手动调用ScrollView的scrollBy)
4. ACTION_MOVE,松开滚动或者小于滚动阈,取消ScrollView的自动滚动
5. ACTION_MOVE, 超过滑动阈值时,如果已经取消长按拖拽,将请求父控件拦截,return false,
6. ACTION_MOVE,超过滑动阈值时,如果没有取消拖拽,将执行拖拽(1,展示悬浮窗,2,隐藏当前点位的视图,3,移动悬浮窗),并根据拖拽的点位,时刻计算当前位置,判断是否满足数据交换算法,满足将执行数据交换,并在动画结束的时候,将数据展示在视图上。记录当前点位,将返回true
7. ACTION_UP 如果取消了长按拖拽,将停止拖拽(1,隐藏悬浮窗,2,显示当前拖拽的视图)
8. ACTION_UP 最终走super.onTouchEvent() 并返回值
6.Item的点击
1. 点击item 由于点击操作,时间很短,所以在UP的时候判断没有长按的触发为false,从而取消长按事件触发的任务。
2. 同时UP不应该拦截点击事件,因为点击事件的UP必须由子视图的onTouchEvent处理。
7.抽象
数据
public interface DragItemInfo<D> {
public D getData();
/**
* 拖动状态
*
* @return
*/
public abstract boolean isDrag();
/**
* 设置拖动状态
*
* @param isDrag
*/
public abstract void setDrag(boolean isDrag);
/**
* 可见状态
*
* @return
*/
public abstract boolean isVisible();
/**
* 设置可见状态
*
* @param visible
*/
public abstract void setVisible(boolean visible);
}
Item视图
public abstract class DragItemView<D extends DragItemInfo<D>> extends FrameLayout {
protected D dragItemInfo;
private View container;
private boolean isFloatView;
public DragItemView(@NonNull Context context) {
this(context, null);
}
public DragItemView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DragItemView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
/**
* 必须重载
*/
protected void initView() {
container = View.inflate(getContext(), bindLayout(), this);
createItem(this, dragItemInfo);
}
/**
* 绑定布局id
*
* @param
*/
public abstract int bindLayout();
/**
* 创建item视图
* @param view
* @param dragItemInfo
*/
public abstract void createItem(DragItemView<D> view, D dragItemInfo);
/**
* 设为可见
*/
public abstract void visibleItem();
/**
* 设为不可见
*/
public abstract void inVisibleItem();
/**
* 删除按钮
*
* @return
*/
public abstract View getDeleteView();
/**
* 添加按钮
*
* @return
*/
public abstract View getAddView();
/**
* 视图inflate的布局
* @return
*/
public View getContainer() {
return container;
}
/**
* 作为悬浮窗视图
* @param isFloatView
*/
public void setAsFloatView(boolean isFloatView) {
this.isFloatView = isFloatView;
}
/**
* 是否是悬浮窗视图
* @return
*/
public boolean isFloatViewView() {
return isFloatView;
}
/**
* 更新视图数据
* @param dragItemInfo
*/
public abstract void updateData(D dragItemInfo);
/**
* 设置编辑模式
* @param edit
*/
public abstract void setEdit(boolean edit);
}
视图创建工厂
public interface DragItemFactory<D extends DragItemInfo<D>, V extends DragItemView<D>> {
V createDragItem(D dragInfo);
}
重要方法
public abstract class AlipayDragFrameLayout<D extends DragItemInfo<D>, V extends DragItemView<D>>
extends FrameLayout {
//编辑模式,才可以拖拽
public void setEditMode(boolean b);
//debug模式,将展示日志
public void setDebug(boolean isDebug);
//添加数据
public void setDataList(List<D> newDataList);
//添加一条数据
public void addData(D info);
//移除当前视图位置的数据
public void removeViewData(int viewPosition);
//移除最后一条数据
public void removeLastData();
//设置顶部的行数和列数
public void setRowColumnNumber(int rowNumber, int columnNumber);
//返回所有的数据
public List<D> getDataList();
//需要实现,返回悬浮视图的父控件,FrameLayout
public abstract View getDragShadowParent();
}
视图拖拽监听
public interface DragFloatListener<D extends DragItemInfo<D>> {
/**
* 展示拖拽悬浮视图
*
* @param data 数据
* @param position 位置
* @param left 视图的左边有坐标
* @param top 视图的右边有坐标
*/
void showFloatView(D data, int position, int left, int top);
/**
* 移动拖拽悬浮视图
*
* @param left
* @param top
*/
void updateFloatView(int left, int top);
/**
* 隐藏拖拽悬浮视图
*/
void hideFloatView();
}
数据移除监听
public interface OnDataEditListener<D extends DragItemInfo<D>> {
void onDeleted(D dragInfo);
}
仓库地址
https://gitee.com/hhsjdesign/alipaydragview.git
前面忘了开放代码权限,现在可以点击下载了
欢迎交流
QQ 2423458506