一個流暢的拖動排序DragSortGridView,自動滾屏


先上效果

這里寫圖片描述

流暢效果超越了網易新聞和UC瀏覽器的欄目收藏.gif圖和實際效果有差距

1.拖拽可以移動item,並且其他item會立即自動補位,快速拖拽也非常流暢
2.item太多時,拖拽到邊緣時會自動滾屏.
3.可以自定義被拖拽的的View放大,添加陰影等效果
4.長按啟動刪除模式,需要自己實現item的刪除按鈕展示,自己管理刪除模式的切換
5.可以放在ScrollView中拖動排序,需要ScrollView繼承ListenScrollView,不影響外面控件的大部分事件.
6.可以長按啟動item拖拽,也可以觸摸直接開始拖動

簡單使用方法

        dragSortGridView = (DragSortGridView) findViewById(R.id.dragSort1);
//長按item響應該item的拖動排序,默認是觸摸就開始拖動
dragSortGridView.setDragModel(DragSortGridView.DRAG_BY_LONG_CLICK);
dragAdapter = new MyAdapter();
dragSortGridView.setAdapter(dragAdapter);

dragAdapter 需要多實現onDataModelMove一個方法,界面排序改變需要提供真實數據排序改變.示例

class MyAdapter extends DragAdapter {
@Override
public void onDataModelMove(int from, int to) {
String s = list.remove(from);
list.add(to, s);
}

@Override
public int getCount() {
return list.size();
}

@Override
public String getItem(int position) {
return list.get(position);
}

@Override
public long getItemId(int position) {
return 0;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView;
if (convertView == null) {
FrameLayout frameLayout = new FrameLayout(SecondActivity.this);
convertView = frameLayout;
textView = new TextView(SecondActivity.this);
frameLayout.setPadding(20, 20, 20, 20);
textView.setPadding(20, 100, 20, 100);
frameLayout.addView(textView);
textView.setBackgroundColor(0x33ff00ff);
textView.setGravity(Gravity.CENTER);
} else {
textView = (TextView) ((FrameLayout) convertView).getChildAt(0);
}
textView.setText(getItem(position));
return convertView;
}
}

擴展可以改變的功能

            //設置每行個數
dragSortGridView.setNumColumns(4);

/*改變拖動item所在動畫層,例如frameLayout是位於最上冊的全屏透明層,
則item拖拽可以在全屏范圍內,超出dragSortGridView本身范圍,
這個一般用來配合外層是ListenScrollView用*/
dragSortGridView.setAnimFrame(frameLayout);

//設置前面多少個位置固定,不能拖動
dragSortGridView.setNoPositionChangeItemCount(2);
//設置尾部多少個位置固定,不能拖動
dragSortGridView.setFootNoPositionChangeItemCount(1);
//修改item響應拖動時的效果,默認是放大到120%
dragSortGridView.setOnDragSelectListener(new DragSortGridView.OnDragSelectListener() {
@Override
public void onDragSelect(View mirror) {
//當item開始拖動時調用該方法
}

@Override
public void onPutDown(View itemView) {
//當item被放時是調用該方法
}
});
//修改長按拖動的響應時間
dragSortGridView.setDragLongPressTime(1500);

dragSortGridView.setOnItemClickListener(...);
dragSortGridView.setOnLongClickListener(...);

注意

  1. 不能給DragSortGridView設置padding,但是可以用margin,不能使用橫豎間隙,要item之間的間距只能在adapter生成item里面設置padding
  2. 必須做contentView的復用,不做則不流暢.

下面是源碼
有一個R.id.first,需要在values的ids.xml里面添加

<item name="first" type="id" />


/**
* Copyright (C), 2008-2015, Huawei Tech. Co., Ltd.
* <p/>
* Description : 拖動排序布局
*
* @version V100R001
* @since V100R001
*/

@SuppressLint({ "NewApi", "Override" })
public class DragSortGridView extends FrameLayout {
protected NoScrollGridView mGridView;
private ScrollView mScrollView;
private int headDragPosition = 0;
private int footDragPosition = 0;
private FrameLayout mDragFrame;
private View mCopyView, hideView;
private GestureDetector detector;
/** 動畫時間 */
private static final long ANIM_DURING = 250;
protected int mNumColumns = 3, mColHeight = 0, mColWidth = 0, mChildCount = 0, mMaxHeight = 0;
private int currentDragPosition = -1;
private DragAdapter adapter;
/** 持有子view */
private List<View> mChilds = new ArrayList<View>();
private static final int TAG_KEY = R.id.first;
// private static final int TAG_KEY = R.id.tag_key;
private int mCurrentY = 0;
/**
* 觸摸區域,0不滾動區域,1可向上滾動的區域,-1可向下滾動的區域
*/

private int mTouchArea = 0;
/**
* gridview能否滾動,是否內容太多
*/

private boolean canScroll = true;
/**
* 是否可以拖動,點擊拖動策略下直接開啟,長按拖動需要長按以后開啟
*/

private boolean isDragable = true;
/**
* 自動滾屏的動畫
*/

private ValueAnimator animator;
/**
* view是否加載完成,如果未加載完成,沒有寬高,無法接受事件
*/

private boolean isViewInitDone = false;

/** 是否有位置發生改變,否則不用重繪 */
private boolean hasPositionChange = false;

/** 適配器的觀察者,觀察適配器的數據改變 */
private DataSetObserver observer = new DataSetObserver() {
@Override
public void onChanged() {
mChildCount = adapter.getCount();
// 下列屬性狀態清除,才會在被調用notifyDataSetChange時,在gridview測量布局完成后重新獲取
mChilds.clear();
mColHeight = mColWidth = mMaxHeight = 0;
isViewInitDone = false;
}

@Override
public void onInvalidated() {
mChildCount = adapter.getCount();
}
};
private float[] lastLocation = null;
/**
* 手勢監聽器,滾動和單擊
*/

private SimpleOnGestureListener simpleOnGestureListener = new SimpleOnGestureListener() {

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (hasSendDragMsg) {
hasSendDragMsg = false;
handler.removeMessages(0x123);
}
if (isDragable && mCopyView != null) {// 可以拖動,實現跟隨手指的拖動效果

// /// 2015/11/27補充修正跟隨手指移動方法,適用於當本控件在drag時同時滾動的情況
if (lastLocation == null) {
lastLocation = new float[] { e1.getRawX(), e1.getRawY() };
}
distanceX = lastLocation[0] - e2.getRawX();
distanceY = lastLocation[1] - e2.getRawY();
lastLocation[0] = e2.getRawX();
lastLocation[1] = e2.getRawY();
// ////////

mCopyView.setX(mCopyView.getX() - distanceX);
mCopyView.setY(mCopyView.getY() - distanceY);
mCopyView.invalidate();
int to = eventToPosition(e2);
if (to != currentDragPosition && to >= headDragPosition && to < mChildCount - footDragPosition) {
onDragPositionChange(currentDragPosition, to);
}
}
return true;
}

@Override
public void onShowPress(MotionEvent e) {
/** 響應長按拖拽 */
if (mDragMode == DRAG_BY_LONG_CLICK) {
// 啟動拖拽模式
// isDragable = true;
// 通知父控件不攔截我的事件
getParent().requestDisallowInterceptTouchEvent(true);
// 根據點擊的位置生成該位置上的view鏡像
int position = eventToPosition(e);
if (position >= headDragPosition && position < mChildCount - footDragPosition) {
// copyView(currentDragPosition = position);
Message msg = handler.obtainMessage(0x123, position, 0);
// showpress本身大概需要170毫秒
handler.sendMessageDelayed(msg, dragLongPressTime - 170);
hasSendDragMsg = true;
}
}
};

};
private boolean hasSendDragMsg = false;
private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 0x123:
// 啟動拖拽模式
isDragable = true;
// 根據點擊的位置生成該位置上的view鏡像
copyView(currentDragPosition = msg.arg1);
hasSendDragMsg = false;
break;
default:
break;
}
return false;
}
});

public DragSortGridView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public DragSortGridView(Context context) {
super(context);
init();
}

private void init() {
Context context = getContext();
mGridView = new NoScrollGridView(context);
mGridView.setVerticalScrollBarEnabled(false);
mGridView.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
mGridView.setSelector(new ColorDrawable());
// View的寬高之類必須在測量,布局,繪制一系列過程之后才能獲取到
mGridView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

@Override
public void onGlobalLayout() {
if (mChilds.isEmpty()) {
for (int i = 0; i < mGridView.getChildCount(); i++) {
View view = mGridView.getChildAt(i);
view.setTag(TAG_KEY, new int[] { 0, 0 });
view.clearAnimation();
mChilds.add(view);
}
}
if (!mChilds.isEmpty()) {
mColHeight = mChilds.get(0).getHeight();
}
mColWidth = mGridView.getColumnWidth();
if (mChildCount % mNumColumns == 0) {
mMaxHeight = mColHeight * mChildCount / mNumColumns;
} else {
mMaxHeight = mColHeight * (mChildCount / mNumColumns + 1);
}
canScroll = mMaxHeight - getHeight() > 0;
// 告知事件處理,完成View加載,許多屬性也已經初始化了
isViewInitDone = true;
}
});
mScrollView = new ListenScrollView(context);

mDragFrame = new FrameLayout(context);
addView(mScrollView, -1, -1);
mScrollView.addView(mGridView, -1, -1);

addView(mDragFrame, new LayoutParams(-1, -1));
detector = new GestureDetector(context, simpleOnGestureListener);
detector.setIsLongpressEnabled(false);
mGridView.setNumColumns(mNumColumns);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
if (l != null) {
l.onTouch(this, ev);
}
if (!isViewInitDone) {
return false;
}

if (isDragable) {
handleScrollAndCreMirror(ev);
} else {
// 交給子控件自己處理
if (canScroll)
mScrollView.dispatchTouchEvent(ev);
else
mGridView.dispatchTouchEvent(ev);
}

// 處理拖動
detector.onTouchEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) {
lastLocation = null;
if (hasSendDragMsg) {
hasSendDragMsg = false;
handler.removeMessages(0x123);
}
}
return true;
}

/**
* Author :[pWX273343] 2015年7月22日
* <p>
* Description :攔截所有事件
*/

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}

/**
* 處理自動滾屏,和單擊生成鏡像
*/

private void handleScrollAndCreMirror(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 通知父控件不攔截我的事件
getParent().requestDisallowInterceptTouchEvent(true);
// 根據點擊的位置生成該位置上的view鏡像
int position = eventToPosition(ev);
if (position >= headDragPosition && position < mChildCount - footDragPosition) {
copyView(currentDragPosition = position);
}
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);// 通知父控件不攔截我的事件
// 內容太多時,移動到邊緣會自動滾動
if (canScroll) {
int touchArea = decodeTouchArea(ev);
if (touchArea != mTouchArea) {
onTouchAreaChange(touchArea);
mTouchArea = touchArea;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (hideView != null) {
hideView.setVisibility(View.VISIBLE);
if (onDragSelectListener != null) {
onDragSelectListener.onPutDown(hideView);
}
}
mDragFrame.removeAllViews();
// mDragFrame.scrollTo(0, 0);
// isNotifyByDragSort = true;
if (hasPositionChange) {
hasPositionChange = false;
adapter.notifyDataSetChanged();
} else if (mDragMode == DRAG_BY_LONG_CLICK && itemLongClickListener != null) {
itemLongClickListener.onItemLongClick(mGridView, childAt(currentDragPosition), currentDragPosition, 0);
}
// 停止滾動
if (canScroll) {
int scrollStates2 = decodeTouchArea(ev);
if (scrollStates2 != 0) {
onTouchAreaChange(0);
mTouchArea = 0;
}
}
// 放手時取消拖動排序模式
if (mDragMode == DRAG_BY_LONG_CLICK) {
isDragable = false;
}
break;
default:
break;
}
}

/**
* @param ev
* 事件
* @return 0中間區域, 1底部,-1頂部
* @描述: 檢查當前觸摸事件位於哪個區域, 頂部1/5可能觸發下滾,底部1/5可能觸發上滾
* @作者 [pWX273343] 2015年6月30日
*/

private int decodeTouchArea(MotionEvent ev) {
if (ev.getY() > getHeight() * 4 / (double) 5) {
return 1;
} else if (ev.getY() < getHeight() / (double) 5) {
return -1;
} else {
return 0;
}
}

/**
* @param ev
* @return
* @描述 得到事件觸發點,摸到的是哪一個item
* @作者 [pWX273343] 2015年7月6日
*/

public int eventToPosition(MotionEvent ev) {

if (ev != null) {
int m = (int) ev.getX() / mColWidth;
int n = (int) (ev.getY() + mCurrentY) / mColHeight;
int position = n * mNumColumns + m;
if (position >= mChildCount) {
return mChildCount - 1;
} else {
return position;
}
}
return 0;
}

// 這里把控件作為假的橫向ListView,所以返回position跟高度無關,暫時這樣
// public int eventToPosition(MotionEvent ev) {
//
// if (ev != null) {
// int m = (int) ev.getX() / mColWidth;
// if (m >= mChildCount) {
// return mChildCount - 1;
// } else {
// return m;
// }
// }
// return 0;
// }

/**
* @param dragPosition
* @描述:復制一個鏡像,並添加到透明層
* @作者 [pWX273343] 2015年7月6日
*/

private void copyView(int dragPosition) {
hideView = mChilds.get(dragPosition);
int realPosition = mGridView.indexOfChild(hideView);
if (!adapter.isUseCopyView()) {
mCopyView = adapter.getView(realPosition, mCopyView, mDragFrame);
} else {
mCopyView = adapter.copyView(realPosition, mCopyView, mDragFrame);
}
hideView.setVisibility(View.INVISIBLE);
mDragFrame.addView(mCopyView, mColWidth, mColHeight);

int[] l1 = new int[2];
int[] l2 = new int[2];
hideView.getLocationOnScreen(l1);
mDragFrame.getLocationOnScreen(l2);

// mCopyView.setX(hideView.getLeft());
// mCopyView.setY(hideView.getTop() - mCurrentY);
mCopyView.setX(l1[0] - l2[0]);
mCopyView.setY(l1[1] - l2[1]);
if (onDragSelectListener == null) {
mCopyView.setScaleX(1.2f);
mCopyView.setScaleY(1.2f);
} else {
onDragSelectListener.onDragSelect(mCopyView);
}
}

/**
* @param from
* @param to
* @描述:動畫效果移動View
* @作者 [pWX273343] 2015年6月24日
*/

private void translateView(int from, int to) {
View view = mChilds.get(from);
int fromXValue = ((int[]) view.getTag(TAG_KEY))[0];
int fromYValue = ((int[]) view.getTag(TAG_KEY))[1];
int toXValue = to % mNumColumns - from % mNumColumns + fromXValue;
int toYValue = to / mNumColumns - from / mNumColumns + fromYValue;
Animation animation = new TranslateAnimation(1, fromXValue, 1, toXValue, 1, fromYValue, 1, toYValue);
animation.setDuration(ANIM_DURING);
animation.setFillAfter(true);
view.setTag(TAG_KEY, new int[] { toXValue, toYValue });
view.startAnimation(animation);
}

/**
* @param from
* @param to
* @描述:拖動View使位置發生改變時
* @作者 [pWX273343] 2015年7月6日
*/

private void onDragPositionChange(int from, int to) {
if (from > to) {
for (int i = to; i < from; i++) {
translateView(i, i + 1);
}
} else {
for (int i = to; i > from; i--) {
translateView(i, i - 1);
}
}
if (!hasPositionChange) {
hasPositionChange = true;
}
adapter.onDataModelMove(from, to);
View view = mChilds.remove(from);
mChilds.add(to, view);
currentDragPosition = to;
}

/**
* Function :setAdapter
* <p/>
* Author :[pWX273343] 2015年6月24日
* <p/>
* Description :設置適配器.該適配器必須實現一個方法,當view的位置發生變動時,對實際數據的改動
*
* @param adapter
* @see GridView#setAdapter(android.widget.ListAdapter)
*/

public void setAdapter(DragAdapter adapter) {
if (this.adapter != null && observer != null) {
this.adapter.unregisterDataSetObserver(observer);
}
this.adapter = adapter;
mGridView.setAdapter(adapter);
adapter.registerDataSetObserver(observer);
mChildCount = adapter.getCount();
}

public int getNumColumns() {
return mNumColumns;
}

/**
* 每行幾個
*/

public void setNumColumns(int numColumns) {
this.mNumColumns = numColumns;
mGridView.setNumColumns(numColumns);
}

/**
* 設置前幾個item不可以改變位置
*/

public void setNoPositionChangeItemCount(int count) {
headDragPosition = count;
}

/**
* 設置后幾個item不可以改變位置
*/

public void setFootNoPositionChangeItemCount(int count) {
footDragPosition = count;
}

/**
* 控制自動滾屏的動畫監聽器.
*/

private AnimatorUpdateListener animUpdateListener = new AnimatorUpdateListener() {

@Override
public void onAnimationUpdate(ValueAnimator animation) {
int targetY = Math.round((Float) animation.getAnimatedValue());
if (targetY < 0) {
targetY = 0;
} else if (targetY > mMaxHeight - getHeight()) {
targetY = mMaxHeight - getHeight();
}
// mGridView.scrollTo(0, targetY);
mScrollView.smoothScrollTo(0, targetY);
// mCurrentY = targetY;
}

};

/**
* @param scrollStates
* @描述:觸摸區域改變,做相應處理,開始滾動或停止滾動
* @作者 [pWX273343] 2015年6月29日
*/

protected void onTouchAreaChange(int scrollStates) {
if (!canScroll) {
return;
}
if (animator != null) {
animator.removeUpdateListener(animUpdateListener);
}
if (scrollStates == 1) {// 從普通區域進入觸發向上滾動的區域
int instance = mMaxHeight - getHeight() - mCurrentY;
animator = ValueAnimator.ofFloat(mCurrentY, mMaxHeight - getHeight());
animator.setDuration((long) (instance / 0.5f));
animator.setTarget(mGridView);
animator.addUpdateListener(animUpdateListener);
animator.start();
} else if (scrollStates == -1) {// 進入觸發向下滾動的區域
animator = ValueAnimator.ofFloat(mCurrentY, 0);
animator.setDuration((long) (mCurrentY / 0.5f));
animator.setTarget(mGridView);
animator.addUpdateListener(animUpdateListener);
animator.start();
}
}

private OnDragSelectListener onDragSelectListener;

/**
* @描述:一個item view剛被拖拽和放下時起來生成鏡像時調用.
* @作者 [pWX273343] 2015年6月30日
*/

public void setOnDragSelectListener(OnDragSelectListener onDragSelectListener) {
this.onDragSelectListener = onDragSelectListener;
}

public interface OnDragSelectListener {
/**
* @param mirror
* 所拖拽起來的view生成的鏡像 ,並不是實際的view.可對這個鏡像實施變換效果,但是並不改變放下后的效果
* @描述:拖拽起一個view時調用
* @作者 [pWX273343] 2015年6月30日
*/

void onDragSelect(View mirror);

/**
* @param itemView
* @描述:拖拽的View放下時調用
* @作者 [pWX273343] 2015年7月3日
*/

void onPutDown(View itemView);
}

class NoScrollGridView extends GridView {

public NoScrollGridView(Context context) {
super(context);
}

/**
* @return
* @描述:兼容老版本的getColumWidth
* @作者 [pWX273343] 2015年7月1日
*/

public int getColumnWidth() {
return getWidth() / getNumColumns();
}

public NoScrollGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mExpandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, mExpandSpec);
}
}

/**
*
* Copyright (C), 2008-2015, Huawei Tech. Co., Ltd.
* <p>
* Description : 監聽滾動的scrollview,我們需要實時知道他已滾動的距離
*
* @author [pWX273343] 2015年7月22日
* @version V100R001
* @since V100R001
*
*/

class ListenScrollView extends ScrollView {
public ListenScrollView(Context context) {
super(context);
}

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
mCurrentY = getScrollY();
}
}

public View getChildViewAtIndex(int index) {
if (index < mChilds.size()) {
return mChilds.get(index);
}
return null;
}

// 轉交給gridview一些常用監聽器
private OnItemLongClickListener itemLongClickListener;

/**
*
* @描述:item 轉交給gridview一些常用監聽器
*
* @param itemClickListener
* @作者 [pWX273343] 2015年7月27日
*/

public void setOnItemClickListener(OnItemClickListener itemClickListener) {
mGridView.setOnItemClickListener(itemClickListener);
}

/**
* 長按監聽器自己觸發,點擊拖動模式不存在長按
*
* @param
*/

public void setOnItemLongClickListener(OnItemLongClickListener itemLongClickListener) {
this.itemLongClickListener = itemLongClickListener;
}

/** 點擊拖動 */
public static final int DRAG_WHEN_TOUCH = 0;
/** 長按拖動 */
public static final int DRAG_BY_LONG_CLICK = 1;

private int mDragMode = DRAG_WHEN_TOUCH;

/**
* @param mode
* int類型
* @描述:設置拖動的策略是點擊還是長按
* @作者 [pWX273343] 2015年7月20日 參考 DRAG_WHEN_TOUCH,DRAG_BY_LONG_CLICK
*/

public void setDragModel(int mode) {
this.mDragMode = mode;
isDragable = mode == DRAG_WHEN_TOUCH;
}

public View childAt(int index) {
return mGridView.getChildAt(index);
}

public int childCount() {
return mGridView.getChildCount();
}

public void setAnimFrame(FrameLayout mDragFrame) {
this.mDragFrame = mDragFrame;
}

private OnTouchListener l;

@Override
public void setOnTouchListener(OnTouchListener l) {
this.l = l;
}

private long dragLongPressTime = 600;

/**
* 設置長按需要用時
*
* @param time
*/

public void setDragLongPressTime(long time) {
dragLongPressTime = time;
}
}

DragAdapter

public abstract class DragAdapter extends BaseAdapter {

/**
*
* @描述:當從from排序被拖到to排序時的處理方式,請對相應的數據做處理。
*
* @param from
* @param to
* @作者 [pWX273343] 2015年6月24日
*/

public abstract void onDataModelMove(int from, int to);

/**
* 復制View使用的方法,默認直接使用getView方法獲取
* @param position
* @param convertView
* @param parent
* @return
*/

public View copyView(int position, View convertView, ViewGroup parent) {
return null;
}

/**
* 是否啟用copyView方法
* @return true 使用copyView復制 false 使用getView直接獲取鏡像
*/

public boolean isUseCopyView() {
return false;
}
}

ps:
其實相同功能的控件已經很多了,但是當時初學android寫這個控件花了一些心血.或許也只能安慰地說一句,寫這個控件的過程中了解了更多android的機制,也是一種成長經歷吧.


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com