Android仿QQ列表滑動刪除操作
這篇山寨一個新版QQ的列表滑動刪除,上篇有說到QQ的滑動刪除,推測原理就是ListView本身每個item存在一個Button,只不過普通的狀態(tài)下隱藏掉了,檢測到向左的滑動事件的時候彈出隱藏的Button,不過再切換Button狀態(tài)的時候會給Button一個出現(xiàn)和隱藏的動畫。下面實(shí)現(xiàn)這個ListView。
首先有個難點(diǎn)就是通過ListView獲取它某個item的View,對于ViewGroup,可以直接調(diào)用getChildAt()方法獲取對應(yīng)的子view,但是在ListView直接使用getChildAt()的話,會發(fā)現(xiàn)只要滑動ListView就會報空指針異常,很明顯對于ListView直接使用getChildAt()方法是行不通的,雖然ListView就是個ViewGroup。已經(jīng)有人解釋了這個問題以及解決方法,大概意思就是可以理解為,ListView雖然看上去有很多item,但是這只是看上去而已,實(shí)際上ListView只構(gòu)造了你能看到的,就是屏幕上能看到的那么多item的view,所以要獲取ListView某一個位置position的item的view,就需要用如下的代碼:
int firstVisiblePos = getFirstVisiblePosition() - getHeaderViewsCount(); int factPos = curPos - firstVisiblePos; mItemView = getChildAt(factPos);
就是先獲取ListView當(dāng)前第一個可見的item的firstVisiblePos,當(dāng)然啦,還要記得減去header view的數(shù)目,然后用想獲取的item的curPos減去firstVisiblePos就是對應(yīng)的item實(shí)際在ListView的位置factPos了。這下就不會報空指針異常了。
知道了獲取某一個位置的item的view,現(xiàn)在就需要通過檢測滑動事件,判斷當(dāng)前是在和ListView哪個position的item交互。使用ListView中如下方法:
int curPos = pointToPosition((int)curX, (int)curY);
接下來就是截獲ListView的touch事件了,自定義一個SlidingDeleteListView,繼承自ListView,重寫onTouchEvent()方法:
@Override public boolean onTouchEvent(MotionEvent event) { if(!mEnableSliding) return false; if(mCancelMotionEvent && event.getAction() == MotionEvent.ACTION_MOVE) { return true; } else if(mCancelMotionEvent && event.getAction() == MotionEvent.ACTION_DOWN) { event.setAction(MotionEvent.ACTION_CANCEL); } switch(event.getAction()) { case MotionEvent.ACTION_DOWN: { if(mTracker == null) mTracker = VelocityTracker.obtain(); else mTracker.clear(); mLastMotionX = event.getX(); mLastMotionY = event.getY(); }break; case MotionEvent.ACTION_MOVE: { mTracker.addMovement(event); mTracker.computeCurrentVelocity(1000); int curVelocityX = (int) mTracker.getXVelocity(); float curX = event.getX(); float curY = event.getY(); int lastPos = pointToPosition( (int)mLastMotionX, (int)mLastMotionY); int curPos = pointToPosition((int)curX, (int)curY); int distanceX = (int)(mLastMotionX - curX); if(lastPos == curPos && (distanceX >= MAX_DISTANCE || curVelocityX < -MAX_FLING_VELOCITY)) { int firstVisiblePos = getFirstVisiblePosition() - getHeaderViewsCount(); int factPos = curPos - firstVisiblePos; mItemView = getChildAt(factPos); if(mItemView != null) { if(mButtonID == -1) throw new IllegalButtonIDException("Illegal DeleteButton resource id," + "ensure excute the function setButtonID(int id)"); mButton = mItemView.findViewById(mButtonID); mButton.setVisibility(View.VISIBLE); mButton.startAnimation(mShowAnim); mLastButtonShowingPos = curPos; mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(mDeleteItemListener != null) mDeleteItemListener.onButtonClick(v, mLastButtonShowingPos); mButton.setVisibility(View.GONE); mLastButtonShowingPos = -1; } }); mCancelMotionEvent = true; } } }break; case MotionEvent.ACTION_UP: { if(mTracker != null) { mTracker.clear(); mTracker.recycle(); mTracker = null; } mCancelMotionEvent = false; if(mLastButtonShowingPos != -1) { event.setAction(MotionEvent.ACTION_CANCEL); } }break; case MotionEvent.ACTION_CANCEL: { hideShowingButtonWithAnim(); }break; } return super.onTouchEvent(event); }
解釋上面代碼之前先簡單說一下android的touch事件的分發(fā)原理,主要是MotionEvent.ACTION_DOWN這個事件是最重要的,事件的分發(fā)有一來一回兩部分,“來”是指ViewGroup獲取到系統(tǒng)傳遞過來的ACTION_DOWN事件,先調(diào)用ViewGroup的onInterceptTouchEvent()方法,這個方法表示這個事件ViewGroup是否想截獲,如果返回true的話,則會將ACTION_DOWN事件分發(fā)到ViewGroup的onTouchEvent()方法進(jìn)行處理了,表示該事件被父view截獲掉了,子view將不再會獲取到事件。而如果ViewGroup的onInterceptTouchEvent()方法返回false則意味ViewGroup不截獲該事件,接下來事件發(fā)生的位置存在子view的話,ViewGroup會將該ACTION_DOWN事件傳遞給該子view進(jìn)行處理。這個過程是事件的分發(fā)過程,接下來是“回”,”回“這個過程是事件的消耗過程,子view的onTouchEvent()方法如果返回true,表示該ACTION_DOWN事件被該子view消耗了,則ViewGroup將不會在onTouchEvent()方法接收到該事件了,因為該事件被消耗了。如果子view的onTouchEvent()方法返回false表示子view不消耗該ACTION_DOWN事件(當(dāng)然啦,子view依然可以處理該事件,但是返回false依然會把事件拋回給ViewGroup,這就可以做很多事了),之后事件會返回給父view。最終MotionEvent.ACTION_DOWN事件在哪一層的view消耗了,則接下來的后續(xù)touch事件,如ACTION_UP、ACTION_MOVE、ACTION_CANCEL等事件都將會直接傳遞給消耗ACTION_DOWN事件的view,其他層的view將不再受到后續(xù)的事件,直到下一次的ACTION_DOWN事件。
以上的代碼,暫時關(guān)注switch的代碼塊,對于檢測到MotionEvent.ACTION_DOWN事件的時候,記錄下當(dāng)前touch事件的位置,同時我們先獲取mTracker,這是一個VelocityTracker對象,android提供的用于計算當(dāng)前滑動事件的速率的;檢測到MotionEvent.ACTION_MOVE事件,我們有兩個情況下確定要處理,一種情況是用戶在滑動一定距離就彈出button,這個距離是當(dāng)前滑動的位置和本次ACTION_DOWN記錄下的事件位置的距離,第二中情況是用戶滑動速度超過一個閾值的時候,彈出button,這個速度的計算就是用前面提到的mTracker了,用法很簡單;檢測到ACTION_UP事件表示當(dāng)前的這次交互完成,我們可以做一些清理工作;至于ACTION_CANCEL事件,這個這里暫且買個關(guān)子,這個使用個偷梁換柱的小技巧欺負(fù)一下系統(tǒng)~
上面的ACTION_MOVE事件里面如果處理了事件,彈出了button,那我們在下次檢測到ACTION_DOWN事件,如果這個事件發(fā)生的位置沒有在button的區(qū)域,則表示用戶不是點(diǎn)擊彈出的button,那我們需要gone掉這個button,即在此隱藏它。那這里就需要使用帶前面提及的ViewGroup的onInterceptTouchEvent()方法,在這次的ACTION_DOWN事件傳遞給子view前截獲它,當(dāng)然先判斷一下這次的事件是不是點(diǎn)擊button的事件:
private boolean isClickButton(MotionEvent ev) {
mButton.getLocationOnScreen(mShowingButtonLocation); int left = mShowingButtonLocation[0]; int right = mShowingButtonLocation[0] + mButton.getWidth(); int top = mShowingButtonLocation[1]; int bottom = mShowingButtonLocation[1] + mButton.getHeight(); return (ev.getRawX() >= left && ev.getRawX() <= right && ev.getRawY() >= top && ev.getRawY() <= bottom); } 接下來重寫onInterceptTouchEvent()方法: @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if(mEnableSliding && mLastButtonShowingPos != -1 && ev.getAction() == MotionEvent.ACTION_DOWN && !isClickButton(ev)) { ev.setAction(MotionEvent.ACTION_CANCEL); mCancelMotionEvent = true; return true; } return super.onInterceptTouchEvent(ev); };
判斷要不要截獲ACTION_DOWN事件,一先判斷當(dāng)前有沒有button有彈出,因為每次彈出一個button,會記下當(dāng)前彈出的item的位置mLastButtonShowingPos;然后就是當(dāng)前是不是ACTION_DOWN事件;以及是否點(diǎn)擊彈出的button。所有條件符合,我們就截獲這個ACTION_DOWN事件,在onInterceptTouchEvent()方法return true。這樣該ACTION_DOWN事件就會傳遞到本SlidingDeleteListView的onTouchEvent()方法里面,這里再解釋前面的那個ACTION_CANCEL事件,在onTouchEvent()方法里面判斷到是ACTION_DOWN,并且前面在onInterceptTouchEvent()里面做的標(biāo)記mCancelMotionEvent,這個標(biāo)記表示截獲了ACTION_DOWN事件,需要特殊處理這個ACTION_DOWN事件,然后看onTouchEvent()方法里面是如何處理這次的ACTION_DOWN事件呢:
else if(mCancelMotionEvent && event.getAction() == MotionEvent.ACTION_DOWN) { event.setAction(MotionEvent.ACTION_CANCEL); }
是滴,偷梁換柱,把當(dāng)前的ACTION_DOWN事件換成ACTION_CANCEL事件,在ACTION_CANCEL事件的處理就是gone掉當(dāng)前彈出的button,這樣就把兩種情況下的ACTION_DOWN區(qū)分出來進(jìn)行了額外的處理了。
同時我們可以看到在ACTION_UP事件中,有進(jìn)行判斷,當(dāng)當(dāng)前的mLastButtonShowingPos不為-1,,則表示這次是用戶滑動彈出button的操作,這次的touch事件我們有進(jìn)行處理了,這樣我們就不能在把這次的ACTION_UP事件拋回給ListView本身默認(rèn)的super.onTouchEvent()邏輯處理了,因為前面的ACTION_DOWN以及ACTION_MOVE我們都是走的默認(rèn)流程,那現(xiàn)在ListView原本的邏輯就等著ACTION_UP事件派發(fā),這樣就是ListView本身OnItemClick或者OnItemLongClick事件的觸發(fā)了,想想一下,如果我們彈出了隱藏的button,ListView依然處理OnItemClick或者OnItemLongClick這樣肯定就不合適了,所以這里我們依然要稍微欺騙一下系統(tǒng),將原本的ACTION_UP替換成ACTION_CANCEL,這樣當(dāng)處理了button的彈出后,就不會再處理ListView原本的OnItemClick或者OnItemLongClick事件了:
if(mLastButtonShowingPos != -1) { event.setAction(MotionEvent.ACTION_CANCEL); }
最后講一下我們這樣重寫onTouchEvent()方法的話,會不會影響到這個自定義的ListView的onItemClick()和onItemLongClick()方法呢,答案是本方案不會,因為onTouchEvent()方法對于沒有截獲的事件,都是返回super.onTouchEvent(ev),這樣既處理了滑動事件的檢測,有沒有干擾到系統(tǒng)對于這次事件的處理流程,而截獲的事件,有給了事件的完整的生命周期(我有偽造一個ACTION_CANCEL事件結(jié)束一次touch的交互),這里我姑且就說生命周期吧,以ACTION_DOWN事件起始,ACTION_UP或是ACTION_CANCEL事件結(jié)束,中間夾雜著一系列的ACTION_MOVE事件。我最初的方案是采用ListView.setOnTouchListener(),并實(shí)現(xiàn)該TouchListener的onTouch()方法,這樣處理事件略復(fù)雜,因為這個控件的處理邏輯在ACTION_MOVE里面彈出了button之后,就把所有的后續(xù)ACTION_MOVE事件無效化,因為如果不無效化的話后續(xù)的ACTION_MOVE事件ListView依然會受到,那用戶可以上下拖動ListView,知道ListView的item都是重用幾個共同的view的同學(xué)就應(yīng)該會想到接下來要出什么bug了,就是原本沒有彈出button的item出現(xiàn)在屏幕上后竟然也會彈出button,因為這個item重用了已經(jīng)消失的item的view。那我用OnTouchListener.onTouch()方法的時候,在彈出了button就直接return回了true,表示這個事件被OnTouchListener處理了,但這里就出了問題,因為前面的ACTION_DOWN事件一直都是返回false,表示touch的交互的最初始事件由ListView默認(rèn)的onTouchEvent()邏輯處理(也必須返回false,要不然所有的事件都被這和OnTouchListener吃掉了),由于我們不知道默認(rèn)的onTouchEvent()里面如何處理了這次的ACTION_DOWN,雖然一般情況下是ListView消耗這次的ACTION_DOWN,開始一個OnItemClick或者OnItemLongClick事件的處理,這是因為item的點(diǎn)擊事件都是由ListView的onTouchEvent()處理的,ACTION_DOWN被ListView自身的onTouchEvent()消耗了,但是后續(xù)的ACTION_MOVE甚至ACTION_UP事件又被OnTouchListener消耗了的話,無法再傳遞到默認(rèn)的onTouchEvent()里面處理,一個本來完整的touch生命周期硬生生的被切成了兩部分交由兩個地方處理,這樣肯定會導(dǎo)致一大推問題,最明顯的就是ListView本身的OnItemClickListener等處理事件的監(jiān)聽器與處理滑動事件檢測的代碼產(chǎn)生沖突,像是滑動之后彈出了button,而當(dāng)前處理滑動事件的item則處于高亮的選中狀態(tài)(android里面用pressed表示),即使已經(jīng)手指離開了屏幕。最后采用的方案則是維持了事件處理的邏輯在一個方法之內(nèi),既能做到系統(tǒng)事件正常的分發(fā)運(yùn)轉(zhuǎn),本身也能處理滑動事件。
最后代碼提交到了我的github上:https://github.com/YoungLeeForeverBoy/SlidingDeleteListView。
下面是本控件的展示:
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android開發(fā)藝術(shù)探索學(xué)習(xí)筆記(七)
這篇文章主要介紹了Android開發(fā)藝術(shù)探索學(xué)習(xí)筆記(七)的相關(guān)資料,需要的朋友可以參考下2016-01-01在Android模擬器上模擬GPS功能總是null的解決方法
在我們開發(fā)時需要在模擬器上模擬GPS,可在Location的時候總是null,下面與大家分享下具體的解決方法,感興趣的朋友可以參考下哈2013-06-06android實(shí)現(xiàn)查詢公交車還有幾站的功能
這篇文章主要為大家詳細(xì)介紹了android實(shí)現(xiàn)查詢公交車還有幾站的功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05Android實(shí)現(xiàn)類似微信的文本輸入框 效果
本文給大家介紹一下微信的文本輸入框是如何實(shí)現(xiàn)的,其實(shí)那只是個普通的文本框設(shè)了一個特殊的背景而已。具體微信怎么實(shí)現(xiàn)的,大家可以反編譯下,這里介紹下如何實(shí)現(xiàn)這個背景2017-05-05Android自定義processor實(shí)現(xiàn)bindView功能的實(shí)例
下面小編就為大家分享一篇Android自定義processor實(shí)現(xiàn)bindView功能的實(shí)例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12Android編程實(shí)現(xiàn)控件不同狀態(tài)文字顯示不同顏色的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)控件不同狀態(tài)文字顯示不同顏色的方法,涉及Android針對控件布局文件屬性設(shè)置及狀態(tài)判定等相關(guān)技巧,需要的朋友可以參考下2016-02-02怎樣刪除android的gallery中的圖片實(shí)例說明
長按gallery中的圖片進(jìn)行刪除該圖片的操作,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06Android多線程斷點(diǎn)續(xù)傳下載示例詳解
這篇文章主要為大家詳細(xì)介紹了Android多線程斷點(diǎn)續(xù)傳下載示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11