轉帖請注明本文出自xiaanming的部落格(http://blog.csdn.net/xiaanming/article/details/17718579),請尊重他人的辛勤勞動成果,謝謝!
在android開發中,我們常常用到listview和gridview,而有的時候系統的listview,gridview并不能滿足我們的需求,是以我們需要自己定義一個listview或者gridview,我的上一篇文章中就是自定義的一個左右滑動删除item的例子,大家有興趣的可以去看看 android
使用scroller實作絢麗的listview左右滑動删除item效果,今天這篇文章就給大家來自定義gridview的控件,gridview主要是來顯示網格的控件,在android的開發中使用很普通,相對于textview,button這些控件來說要來的複雜些,今天給大家帶來長按gridview的item,然後将其拖拽其他item上面,使得gridview的item發生交換,比較典型的就是我們的launcher,網上有很多關于gridview的拖動的demo,但是大部分都是相同的,而且存在一些bug,而且大部分都是點選gridview的item然後進行拖動,或者item之間不進行實時交換,今天給大家更加詳細的介紹gridview拖拽,并且将demo做的更完美,大家更容易接受,也許很多人聽到這個感覺實作起來很複雜,就關掉的這篇文章,其實告訴大家,隻要知道了思路就感覺一點都不複雜了,不信大家可以接着往下看看,首先還是跟大家說說實作的思路
根據手指按下的x,y坐标來擷取我們在gridview上面點選的item
手指按下的時候使用handler和runnable來實作一個定時器,假如定時時間為1000毫秒,在1000毫秒内,如果手指擡起了移除定時器,沒有擡起并且手指點選在gridview的item所在的區域,則表示我們長按了gridview的item
如果我們長按了item則隐藏item,然後使用windowmanager來添加一個item的鏡像在螢幕用來代替剛剛隐藏的item
當我們手指在螢幕移動的時候,更新item鏡像的位置,然後在根據我們移動的x,y的坐标來擷取移動到gridview的哪一個位置
到gridview的item過多的時候,可能一螢幕顯示不完,我們手指拖動item鏡像到螢幕下方,要觸發gridview想上滾動,同理,當我們手指拖動item鏡像到螢幕上面,觸發gridview向下滾動
gridview交換資料,重新整理界面,移除item的鏡像
看完上面的這些思路你是不是找到了些感覺了呢,心裡癢癢的想動手試試吧,好吧,接下來就帶大家根據思路來實作可拖拽的gridview,建立一個項目就叫draggridview
建立一個類draggridview繼承gridview,先來看看draggridview的代碼,然後在根據代碼進行相關的講解
[java] view
plaincopy
package com.example.draggridview;
import android.annotation.suppresslint;
import android.app.activity;
import android.content.context;
import android.graphics.bitmap;
import android.graphics.pixelformat;
import android.graphics.rect;
import android.os.handler;
import android.os.vibrator;
import android.text.getchars;
import android.util.attributeset;
import android.view.gravity;
import android.view.motionevent;
import android.view.view;
import android.view.windowmanager;
import android.widget.adapterview;
import android.widget.gridview;
import android.widget.imageview;
/**
* @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
@suppresslint("newapi")
public class draggridview extends gridview{
/**
* draggridview的item長按響應的時間, 預設是1000毫秒,也可以自行設定
*/
private long dragresponsems = 1000;
* 是否可以拖拽,預設不可以
private boolean isdrag = false;
private int mdownx;
private int mdowny;
private int movex;
private int movey;
* 正在拖拽的position
private int mdragposition;
* 剛開始拖拽的item對應的view
private view mstartdragitemview = null;
* 用于拖拽的鏡像,這裡直接用一個imageview
private imageview mdragimageview;
* 震動器
private vibrator mvibrator;
private windowmanager mwindowmanager;
* item鏡像的布局參數
private windowmanager.layoutparams mwindowlayoutparams;
* 我們拖拽的item對應的bitmap
private bitmap mdragbitmap;
* 按下的點到所在item的上邊緣的距離
private int mpoint2itemtop ;
* 按下的點到所在item的左邊緣的距離
private int mpoint2itemleft;
* draggridview距離螢幕頂部的偏移量
private int moffset2top;
* draggridview距離螢幕左邊的偏移量
private int moffset2left;
* 狀态欄的高度
private int mstatusheight;
* draggridview自動向下滾動的邊界值
private int mdownscrollborder;
* draggridview自動向上滾動的邊界值
private int mupscrollborder;
* draggridview自動滾動的速度
private static final int speed = 80;
* item發生變化回調的接口
private onchanagelistener onchanagelistener;
public draggridview(context context) {
this(context, null);
}
public draggridview(context context, attributeset attrs) {
this(context, attrs, 0);
public draggridview(context context, attributeset attrs, int defstyle) {
super(context, attrs, defstyle);
mvibrator = (vibrator) context.getsystemservice(context.vibrator_service);
mwindowmanager = (windowmanager) context.getsystemservice(context.window_service);
mstatusheight = getstatusheight(context); //擷取狀态欄的高度
private handler mhandler = new handler();
//用來處理是否為長按的runnable
private runnable mlongclickrunnable = new runnable() {
@override
public void run() {
isdrag = true; //設定可以拖拽
mvibrator.vibrate(50); //震動一下
mstartdragitemview.setvisibility(view.invisible);//隐藏該item
//根據我們按下的點顯示item鏡像
createdragimage(mdragbitmap, mdownx, mdowny);
}
};
* 設定回調接口
* @param onchanagelistener
public void setonchangelistener(onchanagelistener onchanagelistener){
this.onchanagelistener = onchanagelistener;
* 設定響應拖拽的毫秒數,預設是1000毫秒
* @param dragresponsems
public void setdragresponsems(long dragresponsems) {
this.dragresponsems = dragresponsems;
@override
public boolean dispatchtouchevent(motionevent ev) {
switch(ev.getaction()){
case motionevent.action_down:
//使用handler延遲dragresponsems執行mlongclickrunnable
mhandler.postdelayed(mlongclickrunnable, dragresponsems);
mdownx = (int) ev.getx();
mdowny = (int) ev.gety();
//根據按下的x,y坐标擷取所點選item的position
mdragposition = pointtoposition(mdownx, mdowny);
if(mdragposition == adapterview.invalid_position){
return super.dispatchtouchevent(ev);
}
//根據position擷取該item所對應的view
mstartdragitemview = getchildat(mdragposition - getfirstvisibleposition());
//下面這幾個距離大家可以參考我的部落格上面的圖來了解下
mpoint2itemtop = mdowny - mstartdragitemview.gettop();
mpoint2itemleft = mdownx - mstartdragitemview.getleft();
moffset2top = (int) (ev.getrawy() - mdowny);
moffset2left = (int) (ev.getrawx() - mdownx);
//擷取draggridview自動向上滾動的偏移量,小于這個值,draggridview向下滾動
mdownscrollborder = getheight() /4;
//擷取draggridview自動向下滾動的偏移量,大于這個值,draggridview向上滾動
mupscrollborder = getheight() * 3/4;
//開啟mdragitemview繪圖緩存
mstartdragitemview.setdrawingcacheenabled(true);
//擷取mdragitemview在緩存中的bitmap對象
mdragbitmap = bitmap.createbitmap(mstartdragitemview.getdrawingcache());
//這一步很關鍵,釋放繪圖緩存,避免出現重複的鏡像
mstartdragitemview.destroydrawingcache();
break;
case motionevent.action_move:
int movex = (int)ev.getx();
int movey = (int) ev.gety();
//如果我們在按下的item上面移動,隻要不超過item的邊界我們就不移除mrunnable
if(!istouchinitem(mstartdragitemview, movex, movey)){
mhandler.removecallbacks(mlongclickrunnable);
case motionevent.action_up:
mhandler.removecallbacks(mlongclickrunnable);
mhandler.removecallbacks(mscrollrunnable);
return super.dispatchtouchevent(ev);
* 是否點選在gridview的item上面
* @param itemview
* @param x
* @param y
* @return
private boolean istouchinitem(view dragview, int x, int y){
int leftoffset = dragview.getleft();
int topoffset = dragview.gettop();
if(x < leftoffset || x > leftoffset + dragview.getwidth()){
return false;
if(y < topoffset || y > topoffset + dragview.getheight()){
return true;
public boolean ontouchevent(motionevent ev) {
if(isdrag && mdragimageview != null){
switch(ev.getaction()){
case motionevent.action_move:
movex = (int) ev.getx();
movey = (int) ev.gety();
//拖動item
ondragitem(movex, movey);
break;
case motionevent.action_up:
onstopdrag();
isdrag = false;
return true;
return super.ontouchevent(ev);
* 建立拖動的鏡像
* @param bitmap
* @param downx
* 按下的點相對父控件的x坐标
* @param downy
private void createdragimage(bitmap bitmap, int downx , int downy){
mwindowlayoutparams = new windowmanager.layoutparams();
mwindowlayoutparams.format = pixelformat.translucent; //圖檔之外的其他地方透明
mwindowlayoutparams.gravity = gravity.top | gravity.left;
mwindowlayoutparams.x = downx - mpoint2itemleft + moffset2left;
mwindowlayoutparams.y = downy - mpoint2itemtop + moffset2top - mstatusheight;
mwindowlayoutparams.alpha = 0.55f; //透明度
mwindowlayoutparams.width = windowmanager.layoutparams.wrap_content;
mwindowlayoutparams.height = windowmanager.layoutparams.wrap_content;
mwindowlayoutparams.flags = windowmanager.layoutparams.flag_not_focusable
| windowmanager.layoutparams.flag_not_touchable ;
mdragimageview = new imageview(getcontext());
mdragimageview.setimagebitmap(bitmap);
mwindowmanager.addview(mdragimageview, mwindowlayoutparams);
* 從界面上面移動拖動鏡像
private void removedragimage(){
if(mdragimageview != null){
mwindowmanager.removeview(mdragimageview);
mdragimageview = null;
* 拖動item,在裡面實作了item鏡像的位置更新,item的互相交換以及gridview的自行滾動
private void ondragitem(int movex, int movey){
mwindowlayoutparams.x = movex - mpoint2itemleft + moffset2left;
mwindowlayoutparams.y = movey - mpoint2itemtop + moffset2top - mstatusheight;
mwindowmanager.updateviewlayout(mdragimageview, mwindowlayoutparams); //更新鏡像的位置
onswapitem(movex, movey);
//gridview自動滾動
mhandler.post(mscrollrunnable);
* 當movey的值大于向上滾動的邊界值,觸發gridview自動向上滾動
* 當movey的值小于向下滾動的邊界值,觸犯gridview自動向下滾動
* 否則不進行滾動
private runnable mscrollrunnable = new runnable() {
int scrolly;
if(movey > mupscrollborder){
scrolly = -speed;
mhandler.postdelayed(mscrollrunnable, 25);
}else if(movey < mdownscrollborder){
scrolly = speed;
}else{
scrolly = 0;
mhandler.removecallbacks(mscrollrunnable);
//當我們的手指到達gridview向上或者向下滾動的偏移量的時候,可能我們手指沒有移動,但是draggridview在自動的滾動
//是以我們在這裡調用下onswapitem()方法來交換item
onswapitem(movex, movey);
view view = getchildat(mdragposition - getfirstvisibleposition());
//實作gridview的自動滾動
smoothscrolltopositionfromtop(mdragposition, view.gettop() + scrolly);
* 交換item,并且控制item之間的顯示與隐藏效果
* @param movex
* @param movey
private void onswapitem(int movex, int movey){
//擷取我們手指移動到的那個item的position
int tempposition = pointtoposition(movex, movey);
//假如tempposition 改變了并且tempposition不等于-1,則進行交換
if(tempposition != mdragposition && tempposition != adapterview.invalid_position){
getchildat(tempposition - getfirstvisibleposition()).setvisibility(view.invisible);//拖動到了新的item,新的item隐藏掉
getchildat(mdragposition - getfirstvisibleposition()).setvisibility(view.visible);//之前的item顯示出來
if(onchanagelistener != null){
onchanagelistener.onchange(mdragposition, tempposition);
mdragposition = tempposition;
* 停止拖拽我們将之前隐藏的item顯示出來,并将鏡像移除
private void onstopdrag(){
getchildat(mdragposition - getfirstvisibleposition()).setvisibility(view.visible);
removedragimage();
* 擷取狀态欄的高度
* @param context
private static int getstatusheight(context context){
int statusheight = 0;
rect localrect = new rect();
((activity) context).getwindow().getdecorview().getwindowvisibledisplayframe(localrect);
statusheight = localrect.top;
if (0 == statusheight){
class<?> localclass;
try {
localclass = class.forname("com.android.internal.r$dimen");
object localobject = localclass.newinstance();
int i5 = integer.parseint(localclass.getfield("status_bar_height").get(localobject).tostring());
statusheight = context.getresources().getdimensionpixelsize(i5);
} catch (exception e) {
e.printstacktrace();
}
return statusheight;
*
* @author xiaanming
*
public interface onchanagelistener{
/**
* 當item交換位置的時候回調的方法,我們隻需要在該方法中實作資料的交換即可
* @param form
* 開始的position
* @param to
* 拖拽到的position
*/
public void onchange(int form, int to);
}
首先看draggridview的事件分發方法,不了解android事件分發的可以先去了解下,android事件分發對于自定義控件很重要,簡單說下,當我們點選draggridview的item,先會去執行dispatchtouchevent()方法将事件分發下去,是以我們要重寫dispatchtouchevent()方法在手指按下的時候根據pointtoposition()方法來擷取我們按下的item的position,根據getchildat()方法來擷取該position上面所對應的view,
并且開啟長按的定時器,預設時間為1000毫秒,如果在1000毫秒内手指擡起或者手指在螢幕上滑動出了該item,則取消長按定時器,否則就表示可以進行拖拽,手機友好的震動一下,隐藏我們長按的item,螢幕調用createdragimage()方法來建立我們長按的item的鏡像,建立item的鏡像使用的是windowmanager類,該類可以建立一個窗體顯示在activity之上,
再此之前大家先要了解這幾個距離,了解這幾個距離之前要首先知道getrawx(),getrawy()和getx(),gety()的差別,getrawx(),getrawy()是相對于螢幕的原點的距離,而getx(),gety()是相對于控件左上方的點的距離,為了友善大家了解我用word簡單的畫了下圖,畫得不好,大家将就的看下,紅色框框為我們的gridview
mpoint2itemtop 手指按下的點到該item的上邊緣的距離,如上圖的1号線
mpoint2itemleft 手指按下的點到該item的左邊緣的距離,如上圖的2号線
moffset2top draggridview的上邊緣到螢幕上邊緣的距離,如上圖的3号線,這個距離包裹狀态欄,标題欄,或者一些在draggridview上面的布局的高度,這個很重要我們現實item鏡像需要用到
moffset2left draggridview的左邊緣到螢幕左邊緣的距離,如上圖的4号線,我這個demo的這個距離為0,因為我設定draggridview的寬度為充滿螢幕,但是我們要考慮假如draggridview與螢幕左邊緣設定了間隙或者左邊有其他的布局的情形
mdownscrollborder 這個距離表示當draggridview的item過多的時候,手機一屏顯示不完全,我們拖動item鏡像到這個高度的時候,draggridview自動向下滾動,如上圖的5号線
.mupscrollborder 這個和mdownscrollborder相反,當我們大于這個高度的時候,draggridview自動向上滾動,如上圖的6号線
了解了這六個距離,我們就來看看建立item鏡像的方法裡面,其他的我不多說,首先設定format為pixelformat.translucent,表示除了我們顯示圖檔和文字的其他地方為透明,之後就是x,y這兩個距離的計算,計算的是item的左上角的坐标,了解了上面這六個距離我們很容易得出x,y的坐标,可是你會發現y的坐标減去了狀态欄的高度,這點大家需要注意下,另外我們需要擷取item的繪制緩存的bitmap對象,然後将bitmap設定到一個imageview上面,為什麼要這麼做呢?如果調用addview()方法将item
直接添加到windowmanager裡面,會有異常産生,因為item已經有了自己歸屬的父容器draggridview,所有我們這裡使用一個imageview來代替item添加到windowmanager裡面
上面已經完成了開始拖拽的準備工作,要想拖動鏡像我們還需要重寫ontouchevent()方法,擷取移動的x,y的坐标,利用windowmanager的updateviewlayout方法就能對鏡像進行拖動,拖動的鏡像的時候為了有更好的使用者體驗,我們還要做item的實時交換效果,我們利用手指移動的x,y坐标,利用pointtoposition()來擷取拖拽到的position,然後将之前的item顯示出來,将拖拽到的item進行隐藏,這樣子就完成了item在界面上面的交換,但是資料交換我這裡沒有做,是以我提供了回調接口onchanagelistener,我們隻需要自己實作資料的交換邏輯然後重新整理draggridview即可,我們還需要實作draggridview的自動向上滾動或者向下滾動,使用handler和mscrollrunnable利用smoothscrolltopositionfromtop()來實作draggridview滾動,具體的實作大家可以看代碼
手指離開界面,将item的鏡像移除,并将拖拽到的item顯示出來,這樣子就實作了girdview的拖拽效果啦,接下來我們來使用下我們自定義可拖拽的gridview吧,先看主界面布局,隻有我們自定義的一個draggridview
[html] view
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.draggridview.draggridview
android:id="@+id/draggridview"
android:listselector="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cachecolorhint="@android:color/transparent"
android:verticalspacing="10dip"
android:horizontalspacing="10dip"
android:stretchmode="columnwidth"
android:gravity="center"
android:numcolumns="3" >
</com.example.draggridview.draggridview>
</relativelayout>
接下來我們看看draggridview的item的布局,上面一個imageview下面一個textview
<?xml version="1.0" encoding="utf-8"?>
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent" >
<imageview
android:id="@+id/item_image"
android:scaletype="centercrop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerhorizontal="true" >
</imageview>
<textview
android:id="@+id/item_text"
android:layout_below="@+id/item_image"
</textview>
布局搞定了我們就來看看首頁面mainactivity的代碼吧
import java.util.arraylist;
import java.util.collections;
import java.util.hashmap;
import java.util.list;
import android.os.bundle;
import android.widget.simpleadapter;
import com.example.draggridview.draggridview.onchanagelistener;
public class mainactivity extends activity {
private list<hashmap<string, object>> datasourcelist = new arraylist<hashmap<string, object>>();
protected void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
draggridview mdraggridview = (draggridview) findviewbyid(r.id.draggridview);
for (int i = 0; i < 30; i++) {
hashmap<string, object> itemhashmap = new hashmap<string, object>();
itemhashmap.put("item_image",r.drawable.com_tencent_open_notice_msg_icon_big);
itemhashmap.put("item_text", "拖拽 " + integer.tostring(i));
datasourcelist.add(itemhashmap);
final simpleadapter msimpleadapter = new simpleadapter(this, datasourcelist,
r.layout.grid_item, new string[] { "item_image", "item_text" },
new int[] { r.id.item_image, r.id.item_text });
mdraggridview.setadapter(msimpleadapter);
mdraggridview.setonchangelistener(new onchanagelistener() {
@override
public void onchange(int from, int to) {
hashmap<string, object> temp = datasourcelist.get(from);
//直接互動item
// datasourcelist.set(from, datasourcelist.get(to));
// datasourcelist.set(to, temp);
//這裡的處理需要注意下
if(from < to){
for(int i=from; i<to; i++){
collections.swap(datasourcelist, i, i+1);
}
}else if(from > to){
for(int i=from; i>to; i--){
collections.swap(datasourcelist, i, i-1);
}
datasourcelist.set(to, temp);
msimpleadapter.notifydatasetchanged();
});
這裡面的代碼還是比較簡單,主要講下onchange()方法,我們要為mdraggridview設定一個onchanagelistener的回調接口,在onchange()方法裡面實作資料的交換邏輯,第一個參數from為item開始的位置,第二個參數to為item拖拽到的位置,剛開始我使用的交換邏輯是
hashmap<string, object> temp = datasourcelist.get(from);
直接交換的item的資料,然後看了下網易新聞的拖拽的gridview,他不是直接實作兩個item直接的資料交換,是以将資料交換邏輯改成了下面的方式
簡單說下,資料的交換邏輯,比如我們将position從5拖拽到7這個位置,我注釋掉的邏輯是直接将5和7的資料交換,而後面的那種邏輯是将6的位置資料移動到5,将7的位置移動到6,然後再7顯示5 6->5, 7->6, 5->7不知道大家了解了沒有。
接下來我們來運作下項目,在運作之前我們不要忘了在androidmanifest.xml裡面加入震動的權限<uses-permission android:name="android.permission.vibrate"/>
好了,今天的講解就到此結束,效果還不錯吧,看完這篇文章你是不是覺得gridview拖拽也不是那麼難實作呢?你心裡是不是也大概有自己的一個思路,建議大家自己敲敲看看,可以自己去實作下listview的拖拽實作,listview比gridview簡單些,好的學習方法不是看得懂人家的代碼,而是看完代碼自己根據腦海裡的思路自己敲出來,是以還是鼓勵大家多敲代碼,不明白的同學在下面留言,我會為大家解答的!
項目源碼,點選下載下傳
ps:上面的代碼在4.0以上的機器上面運作是ok的,但是在4.0以下的機器存在幾個問題,首先是相容性的問題,首先smoothscrolltopositionfromtop()方法在2.x的機器是不存在的,但是我們可以使用smoothscrollby()來代替上面的方法使得gridview滾動
注意:很多朋友說運作在2.3的機器上面拖動的時候出現某些item無緣無故的隐藏了,筆者在寫demo的時候一直用的是4.0的真機運作的,後面部落客使用模拟器運作在2.3的機器上面,确實存在很多朋友反應的問題,原因就是因為部落客使用的是simpleadapter,simpleadapter會複用item,是以才導緻本不該隐藏的item隐藏了,但是為什麼運作在部落客4.0的機器上面不出現問題,部落客也很納悶,現在我對其做出了修改,采用自定義adapter,對item不采用複用的原則,雖然效率上面有點點不足,但是如果對于item不多的gridview,效率不足可以忽略,新修改的代碼可以運作在2.x以上的機器不出現朋友們所說的問題了,非常感謝大家提出的問題!
修改版源碼,點選下載下傳
再次聲明, 在修改版的源碼中還存在一點小bug,不過已解決,正如28樓所說的一樣,是因為我在mainactivity的onchange()方法中調用了mdragadapter.setitemhide(to)方法,主要是為了實作拖動到新的位置隐藏該item, 使得mhideposition不為-1,忘記在停止拖動onstopdrag()方法中将mhideposition設定為-1了,是以為了解決28樓所說的問題,隻需要在onstopdrag()方法添加一句 ((dragadapter)this.getadapter()).setitemhide(-1)就行了