上篇博客分享了一个实现ListView中item交换动画的控件(戳这里查看),但是有些情况下我们的需求比这种效果要复杂。比如说需要手动拖拽item来完成item交换的交互。像这样:
还有这样:
这次分享的控件就实现这样的功能,下面开始正文。
先说实现item拖拽功能的DragListView。上代码:
public class DragListView extends ListView { /** * 速度模板,影响视图移动时的速度变化 * <p> * 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<Animator> animatorList; // 视图列表,存放当前屏幕上正在播放的所有滑动动画的视图对象 private ArrayList<View> 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<View> 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的布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#FFFAFA" android:orientation="vertical"> <TextView android:id="@+id/content_textview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="15dp" android:paddingTop="20dp" android:paddingRight="15dp" android:paddingBottom="20dp" android:text="我是内容" android:textSize="20sp" /> <View android:id="@+id/divider_line" android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginLeft="15dp" android:background="#eeeeee" /> </LinearLayout>再简单写一个适配器TestListViewAdapter继承自BaseDragAdapter:
public class TestListViewAdapter extends BaseDragAdapter { private Context context; private int resourceId; private ArrayList<String> list; private Vibrator vibrator; private OnItemLongClickListener listener; public TestListViewAdapter(Context context, int resourceId, ArrayList<String> 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<String> 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长这样:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <stroke android:width="1dp" android:color="#FF6347" /> <solid android:color="#70FFC0CB" /> </shape>代码很简单,就是新建一个Layout,里面放一张图片,再在之上加一层高亮遮罩,并将这个layout返回给DragViewCreator接口即可。运行一下看一下效果:
同样的原理再写一个支持item拖拽的GridView,上源码:
public class DragGridView extends GridView { /** * 速度模板,影响视图移动时的速度变化 * <p> * 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<Animator> animatorList; // 视图列表,存放当前屏幕上正在播放的所有滑动动画的视图对象 private ArrayList<View> 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<Animator> 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<View> 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:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_layout" android:layout_width="match_parent" android:minHeight="130dp" android:layout_height="match_parent" android:background="#FFFAFA" android:gravity="center_horizontal" android:orientation="vertical"> <TextView android:id="@+id/content_textview" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:padding="15dp" android:text="我是内容" android:textSize="20sp" /> </LinearLayout>再写一个适配器TestGridViewAdapter:
public class TestGridViewAdapter extends BaseDragAdapter { private Context context; private int resourceId; private ArrayList<String> list; private Vibrator vibrator; private OnItemLongClickListener listener; public TestGridViewAdapter(Context context, int resourceId, ArrayList<String> 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<String> 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
这次的内容就到这里,我们下次再见。