自定義ListView實(shí)現(xiàn)拖拽ListItem項(xiàng)交換位置(附源碼)
在上一篇實(shí)現(xiàn)了通過(guò)布局泵拿到不同布局為listitem布局,然后實(shí)現(xiàn)聯(lián)系人的ListView,這一章要做的是拖拽ListView的Item項(xiàng),本章原理是在上一篇博客基礎(chǔ)之上的,上一篇博客:自定義Adapter并通過(guò)布局泵LayoutInflater抓取layout模板編輯每一個(gè)item
實(shí)現(xiàn)效果圖

說(shuō)明
首先我們看到的上面這張圖就是實(shí)現(xiàn)的效果圖了。拖動(dòng)之后數(shù)據(jù)項(xiàng)完成交換位置。
功能剖析
我們看到做的這個(gè)效果是一個(gè)拖拽ListView的Item項(xiàng)位置的功能,在布局方面還是用基于布局泵LayoutInflater來(lái)從不同的Layout模板拿到不同的布局然后將view返回。關(guān)于布局這一點(diǎn)的知識(shí)在上一篇有詳細(xì)說(shuō)明,文章開(kāi)頭已經(jīng)說(shuō)明,OK,下面我們來(lái)剖析一下這個(gè)拖動(dòng)效果的實(shí)現(xiàn)吧,下面的文章將會(huì)以方法執(zhí)行的順序來(lái)給出各個(gè)方法的代碼。然后依次剖析每個(gè)方法的作用。
方法執(zhí)行順序
[DragView] -> [初始化ListViewContext,和觸發(fā)move事件的最小距離變量]
[onInterceptTouchEvent] -> [初始化拖動(dòng)的變量]
[startDrag] -> [準(zhǔn)備拖動(dòng)的影像、window等變量]
[stopDrag] -> [判斷重置拖動(dòng)的影像]
[startDrag] -> [準(zhǔn)備拖動(dòng)的影像、window等變量]
[onTouchEvent] -> [判斷點(diǎn)擊事件、根據(jù)動(dòng)作做不同操作,或重新繪制Move影響,或者Stop停止拖動(dòng)。交換數(shù)據(jù),也就是下面的這兩個(gè)方法]
[onDrag] -> [實(shí)現(xiàn)滾動(dòng)的動(dòng)作]
[onDrop] -> [實(shí)現(xiàn)數(shù)據(jù)item位置切換]
注意
上面的方法執(zhí)行順序只是大概邏輯,這其中還有判斷和方法中調(diào)用其他方法,所以方法的調(diào)用是多次的,大家湊合看一下方法的大體功能吧,需要注意的是我們重寫(xiě)的onTouchEvent是要一直不斷的監(jiān)聽(tīng)我們的按鍵的,如果為Move的話也就是會(huì)一直不斷的去調(diào)用onDrag方法去實(shí)現(xiàn)滾動(dòng)的動(dòng)作,在此我做了測(cè)試,給大家看一下打印的日志如下:

我們看到move日志被執(zhí)行了多次。說(shuō)明我們?cè)谕蟿?dòng)的時(shí)候一直在執(zhí)行這個(gè)方法。下面我會(huì)給大家自定義ListView的代碼,代碼中注釋量很大,可讀性很高,推薦大家參考上面我給出的方法運(yùn)行順序去理解,最后我將給出運(yùn)行源碼。
package com.example.draglistview;
import com.example.draglistview.MainActivity.DragListAdapter;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Toast;
public class DragView extends ListView{
private ImageView imageView; //被拖動(dòng)的圖片
private int scaledTouchSlop; //判斷拖動(dòng)的距離
private int dragSrcPosition; //手指在touch事件觸摸時(shí)候的原始位置
private int dragPosition; //手指拖動(dòng)列表項(xiàng)時(shí)候的位置
private int dragPoint; //手指點(diǎn)擊位置在當(dāng)前數(shù)據(jù)項(xiàng)item中的位置,只有Y坐標(biāo)
private int dragOffset; //當(dāng)前視圖listview在屏幕中的位置,只有Y坐標(biāo)
private int upScrollBounce; //向上滑動(dòng)的邊界
private int downScrollBounce; //拖動(dòng)的時(shí)候向下滑動(dòng)的邊界
private WindowManager windowManager = null; //窗口管理類
//窗口參數(shù)類
private WindowManager.LayoutParams layoutParams = null;
//注意該View如果在Layout xml 注冊(cè)使用的話必須使用下面的這個(gè)構(gòu)造進(jìn)行初始化
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
//觸發(fā)移動(dòng)事件的最小距離
scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
//重寫(xiě)于absListView
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
//獲取的該touch事件的x坐標(biāo)和y坐標(biāo),該坐標(biāo)是相對(duì)于組件的左上角的位置
int x = (int) ev.getX();
int y = (int) ev.getY();
//賦值手指點(diǎn)擊時(shí)候的開(kāi)始坐標(biāo)
dragSrcPosition = dragPosition = this.pointToPosition(x, y);
//如果點(diǎn)擊在列表之外,也就是不允許的位置
if(dragPosition == AdapterView.INVALID_POSITION){
//直接執(zhí)行父類,不做任何操作
return super.onInterceptTouchEvent(ev);
}
/***
* 鎖定手指touch的列表item,
* 參數(shù)為屏幕的touch坐標(biāo)減去listview左上角的坐標(biāo)
* 這里的getChildAt方法參數(shù)為相對(duì)于組件左上角坐標(biāo)為00的情況
* 故有下面的這種參數(shù)算法
*/
ViewGroup itemView = (ViewGroup) this.getChildAt(dragPosition-this.getFirstVisiblePosition());
/****
* 說(shuō)明:getX Y為touch點(diǎn)相對(duì)于組件左上角的距離
* getRawX 、Y 為touch點(diǎn)相對(duì)于屏幕左上角的距離
* 參考http://blog.csdn.net/love_world_/article/details/8164293
*/
//touch點(diǎn)的view相對(duì)于該childitem的top坐標(biāo)的距離
dragPoint = y-itemView.getTop();
//為距離屏幕左上角的Y減去距離組件左上角的Y,其實(shí)就是
//組件上方的view+標(biāo)題欄+狀態(tài)欄的Y
dragOffset = (int) (ev.getRawY()-y);
//拿到拖動(dòng)的imageview對(duì)象
View drager = itemView.findViewById(R.id.imageView1);
//判斷條件為拖動(dòng)touch圖片是否為null和touch的位置,是否符合
if(drager != null && x>drager.getLeft()-20){
//判斷得出向上滑動(dòng)和向下滑動(dòng)的值
upScrollBounce = Math.min(y-scaledTouchSlop, getHeight()/3);
downScrollBounce = Math.max(y+scaledTouchSlop, getHeight()*2/3);
//啟用繪圖緩存
itemView.setDrawingCacheEnabled(true);
//根據(jù)圖像緩存拿到對(duì)應(yīng)位圖
Bitmap bm = Bitmap.createBitmap(itemView.getDrawingCache());
startDrag(bm, y);
}
return false;
}
return super.onInterceptTouchEvent(ev);
}
//重寫(xiě)OnTouchEvent,觸摸事件
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(imageView != null && dragPosition != INVALID_POSITION){
int currentAction = ev.getAction();
switch (currentAction) {
case MotionEvent.ACTION_UP:
int upY = (int) ev.getY();
//還有一些操作
stopDrag();
onDrop(upY);
break;
case MotionEvent.ACTION_MOVE:
Log.v("move", "move---------");
int moveY = (int) ev.getY();
onDrag(moveY);
break;
default:
break;
}
return true;
}
//決定了選中的效果
return super.onTouchEvent(ev);
}
/****
* 準(zhǔn)備拖動(dòng),初始化拖動(dòng)時(shí)的影像,和一些window參數(shù)
* @param bm 拖動(dòng)緩存位圖
* @param y 拖動(dòng)之前touch的位置
*/
public void startDrag(Bitmap bm,int y){
stopDrag();
layoutParams = new WindowManager.LayoutParams();
//設(shè)置重力
layoutParams.gravity = Gravity.TOP;
//橫軸坐標(biāo)不變
layoutParams.x = 0;
/**
*
* y軸坐標(biāo)為 視圖相對(duì)于自身左上角的Y-touch點(diǎn)在列表項(xiàng)中的y
* +視圖相對(duì)于屏幕左上角的Y,=
* 該view相對(duì)于屏幕左上角的位置
*/
layoutParams.y = y-dragPoint+dragOffset;
/****
* 寬度和高度都為wrapContent
*/
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
/****
* 設(shè)置該layout參數(shù)的一些flags參數(shù)
*/
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
//設(shè)置該window項(xiàng)是半透明的格式
layoutParams.format = PixelFormat.TRANSLUCENT;
//設(shè)置沒(méi)有動(dòng)畫(huà)
layoutParams.windowAnimations = 0;
//配置一個(gè)影像ImageView
ImageView imageViewForDragAni = new ImageView(getContext());
imageViewForDragAni.setImageBitmap(bm);
//配置該windowManager
windowManager = (WindowManager) this.getContext().getSystemService("window");
windowManager.addView(imageViewForDragAni, layoutParams);
imageView = imageViewForDragAni;
}
/***
* 停止拖動(dòng),去掉拖動(dòng)時(shí)候的影像
*/
public void stopDrag(){
if(imageView != null){
windowManager.removeView(imageView);
imageView = null;
}
}
/****
* 拖動(dòng)方法
* @param y
*/
public void onDrag(int y){
if(imageView != null){
//透明度
layoutParams.alpha = 0.8f;
layoutParams.y = y-this.dragPoint+this.dragOffset;
windowManager.updateViewLayout(imageView, layoutParams);
}
//避免拖動(dòng)到分割線返回-1
int tempPosition = this.pointToPosition(0, y);
if(tempPosition != this.INVALID_POSITION){
this.dragPosition = tempPosition;
}
int scrollHeight = 0;
if(y<upScrollBounce){
scrollHeight = 8;//定義向上滾動(dòng)8個(gè)像素,如果可以向上滾動(dòng)的話
}else if(y>downScrollBounce){
scrollHeight = -8;//定義向下滾動(dòng)8個(gè)像素,,如果可以向上滾動(dòng)的話
}
if(scrollHeight!=0){
//真正滾動(dòng)的方法setSelectionFromTop()
setSelectionFromTop(dragPosition, getChildAt(dragPosition-getFirstVisiblePosition()).getTop()+scrollHeight);
}
}
/***
* 拖動(dòng)放下的時(shí)候
* param : y
*/
public void onDrop(int y){
int tempPosition = this.pointToPosition(0, y);
if(tempPosition != this.INVALID_POSITION){
this.dragPosition = tempPosition;
}
//超出邊界處理
if(y<getChildAt(1).getTop()){
//超出上邊界
dragPosition = 1;
}else if(y>getChildAt(getChildCount()-1).getBottom()){
//超出下邊界
dragPosition = getAdapter().getCount()-1;
//
}
//數(shù)據(jù)交換
if(dragPosition>0&&dragPosition<getAdapter().getCount()){
@SuppressWarnings("unchecked")
DragListAdapter adapter = (DragListAdapter)getAdapter();
//原始位置的item
String dragItem = adapter.getItem(dragSrcPosition);
adapter.remove(dragItem);
adapter.insert(dragItem, dragPosition);
Toast.makeText(getContext(), adapter.getList().toString(), Toast.LENGTH_SHORT).show();
}
}
}
寫(xiě)在后面的話
以上就為自定義ListView的源碼了。當(dāng)然還有部分未給出的代碼。包括MainActivity、3個(gè)Layout xml 文件。[比較簡(jiǎn)單] 如果你看懂了[或者有一些疑問(wèn)]上面的這部分代碼,你可以下載我的源碼查相關(guān)的API和案例去學(xué)習(xí),去進(jìn)步,祝成功!
源碼下載
相關(guān)文章
android開(kāi)發(fā)教程之switch控件使用示例
這篇文章主要介紹了android開(kāi)的switch控件使用示例,需要的朋友可以參考下2014-04-04Android開(kāi)發(fā)之ListView列表刷新和加載更多實(shí)現(xiàn)方法
這篇文章主要介紹了Android開(kāi)發(fā)之ListView列表刷新和加載更多實(shí)現(xiàn)方法,實(shí)例分析了ListView列表操作的相關(guān)技巧,需要的朋友可以參考下2015-06-06android編程實(shí)現(xiàn)為程序創(chuàng)建快捷方式的方法
這篇文章主要介紹了android編程實(shí)現(xiàn)為程序創(chuàng)建快捷方式的方法,實(shí)例分析了Android創(chuàng)建程序快捷方式的相關(guān)設(shè)置與功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-11-11Android開(kāi)發(fā)快速實(shí)現(xiàn)底部導(dǎo)航欄示例
這篇文章主要為大家介紹了Android開(kāi)發(fā)快速實(shí)現(xiàn)底部導(dǎo)航欄的示例代碼,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04Android實(shí)現(xiàn)原生側(cè)滑菜單的超簡(jiǎn)單方式
網(wǎng)上關(guān)于Android實(shí)現(xiàn)側(cè)滑菜單的文章有很多,可是我們這篇文章是給大家分享一種超簡(jiǎn)單的方式,對(duì)大家開(kāi)發(fā)Android具有一定的參考借鑒價(jià)值,有需要的朋友們可以一起來(lái)看看。2016-09-09Android利用Senser實(shí)現(xiàn)不同的傳感器
這篇文章主要為大家詳細(xì)介紹了Android利用Senser實(shí)現(xiàn)不同傳感器的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02Android實(shí)現(xiàn)底部導(dǎo)航欄功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)底部導(dǎo)航欄功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06Android ProgressDialog用法之實(shí)現(xiàn)app上傳文件進(jìn)度條轉(zhuǎn)圈效果
這篇文章主要介紹了Android ProgressDialog用法之實(shí)現(xiàn)app上傳文件進(jìn)度條轉(zhuǎn)圈效果,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03