上篇博客分享了一个实现ListView中item交换动画的控件(戳这里查看),但是有些情况下我们的需求比这种效果要复杂。比如说需要手动拖拽item来完成item交换的交互。像这样:
还有这样:
这次分享的控件就实现这样的功能,下面开始正文。
先说实现item拖拽功能的DragListView。上代码:
public class DragListView extends ListView {
/**
* 速度模板,影响视图移动时的速度变化
*
* MODE_LINEAR // 线性变化模式
* MODE_ACCELERATE // 加速模式
* MODE_DECELERATE // 减速模式
* MODE_ACCELERATE_DECELERATE // 先加速后加速模式
*/
public static final int MODE_LINEAR = 0x001;
public static final int MODE_ACCELERATE = 0x002;
public static final int MODE_DECELERATE = 0x003;
public static final int MODE_ACCELERATE_DECELERATE = 0x004;
private Context context;
// 拖动时的视图
private View dragView;
private WindowManager windowManager;
private WindowManager.LayoutParams windowLayoutParams;
private BaseDragAdapter adapter;
/**
* 可设置选项
*/
// 移动动画储持续时间,单位毫秒
private long duration = 300;
// 速度模板
private int speedMode = MODE_ACCELERATE_DECELERATE;
// 自动滚动的速度
private int scrollSpeed = 50;
/**
* 运行参数
*/
// 拖动块的原始坐标
private int originalPosition = -1;
// 拖动块当前所在坐标
private int currentPosition = -1;
// 用于记录上次点击事件MotionEvent.getX();
private int lastX;
// 用于记录上次点击事件MotionEvent.getY();
private int lastY;
// 用于记录上次点击事件MotionEvent.getRawX();
private int lastRawX;
// 用于记录上次点击事件MotionEvent.getRawY();
private int lastRawY;
// 拖动块中心点x坐标,用于判断拖动块所处的列表位置
private int dragCenterX;
// 拖动块中心点y坐标,用于判断拖动块所处的列表位置
private int dragCenterY;
// 滑动上边界,拖动块中心超过该边界时列表自动向下滑动
private int upScrollBorder;
// 滑动下边界,拖动块中心超过该边界时列表自动向上滑动
private int downScrollBorder;
// 状态栏高度
private int statusHeight;
// 拖动时的列表刷新标识符
private boolean dragRefresh;
// 拖动锁定标记,为false时选中块可被拖动
private boolean dragLock = true;
// 动画列表,存放当前屏幕上正在播放的所有滑动动画的动画对象
private ArrayList animatorList;
// 视图列表,存放当前屏幕上正在播放的所有滑动动画的视图对象
private ArrayList dragViews;
/**
* 可监听接口
*/
// 拖动块视图对象生成器,可通过设置该接口自定义一个拖动视图的样式,不设置时会有默认实现
private DragViewCreator dragViewCreator;
// 拖动监听接口,拖动开始和结束时会在该接口回调
private OnDragingListener dragingListener;
// 当前拖动目标位置改变时,每次改变都会在该接口回调
private OnDragTargetChangedListener targetChangedListener;
// 内部接口,动画观察者,滑动动画结束是回调
private AnimatorObserver animatorObserver;
private Handler handler = new Handler();
// 列表自动滚动线程
private Runnable scrollRunnable = new Runnable() {
@Override
public void run() {
int scrollY;
// 滚动到顶或到底时停止滚动
if (getFirstVisiblePosition() == 0 || getLastVisiblePosition() == getCount() - 1) {
handler.removeCallbacks(scrollRunnable);
}
// 触控点y坐标超过上边界时,出发列表自动向下滚动
if (lastY > upScrollBorder) {
scrollY = scrollSpeed;
handler.postDelayed(scrollRunnable, 25);
}
// 触控点y坐标超过下边界时,出发列表自动向上滚动
else if (lastY < downScrollBorder) {
scrollY = -scrollSpeed;
handler.postDelayed(scrollRunnable, 25);
}
// // 触控点y坐标处于上下边界之间时,停止滚动
else {
scrollY = 0;
handler.removeCallbacks(scrollRunnable);
}
smoothScrollBy(scrollY, 10);
}
};
public DragListView(Context context) {
super(context);
init(context);
}
public DragListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DragListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化方法
*
* @param context
*/
private void init(Context context) {
this.context = context;
statusHeight = getStatusHeight();
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
animatorList = new ArrayList<>();
dragViews = new ArrayList<>();
// 拖动块视图对象生成器的默认实现,返回一个与被拖动项外观一致的ImageView
dragViewCreator = new DragViewCreator() {
@Override
public View createDragView(int width, int height, Bitmap viewCache) {
ImageView imageView = new ImageView(DragListView.this.context);
imageView.setImageBitmap(viewCache);
return imageView;
}
};
}
@Override
public boolean dispatchTouchEvent(MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
downScrollBorder = getHeight() / 5;
upScrollBorder = getHeight() * 4 / 5;
// 手指按下时记录相关坐标
lastX = (int) motionEvent.getX();
lastY = (int) motionEvent.getY();
lastRawX = (int) motionEvent.getRawX();
lastRawY = (int) motionEvent.getRawY();
currentPosition = pointToPosition(lastRawX, lastRawY);
if (currentPosition == AdapterView.INVALID_POSITION || !adapter.isDragAvailable(currentPosition)) {
return true;
}
originalPosition = currentPosition;
break;
}
return super.dispatchTouchEvent(motionEvent);
}
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_MOVE:
if (!dragLock) {
int currentRawX = (int) motionEvent.getRawX();
int currentRawY = (int) motionEvent.getRawY();
if (dragView == null) {
createDragImageView(getChildAt(pointToPosition(lastRawX, lastRawY) - getFirstVisiblePosition()));
getChildAt(originalPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE);
if (dragingListener != null) {
dragingListener.onStart(originalPosition);
}
}
drag(currentRawY - lastRawY);
if (dragingListener != null) {
dragingListener.onDraging((int) motionEvent.getX(), (int) motionEvent.getY(), currentRawX, currentRawY);
}
int position = pointToPosition(dragCenterX, dragCenterY);
// 满足交换条件时让目标位置的原有视图上滑或下滑
if (position != AdapterView.INVALID_POSITION && currentPosition != position && adapter.isDragAvailable(position)) {
translation(position, currentPosition);
currentPosition = position;
if (targetChangedListener != null) {
targetChangedListener.onTargetChanged(currentPosition);
}
}
// 更新点击位置
lastX = (int) motionEvent.getX();
lastY = (int) motionEvent.getY();
lastRawX = currentRawX;
lastRawY = currentRawY;
// 返回true消耗掉这次点击事件,防止ListView本身接收到这次点击事件后触发滚动
return true;
}
break;
case MotionEvent.ACTION_UP:
// 手指抬起时,如果所有滑动动画都已播放完毕,则直接执行拖动完成逻辑
if (animatorList.size() == 0) {
resetDataAndView();
if (dragingListener != null) {
dragingListener.onFinish(currentPosition);
}
}
// 如果还有未播放完成的滑动动画,则注册观察者,延时执行拖动完成逻辑
else {
animatorObserver = new AnimatorObserver() {
@Override
public void onAllAnimatorFinish() {
resetDataAndView();
if (dragingListener != null) {
dragingListener.onFinish(currentPosition);
}
}
};
}
break;
}
return super.onTouchEvent(motionEvent);
}
/**
* 创建拖动块视图方法
*
* @param view 被拖动位置的视图对象
*/
private void createDragImageView(View view) {
if (view == null) {
return;
}
removeDragImageView();
int[] location = new int[2];
view.getLocationOnScreen(location);
view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.destroyDrawingCache();
windowLayoutParams = new WindowManager.LayoutParams();
windowLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
windowLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
windowLayoutParams.format = PixelFormat.TRANSPARENT;
windowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
windowLayoutParams.x = location[0];
windowLayoutParams.y = location[1] - statusHeight;
windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
dragCenterX = windowLayoutParams.x + view.getWidth() / 2;
dragCenterY = windowLayoutParams.y + view.getHeight() / 2;
dragView = dragViewCreator.createDragView(view.getWidth(), view.getHeight(), bitmap);
if (dragView == null) {
throw new NullPointerException("dragView can not be null");
} else {
windowManager.addView(dragView, windowLayoutParams);
}
}
/**
* 移除拖动视图方法
*/
private void removeDragImageView() {
if (dragView != null && windowManager != null) {
windowManager.removeView(dragView);
dragView = null;
windowLayoutParams = null;
}
}
/**
* 拖动方法
*
* @param dy
*/
private void drag(int dy) {
dragCenterY += dy;
windowLayoutParams.y += dy;
windowManager.updateViewLayout(dragView, windowLayoutParams);
handler.post(scrollRunnable);
}
/**
* 移动指定位置视图方法
*
* @param fromPosition 移动起始位置
* @param toPosition 移动目标位置
*/
private void translation(int fromPosition, int toPosition) {
View fromView = getChildAt(fromPosition - getFirstVisiblePosition());
View toView = getChildAt(toPosition - getFirstVisiblePosition());
if (fromView == null || toView == null) {
return;
}
float distance = (toView.getY() - toView.getTranslationY()) - (fromView.getY() - fromView.getTranslationY());
ObjectAnimator animator = ObjectAnimator.ofFloat(fromView, "translationY", 0, distance);
animator.setDuration(duration);
animator.setInterpolator(getAnimatorInterpolator());
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorList.remove(animation);
// 所有滑动动画都播放结束时,执行相关操作
if (animatorList.size() == 0) {
// 重置所有滑动过的视图的translateY,避免列表刷新后视图重叠
resetTranslate(dragViews);
dragViews.clear();
adapter.exchangeData(originalPosition, currentPosition);
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (dragRefresh) {
removeOnLayoutChangeListener(this);
resetChildVisibility();
getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE);
originalPosition = currentPosition;
dragRefresh = false;
if (animatorObserver != null) {
animatorObserver.onAllAnimatorFinish();
animatorObserver = null;
}
}
}
});
dragRefresh = true;
adapter.notifyDataSetChanged();
}
}
});
animatorList.add(animator);
dragViews.add(fromView);
animator.start();
}
/**
* 重置列表所有项的可见性方法
*/
private void resetChildVisibility() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child != null) {
child.setVisibility(VISIBLE);
}
}
}
/**
* 重置指定视图的translateY属性方法
*
* @param list
*/
private void resetTranslate(ArrayList list) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i) != null) {
list.get(i).setTranslationY(0);
}
}
}
/**
* 重置数据和视图相关数据方法
*/
private void resetDataAndView() {
if (currentPosition == -1) {
return;
}
getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE);
originalPosition = -1;
currentPosition = -1;
handler.removeCallbacks(scrollRunnable);
removeDragImageView();
}
@Override
public void setAdapter(ListAdapter adapter) {
if (adapter instanceof BaseDragAdapter) {
this.adapter = (BaseDragAdapter) adapter;
super.setAdapter(adapter);
} else {
throw new IllegalStateException("the adapter must extends BaseDragAdapter");
}
}
/**
* 根据速度模板创建动画迭代器
*
* @return
*/
private Interpolator getAnimatorInterpolator() {
switch (speedMode) {
case MODE_LINEAR:
return new LinearInterpolator();
case MODE_ACCELERATE:
return new AccelerateInterpolator();
case MODE_DECELERATE:
return new DecelerateInterpolator();
case MODE_ACCELERATE_DECELERATE:
return new AccelerateDecelerateInterpolator();
default:
return null;
}
}
/**
* 拖动解锁方法,调用者需手动调用该方法后才能开启列表拖动功能
*/
public void unlockDrag() {
dragLock = false;
}
/**
* 拖动锁定方法,调用者调用该方法后关闭列表拖动功能
*/
public void lockDrag() {
dragLock = true;
}
/**
* 设置移动动画持续时间
*
* @param duration 时间,单位毫秒
*/
public void setDuration(long duration) {
this.duration = duration;
}
/**
* 设置速度模式,可选项:
* MODE_LINEAR 线性变化模式
* MODE_ACCELERATE 加速模式
* MODE_DECELERATE 减速模式
* MODE_ACCELERATE_DECELERATE 先加速后加速模式
*
* @param speedMode
*/
public void setSpeedMode(int speedMode) {
this.speedMode = speedMode;
}
/**
* 设置自动滚动速度
*
* @param scrollSpeed 速度,单位:dp/10ms
*/
public void setScrollSpeed(int scrollSpeed) {
this.scrollSpeed = scrollSpeed;
}
/**
* 设置拖动块视图对象生成器方法
*
* @param creator
*/
public void setDragViewCreator(DragViewCreator creator) {
if (creator == null) {
return;
}
this.dragViewCreator = creator;
}
/**
* 设置拖动监听接口
*
* @param dragingListener
*/
public void setOnDragingListener(OnDragingListener dragingListener) {
this.dragingListener = dragingListener;
}
/**
* 设置拖动目标位置改变监听接口
*
* @param targetChangedListener
*/
public void setOnDragTargetChangedListener(OnDragTargetChangedListener targetChangedListener) {
this.targetChangedListener = targetChangedListener;
}
private int getStatusHeight() {
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
return context.getResources().getDimensionPixelSize(resourceId);
}
return 0;
}
/**
* 动画观察者
*/
private interface AnimatorObserver {
/**
* 滑动动画播放结束时回调
*/
void onAllAnimatorFinish();
}
/**
* 拖动块视图对象生成器
*/
public interface DragViewCreator {
/**
* 创建拖动块视图对象方法,可通过实现该方法自定义拖动块样式
*/
View createDragView(int width, int height, Bitmap viewCache);
}
/**
* 拖动监听接口
*/
public interface OnDragingListener {
/**
* 拖动开始时回调
*
* @param startPosition 拖动起始坐标
*/
void onStart(int startPosition);
/**
* 拖动过程中回调
*
* @param x 触控点相对ListView的x坐标
* @param y 触控点相对ListView的y坐标
* @param rawX 触控点相对屏幕的x坐标
* @param rawY 触控点相对屏幕的y坐标
*/
void onDraging(int x, int y, int rawX, int rawY);
/**
* 拖动结束时回调
*
* @param finalPosition 拖动终点坐标
*/
void onFinish(int finalPosition);
}
/**
* 拖动目标位置改变监听接口
*/
public interface OnDragTargetChangedListener {
/**
* 拖动过程中,每次目标位置改变,会在该方法回调
*
* @param targetPosition 拖动目标位置坐标
*/
void onTargetChanged(int targetPosition);
}
}
简单讲一下实现原理。手指按下时通过ListView的getChildAt方法获得按下位置的item并获取其视图缓存,也就是这句话:
view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.destroyDrawingCache();
然后新建一个View把这个缓存塞进去并置于屏幕之上,并隐藏原来的item,让人看起来就好像是item被“拽”了下来,也就是这句话:
windowManager.addView(dragView, windowLayoutParams);
手指移动时,改变这个View的LayoutParams的y坐标值,让它跟随手指移动,也就是这两句话:
windowLayoutParams.y += dy;
windowManager.updateViewLayout(dragView, windowLayoutParams);
拖拽过程中,当判定交换行为发生时,用一个属性动画不断改变目标item的translationY属性来实现交换效果,也就是这句话:
ObjectAnimator animator = ObjectAnimator.ofFloat(fromView, "translationY", 0, distance);
具体代码大家可以看注释,应该写得比较清楚了。
要特别说明的是,DragListView的setAdapter方法被重写了,只接收BaseDragAdapter的继承类,BaseDragAdapter长这样:
public abstract class BaseDragAdapter extends BaseAdapter {
/**
* 调用者需实现该方法,返回列表的所有数据集合
*
* @return
*/
public abstract List getDataList();
/**
* 调用者可实现该方法自定义某一项是否可被拖动
*
* @param position
* @return
*/
public abstract boolean isDragAvailable(int position);
/**
* 实现数据交换方法
*
* @param oldPosition
* @param newPosition
*/
public void exchangeData(int oldPosition, int newPosition) {
List list = getDataList();
if (list == null) {
return;
}
Object temp = list.get(oldPosition);
if (oldPosition < newPosition) {
for (int i = oldPosition; i < newPosition; i++) {
Collections.swap(list, i, i + 1);
}
} else if (oldPosition > newPosition) {
for (int i = oldPosition; i > newPosition; i--) {
Collections.swap(list, i, i - 1);
}
}
list.set(newPosition, temp);
}
}
BaseDragAdapter的目的是替调用者封装一些必要的操作,它给普通的BaseAdapter增加了两个需要实现的抽象方法:getDataList()和isDragAvailable()。getDataList()返回ListView 的数据列表即可,isDragAvailable()用来让调用者决定某个item是否可被拖拽,比如说需求是列表的第一项不可被拖拽,只需要实现isDragAvailable方法,在position=0时返回false即可。
然后就可以使用了。先写一个item的布局:
再简单写一个适配器TestListViewAdapter继承自BaseDragAdapter:
public class TestListViewAdapter extends BaseDragAdapter {
private Context context;
private int resourceId;
private ArrayList list;
private Vibrator vibrator;
private OnItemLongClickListener listener;
public TestListViewAdapter(Context context, int resourceId, ArrayList list) {
this.context = context;
this.resourceId = resourceId;
this.list = list;
this.vibrator = (Vibrator) context.getSystemService(context.VIBRATOR_SERVICE);
}
@Override
public List getDataList() {
return list;
}
@Override
public boolean isDragAvailable(int position) {
return true;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, null);
viewHolder = new ViewHolder();
viewHolder.itemLayout = view.findViewById(R.id.item_layout);
viewHolder.contentTextView = view.findViewById(R.id.content_textview);
viewHolder.dividerLine = view.findViewById(R.id.divider_line);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.contentTextView.setText(list.get(position));
viewHolder.dividerLine.setVisibility(position != list.size() - 1 ? View.VISIBLE : View.INVISIBLE);
viewHolder.itemLayout.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
vibrator.vibrate(100);
if (listener != null) {
listener.onItemLongClick(position);
}
return false;
}
});
return view;
}
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
this.listener = listener;
}
public interface OnItemLongClickListener {
void onItemLongClick(int position);
}
class ViewHolder {
LinearLayout itemLayout;
TextView contentTextView;
View dividerLine;
}
}
代码很简单,就不多说了。
最后就可以使用了,Activity里这样写:
private DragListView dragListView;
private TestListViewAdapter adapter;
private ArrayList list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drag_listview_test);
initData();
initView();
}
private void initData() {
list = new ArrayList<>();
for (int i = 1; i <= 40; i++) {
list.add("我是第" + i + "条数据");
}
}
private void initView() {
dragListView = findViewById(R.id.drag_listview);
dragListView.setOnDragingListener(new DragListView.OnDragingListener() {
@Override
public void onStart(int startPosition) {
}
@Override
public void onDraging(int x, int y, int rawX, int rawY) {
}
@Override
public void onFinish(int finalPosition) {
dragListView.lockDrag();
}
});
adapter = new TestListViewAdapter(this, R.layout.item_test_listview, list);
adapter.setOnItemLongClickListener(new TestListViewAdapter.OnItemLongClickListener() {
@Override
public void onItemLongClick(int position) {
dragListView.unlockDrag();
}
});
dragListView.setAdapter(adapter);
}
用法和普通的ListView一样,通过调用unlockDrag()来解锁拖动(示例代码中通过长按操作来解锁),通过调用lockDrag()方法来锁定拖动。之后还可以通过设置OnDragingListener来监听拖拽过程。开启和锁定拖动操作的条件视项目需求而定,比如长安开启,或者按编辑按钮开启等等。
最后运行一下就可以看到开头的效果了。
控件支持自定义拖拽View的样式。可以通过setDragViewCreator()方法来实现。比如说我想给拖拽的View加一个高亮效果,就可以这样写:
dragListView.setDragViewCreator(new DragListView.DragViewCreator() {
@Override
public View createDragView(int width, int height, Bitmap viewCache) {
RelativeLayout layout = new RelativeLayout(DragListViewTestActivity.this);
ImageView imageView = new ImageView(DragListViewTestActivity.this);
imageView.setImageBitmap(viewCache);
layout.addView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));
View view = new View(DragListViewTestActivity.this);
view.setBackground(getDrawable(R.drawable.edging_red));
layout.addView(view, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));
return layout;
}
});
其中高亮的资源edging_red.xml长这样:
代码很简单,就是新建一个Layout,里面放一张图片,再在之上加一层高亮遮罩,并将这个layout返回给DragViewCreator接口即可。运行一下看一下效果:
同样的原理再写一个支持item拖拽的GridView,上源码:
public class DragGridView extends GridView {
/**
* 速度模板,影响视图移动时的速度变化
*
* MODE_LINEAR // 线性变化模式
* MODE_ACCELERATE // 加速模式
* MODE_DECELERATE // 减速模式
* MODE_ACCELERATE_DECELERATE // 先加速后加速模式
*/
public static final int MODE_LINEAR = 0x001;
public static final int MODE_ACCELERATE = 0x002;
public static final int MODE_DECELERATE = 0x003;
public static final int MODE_ACCELERATE_DECELERATE = 0x004;
private Context context;
// 拖动时的视图
private View dragView;
private WindowManager windowManager;
private WindowManager.LayoutParams windowLayoutParams;
private BaseDragAdapter adapter;
/**
* 可设置选项
*/
// 移动动画储持续时间,单位毫秒
private long duration = 300;
// 速度模板
private int speedMode = MODE_ACCELERATE_DECELERATE;
// 自动滚动的速度
private int scrollSpeed = 50;
/**
* 运行参数
*/
// 拖动块的原始坐标
private int originalPosition = -1;
// 拖动块当前所在坐标
private int currentPosition = -1;
// 用于记录上次点击事件MotionEvent.getX();
private int lastX;
// 用于记录上次点击事件MotionEvent.getY();
private int lastY;
// 用于记录上次点击事件MotionEvent.getRawX();
private int lastRawX;
// 用于记录上次点击事件MotionEvent.getRawY();
private int lastRawY;
// 拖动块中心点x坐标,用于判断拖动块所处的列表位置
private int dragCenterX;
// 拖动块中心点y坐标,用于判断拖动块所处的列表位置
private int dragCenterY;
// 滑动上边界,拖动块中心超过该边界时列表自动向下滑动
private int upScrollBorder;
// 滑动下边界,拖动块中心超过该边界时列表自动向上滑动
private int downScrollBorder;
// 状态栏高度
private int statusHeight;
// 拖动时的列表刷新标识符
private boolean dragRefresh;
// 拖动锁定标记,为false时选中块可被拖动
private boolean dragLock = true;
// 动画列表,存放当前屏幕上正在播放的所有滑动动画的动画对象
private ArrayList animatorList;
// 视图列表,存放当前屏幕上正在播放的所有滑动动画的视图对象
private ArrayList dragViews;
/**
* 可监听接口
*/
// 拖动块视图对象生成器,可通过设置该接口自定义一个拖动视图的样式,不设置时会有默认实现
private DragViewCreator dragViewCreator;
// 拖动监听接口,拖动开始和结束时会在该接口回调
private OnDragingListener dragingListener;
// 当前拖动目标位置改变时,每次改变都会在该接口回调
private OnDragTargetChangedListener targetChangedListener;
// 内部接口,动画观察者,滑动动画结束是回调
private AnimatorObserver animatorObserver;
private Handler handler = new Handler();
// 列表自动滚动线程
private Runnable scrollRunnable = new Runnable() {
@Override
public void run() {
int scrollY;
// 滚动到顶或到底时停止滚动
if (getFirstVisiblePosition() == 0 || getLastVisiblePosition() == getCount() - 1) {
handler.removeCallbacks(scrollRunnable);
}
// 触控点y坐标超过上边界时,出发列表自动向下滚动
if (lastY > upScrollBorder) {
scrollY = scrollSpeed;
handler.postDelayed(scrollRunnable, 25);
}
// 触控点y坐标超过下边界时,出发列表自动向上滚动
else if (lastY < downScrollBorder) {
scrollY = -scrollSpeed;
handler.postDelayed(scrollRunnable, 25);
}
// // 触控点y坐标处于上下边界之间时,停止滚动
else {
scrollY = 0;
handler.removeCallbacks(scrollRunnable);
}
smoothScrollBy(scrollY, 10);
}
};
public DragGridView(Context context) {
super(context);
init(context);
}
public DragGridView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化方法
*
* @param context
*/
private void init(Context context) {
this.context = context;
statusHeight = getStatusHeight();
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
animatorList = new ArrayList<>();
dragViews = new ArrayList<>();
// 拖动块视图对象生成器的默认实现,返回一个与被拖动项外观一致的ImageView
dragViewCreator = new DragGridView.DragViewCreator() {
@Override
public View createDragView(int width, int height, Bitmap viewCache) {
ImageView imageView = new ImageView(DragGridView.this.context);
imageView.setImageBitmap(viewCache);
return imageView;
}
};
}
@Override
public boolean dispatchTouchEvent(MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
downScrollBorder = getHeight() / 5;
upScrollBorder = getHeight() * 4 / 5;
// 手指按下时记录相关坐标
lastX = (int) motionEvent.getX();
lastY = (int) motionEvent.getY();
lastRawX = (int) motionEvent.getRawX();
lastRawY = (int) motionEvent.getRawY();
currentPosition = pointToPosition(lastRawX, lastRawY);
if (currentPosition == AdapterView.INVALID_POSITION || !adapter.isDragAvailable(currentPosition)) {
return true;
}
originalPosition = currentPosition;
break;
}
return super.dispatchTouchEvent(motionEvent);
}
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_MOVE:
if (!dragLock) {
int currentRawX = (int) motionEvent.getRawX();
int currentRawY = (int) motionEvent.getRawY();
if (dragView == null) {
createDragImageView(getChildAt(pointToPosition(lastRawX, lastRawY) - getFirstVisiblePosition()));
getChildAt(originalPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE);
if (dragingListener != null) {
dragingListener.onStart(originalPosition);
}
}
drag(currentRawX - lastRawX, currentRawY - lastRawY);
if (dragingListener != null) {
dragingListener.onDraging((int) motionEvent.getX(), (int) motionEvent.getY(), currentRawX, currentRawY);
}
int position = pointToPosition(dragCenterX, dragCenterY);
if (position != AdapterView.INVALID_POSITION
&& currentPosition != position
&& adapter.isDragAvailable(position)
&& animatorList.size() == 0) {
translation(position, currentPosition);
currentPosition = position;
if (targetChangedListener != null) {
targetChangedListener.onTargetChanged(currentPosition);
}
}
// 更新点击位置
lastX = (int) motionEvent.getX();
lastY = (int) motionEvent.getY();
lastRawX = currentRawX;
lastRawY = currentRawY;
// 返回true消耗掉这次点击事件,防止ListView本身接收到这次点击事件后触发滚动
return true;
}
break;
case MotionEvent.ACTION_UP:
// 手指抬起时,如果所有滑动动画都已播放完毕,则直接执行拖动完成逻辑
if (animatorList.size() == 0) {
resetDataAndView();
if (dragingListener != null) {
dragingListener.onFinish(currentPosition);
}
}
// 如果还有未播放完成的滑动动画,则注册观察者,延时执行拖动完成逻辑
else {
animatorObserver = new AnimatorObserver() {
@Override
public void onAllAnimatorFinish() {
resetDataAndView();
if (dragingListener != null) {
dragingListener.onFinish(currentPosition);
}
}
};
}
break;
}
return super.onTouchEvent(motionEvent);
}
/**
* 创建拖动块视图方法
*
* @param view 被拖动位置的视图对象
*/
private void createDragImageView(View view) {
if (view == null) {
return;
}
removeDragImageView();
int[] location = new int[2];
view.getLocationOnScreen(location);
view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.destroyDrawingCache();
windowLayoutParams = new WindowManager.LayoutParams();
windowLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
windowLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
windowLayoutParams.format = PixelFormat.TRANSPARENT;
windowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
windowLayoutParams.x = location[0];
windowLayoutParams.y = location[1] - statusHeight;
windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
dragCenterX = windowLayoutParams.x + view.getWidth() / 2;
dragCenterY = windowLayoutParams.y + view.getHeight() / 2;
dragView = dragViewCreator.createDragView(view.getWidth(), view.getHeight(), bitmap);
if (dragView == null) {
throw new NullPointerException("dragView can not be null");
} else {
windowManager.addView(dragView, windowLayoutParams);
}
}
/**
* 移除拖动视图方法
*/
private void removeDragImageView() {
if (dragView != null && windowManager != null) {
windowManager.removeView(dragView);
dragView = null;
windowLayoutParams = null;
}
}
/**
* 拖动方法
*
* @param dx
* @param dy
*/
private void drag(int dx, int dy) {
dragCenterX += dx;
dragCenterY += dy;
windowLayoutParams.x += dx;
windowLayoutParams.y += dy;
windowManager.updateViewLayout(dragView, windowLayoutParams);
handler.post(scrollRunnable);
}
/**
* 移动指定位置视图方法
*
* @param fromPosition 移动起始位置
* @param toPosition 移动目标位置
*/
private void translation(int fromPosition, int toPosition) {
ArrayList list = new ArrayList<>();
if (toPosition > fromPosition) {
for (int position = fromPosition; position < toPosition; position++) {
View view = getChildAt(position - getFirstVisiblePosition());
dragViews.add(view);
if ((position + 1) % getNumColumns() == 0) {
list.add(createTranslationAnimations(view,
0,
-(view.getWidth() + getVerticalSpacing()) * (getNumColumns() - 1),
0,
view.getHeight() + getHorizontalSpacing()));
} else {
list.add(createTranslationAnimations(view,
0,
view.getWidth() + getVerticalSpacing(),
0,
0));
}
}
} else {
for (int position = fromPosition; position > toPosition; position--) {
View view = getChildAt(position - getFirstVisiblePosition());
dragViews.add(view);
if (position % getNumColumns() == 0) {
list.add(createTranslationAnimations(view,
0,
(view.getWidth() + getVerticalSpacing()) * (getNumColumns() - 1),
0,
-(view.getHeight() + getHorizontalSpacing())));
} else {
list.add(createTranslationAnimations(view,
0,
-(view.getWidth() + getVerticalSpacing()),
0,
0));
}
}
}
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(list);
animatorSet.setDuration(duration);
animatorSet.setInterpolator(getAnimatorInterpolator());
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animatorList.remove(animation);
// 所有滑动动画都播放结束时,执行相关操作
if (animatorList.size() == 0) {
// 重置所有滑动过的视图的translateY,避免列表刷新后视图重叠
resetTranslate(dragViews);
dragViews.clear();
adapter.exchangeData(originalPosition, currentPosition);
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (dragRefresh) {
removeOnLayoutChangeListener(this);
resetChildVisibility();
getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.INVISIBLE);
originalPosition = currentPosition;
dragRefresh = false;
if (animatorObserver != null) {
animatorObserver.onAllAnimatorFinish();
animatorObserver = null;
}
}
}
});
dragRefresh = true;
adapter.notifyDataSetChanged();
}
}
});
animatorList.add(animatorSet);
animatorSet.start();
}
/**
* 生成移动动画方法
*
* @param view 需要移动的视图
* @param startX 移动起始x坐标
* @param endX 移动终点x坐标
* @param startY 移动起始y坐标
* @param endY 移动终点y坐标
* @return
*/
private Animator createTranslationAnimations(View view, float startX, float endX, float startY, float endY) {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", startX, endX);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", startY, endY);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animatorX, animatorY);
return animatorSet;
}
/**
* 重置列表所有项的可见性方法
*/
private void resetChildVisibility() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child != null) {
child.setVisibility(VISIBLE);
}
}
}
/**
* 重置指定视图的translateY属性方法
*
* @param list
*/
private void resetTranslate(ArrayList list) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i) != null) {
list.get(i).setTranslationX(0);
list.get(i).setTranslationY(0);
}
}
}
/**
* 重置数据和视图相关数据方法
*/
private void resetDataAndView() {
if (currentPosition == -1) {
return;
}
getChildAt(currentPosition - getFirstVisiblePosition()).setVisibility(View.VISIBLE);
originalPosition = -1;
currentPosition = -1;
dragLock = true;
handler.removeCallbacks(scrollRunnable);
removeDragImageView();
}
@Override
public void setAdapter(ListAdapter adapter) {
if (adapter instanceof BaseDragAdapter) {
this.adapter = (BaseDragAdapter) adapter;
super.setAdapter(adapter);
} else {
throw new IllegalStateException("the adapter must extends BaseDragAdapter");
}
}
/**
* 根据速度模板创建动画迭代器
*
* @return
*/
private Interpolator getAnimatorInterpolator() {
switch (speedMode) {
case MODE_LINEAR:
return new LinearInterpolator();
case MODE_ACCELERATE:
return new AccelerateInterpolator();
case MODE_DECELERATE:
return new DecelerateInterpolator();
case MODE_ACCELERATE_DECELERATE:
return new AccelerateDecelerateInterpolator();
default:
return null;
}
}
/**
* 拖动解锁方法,调用者需手动调用该方法后才能开启列表拖动功能
*/
public void unlockDrag() {
dragLock = false;
}
/**
* 拖动锁定方法,调用者调用该方法后关闭列表拖动功能
*/
public void lockDrag() {
dragLock = true;
}
/**
* 设置移动动画持续时间
*
* @param duration 时间,单位毫秒
*/
public void setDuration(long duration) {
this.duration = duration;
}
/**
* 设置速度模式,可选项:
* MODE_LINEAR 线性变化模式
* MODE_ACCELERATE 加速模式
* MODE_DECELERATE 减速模式
* MODE_ACCELERATE_DECELERATE 先加速后加速模式
*
* @param speedMode
*/
public void setSpeedMode(int speedMode) {
this.speedMode = speedMode;
}
/**
* 设置自动滚动速度
*
* @param scrollSpeed 速度,单位:dp/10ms
*/
public void setScrollSpeed(int scrollSpeed) {
this.scrollSpeed = scrollSpeed;
}
/**
* 设置拖动块视图对象生成器方法
*
* @param creator
*/
public void setDragViewCreator(DragViewCreator creator) {
if (creator == null) {
return;
}
this.dragViewCreator = creator;
}
/**
* 设置拖动监听接口
*
* @param dragingListener
*/
public void setOnDragingListener(OnDragingListener dragingListener) {
this.dragingListener = dragingListener;
}
/**
* 设置拖动目标位置改变监听接口
*
* @param targetChangedListener
*/
public void setOnDragTargetChangedListener(OnDragTargetChangedListener targetChangedListener) {
this.targetChangedListener = targetChangedListener;
}
private int getStatusHeight() {
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
return context.getResources().getDimensionPixelSize(resourceId);
}
return 0;
}
/**
* 动画观察者
*/
private interface AnimatorObserver {
/**
* 滑动动画播放结束时回调
*/
void onAllAnimatorFinish();
}
/**
* 拖动块视图对象生成器
*/
public interface DragViewCreator {
/**
* 创建拖动块视图对象方法,可通过实现该方法自定义拖动块样式
*/
View createDragView(int width, int height, Bitmap viewCache);
}
/**
* 拖动监听接口
*/
public interface OnDragingListener {
/**
* 拖动开始时回调
*
* @param startPosition 拖动起始坐标
*/
void onStart(int startPosition);
/**
* 拖动过程中回调
*
* @param x 触控点相对ListView的x坐标
* @param y 触控点相对ListView的y坐标
* @param rawX 触控点相对屏幕的x坐标
* @param rawY 触控点相对屏幕的y坐标
*/
void onDraging(int x, int y, int rawX, int rawY);
/**
* 拖动结束时回调
*
* @param finalPosition 拖动终点坐标
*/
void onFinish(int finalPosition);
}
/**
* 拖动目标位置改变监听接口
*/
public interface OnDragTargetChangedListener {
/**
* 拖动过程中,每次目标位置改变,会在该方法回调
*
* @param targetPosition 拖动目标位置坐标
*/
void onTargetChanged(int targetPosition);
}
}
实现原理和DragListView差不多,就不多做解释了。DragGridView的setAdapter方法同样只接收BaseDragAdapter的继承类,用法和DragListView一样。
简单使用一下,先写一个item布局item_test_gridview.xml:
再写一个适配器TestGridViewAdapter:
public class TestGridViewAdapter extends BaseDragAdapter {
private Context context;
private int resourceId;
private ArrayList list;
private Vibrator vibrator;
private OnItemLongClickListener listener;
public TestGridViewAdapter(Context context, int resourceId, ArrayList list) {
this.context = context;
this.resourceId = resourceId;
this.list = list;
this.vibrator = (Vibrator) context.getSystemService(context.VIBRATOR_SERVICE);
}
@Override
public List getDataList() {
return list;
}
@Override
public boolean isDragAvailable(int position) {
return true;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, null);
viewHolder = new ViewHolder();
viewHolder.itemLayout = view.findViewById(R.id.item_layout);
viewHolder.contentTextView = view.findViewById(R.id.content_textview);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.contentTextView.setText(list.get(position));
viewHolder.itemLayout.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
vibrator.vibrate(100);
if (listener != null) {
listener.onItemLongClick(position);
}
return false;
}
});
return view;
}
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
this.listener = listener;
}
public interface OnItemLongClickListener {
void onItemLongClick(int position);
}
class ViewHolder {
LinearLayout itemLayout;
TextView contentTextView;
}
}
最后Activity这样写:
private DragGridView dragGridView;
private TestGridViewAdapter adapter;
private ArrayList list;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drag_gridview_test);
initData();
initView();
}
private void initData() {
list = new ArrayList<>();
for (int i = 1; i <= 40; i++) {
list.add("我是第" + i + "条数据");
}
}
private void initView() {
dragGridView = findViewById(R.id.drag_gridview);
dragGridView.setOnDragingListener(new DragGridView.OnDragingListener() {
@Override
public void onStart(int startPosition) {
}
@Override
public void onDraging(int x, int y, int rawX, int rawY) {
}
@Override
public void onFinish(int finalPosition) {
dragGridView.lockDrag();
}
});
adapter = new TestGridViewAdapter(this, R.layout.item_test_gridview, list);
adapter.setOnItemLongClickListener(new TestGridViewAdapter.OnItemLongClickListener() {
@Override
public void onItemLongClick(int position) {
dragGridView.unlockDrag();
}
});
dragGridView.setAdapter(adapter);
}
用法和DragListView一毛一样。运行一下就能看到开头的效果了。
DragGridView同样可以自定义拖拽View的样式,同样通过setDragViewCreator()方法来实现。比如说添加一个高亮效果:
dragGridView.setDragViewCreator(new DragGridView.DragViewCreator() {
@Override
public View createDragView(int width, int height, Bitmap viewCache) {
RelativeLayout layout = new RelativeLayout(DragGridViewTestActivity.this);
ImageView imageView = new ImageView(DragGridViewTestActivity.this);
imageView.setImageBitmap(viewCache);
layout.addView(imageView, new RelativeLayout.LayoutParams(width, height));
View view = new View(DragGridViewTestActivity.this);
view.setBackground(getDrawable(R.drawable.edging_red));
layout.addView(view, new RelativeLayout.LayoutParams(width, height));
return layout;
}
});
看看效果:
以上就是全部内容了,最后来总结一下。
DragListView和DragGridView分别实现ListView和GridView的item拖拽功能。接收Adapter必须是BaseDragAdapter的继承类,通过unlockDrag()方法和lockDrag()方法来开启和关闭拖动。提供OnDragingListener接口来监听拖动过程,提供DragViewCreator接口来自定义拖拽样式。
最后的最后,附上源码地址:https://download.csdn.net/download/Sure_Min/12572918
这次的内容就到这里,我们下次再见。