上篇博客分享了一個實現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
這次的內容就到這裏,我們下次再見。