本文轉自 http://jianwang0412.iteye.com/blog/1267885
重寫listview,通過監聽滑動事件,根據滑動時所處的位置,以及滑動的方向,使用view的內置scrollTo或scrollBy函數來移動view到你手勢互動的距離(此處為一半),然后當確定消費了給事件后,又回滾到(0,0)點。當然只有在超出了邊界時才回滾。而且回滾的過程由TranslateAnimation來控制,這樣的好處在代碼的解釋中。我是基於網絡上的listviewpress改了一些(有幾處好像是被篡改了,我又按我的理解將它改正過來,運行后沒問題)。一下是關鍵的代碼,整個代碼見附件中。有不懂的可以問問,大家互相學習。
- package com.listview.test;
- import android.content.Context;
- import android.graphics.Rect;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.GestureDetector;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.GestureDetector.OnGestureListener;
- import android.view.animation.TranslateAnimation;
- import android.widget.ListView;
- public class CustomerListView extends ListView {
- private Context mContext;
- private boolean outBound = false;
- private int distance;
- private int firstOut;
- public CustomerListView(Context c) {
- super(c);
- this.mContext = c;
- }
- public CustomerListView(Context c, AttributeSet attrs) {
- super(c, attrs);
- this.mContext = c;
- }
- public CustomerListView(Context c, AttributeSet attrs, int defStyle) {
- super(c, attrs, defStyle);
- this.mContext = c;
- }
- GestureDetector gestureDetector = new GestureDetector(
- new OnGestureListener() {
- public boolean onDown(MotionEvent e) {
- // TODO Auto-generated method stub
- return false;
- }
- public boolean onFling(MotionEvent e1, MotionEvent e2,
- float velocityX, float velocityY) {
- // TODO Auto-generated method stub
- return false;
- }
- public void onLongPress(MotionEvent e) {
- // TODO Auto-generated method stub
- }
- /**捕捉滑動事件 e1為此處為的ACTION_DOWN事件(無論什么動作,起始都是該動作),而e2是觸發調用onScroll的事件。而在此期間,可能已經
- * 觸發了多次的onScroll,因為我們滑動過程可能比較長,一旦長於某個值,就會觸發一次(即一個Move應該是由多個
- * move事件組成的,開頭當然是個ACTION_DOWN事件),也就會發出一個移動的MotionEvent。但是期間開始此次
- * scroll的e1是唯一的。而distance是最近一次調用onScroll以來的距離(前一個e2和現在e2的距離:比如上次是-30,這次是-60(比如向下拉),
- * 那么 distanceY=-60-(-30)=-30)。
- */
- public boolean onScroll(MotionEvent e1, MotionEvent e2,
- float distanceX, float distanceY) {
- /**
- * firstPos和lastPos是adapter中元素的Id
- */
- int firstPos = getFirstVisiblePosition();
- int lastPos = getLastVisiblePosition();
- int itemCount = getCount();
- /**
- * 滑出邊界,而且是一個極點,即可視部分已經已經不存在了,那么直接回到原點
- */
- if (outBound && firstPos != 0 && lastPos != (itemCount - 1)) {
- scrollTo(0, 0);
- return false;
- }
- /**
- * getChildAt是屏幕上可見的元素的id,比如現在屏幕上可見的是adapter中的
- * 4號到10號,那么你調用getChildAt應該是0~6號
- * listView.getChildAt(i) works where 0 is the very first visible row and
- * (n-1) is the last visible row (where n is the number of visible views you see).
- * 進入該onScroll有4種可能,第一種是剛開始的時候,此時firstPos==0,而且可視的item在getChildAt的
- * 返回也是第一個元素,即adapter元素的index和可視的view的編號一致,所以firstview不為空(lastview也一樣)。
- * 當你向上 滑動時,distanceY是大於0的。此時將不消費此次事件,那么將正常地在沒有超出邊際出滾動。
- * 第二種是,若以上是向下拉,那么應該屬於超出范圍的情況,則要消費此時事件。
- * 第三種和第一種類似,只是到了當剛好顯示最后一個item時,顯然firstView和lastView都將是null,因為
- * 此時的adapter的index和getChildAt的index不是相等的,而是成對應關系,
- * 即index_adp-firstPos=index_getChild,此時你若使用getChildAt(firstpos-firstpos),那返回的
- * 將是非null。同理在lastView。第四種是當在第三情況下,向上拉,那么屬於超出邊界。那么lastView是null這個特征
- * 將可以判斷是否進入了下臨界區。
- * 總結以上四種情況,每當觸發臨界區時(dispatchTouchEvent時getFirstVisiblePosition()==0
- * 和getLastVisiblePosition()==getCount()-1),就可以通過distanceY的方向性判斷是正常的滑動
- * 還是將要滑出臨界區。若是滑出臨界區,說明此次將消費該事件,所以返回true,那么在dispatchTouchEvent
- * 將設置outBand為true,那么第二次再進入時,將可以通過outBand來確定是否出了臨界區。
- *
- * 帶方向的函數:onScrollBy/To和onScroll
- */
- View firstView = getChildAt(firstPos);
- View lastView = getChildAt(lastPos-1);
- /**
- * 記錄下第一次的e2的y軸距離,此次過后outBound就變為了true。這樣distance就是跟蹤最近的一次e2
- * 和最開始一次的e2的距離。
- */
- if (!outBound) {
- firstOut = (int) e2.getRawY();
- }
- if (firstView != null
- && (outBound || (firstPos == 0
- && firstView.getTop() == 0 && distanceY < 0))) {
- distance = (int) (firstOut - e2.getRawY());//此處應為負值,即view向下滑動
- /**
- * scrollBy中的值帶有方向,x若為正,則應該以view中該x點顯示在新的原點上,即拿新的點去
- <span style="white-space: pre;"> </span>*重合y軸,就好像整個布局被往左拉動。
- * y為正,則向上滑動|y|距離。負則相反。
- */
- scrollBy(0, distance / 2);
- Log.v("onScroll", "e2.getRawY():"+e2.getRawY());
- Log.v("onScroll", "distance:"+distance);
- Log.v("onScroll", "distanceY:"+distanceY);
- return true;
- }
- if (lastView == null&&(outBound || (lastPos == itemCount - 1 && distanceY > 0))) {
- Log.d("bottom", "bottom");
- distance = (int) (firstOut - e2.getRawY());//此處應為正直,因為view向上滑動
- scrollBy(0, distance/2);
- return true;
- }
- return false;
- }
- public void onShowPress(MotionEvent e) {
- // TODO Auto-generated method stub
- }
- public boolean onSingleTapUp(MotionEvent e) {
- // TODO Auto-generated method stub
- return false;
- }
- });
- /**
- * 最早響應觸屏事件,按下和釋放響應兩次
- */
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if(getFirstVisiblePosition()==0){
- int act = ev.getAction();
- if ((act == MotionEvent.ACTION_UP || act == MotionEvent.ACTION_CANCEL)
- && outBound) {
- outBound = false;
- }
- if (!gestureDetector.onTouchEvent(ev)) {
- outBound = false;
- } else {
- outBound = true;
- }
- Rect rect = new Rect();
- getLocalVisibleRect(rect);
- /**
- * rect.top是個正的距離值,而TanslateAnimation填的是坐標值(有方向的);
- */
- TranslateAnimation am = new TranslateAnimation(0, 0, -rect.top, 0);
- /**
- * 若此處時間設為0,將導致一陣的抖動,因為完成回滾的速度不是分步,而是直接到終點
- * 因為每次觸發onScroll時都會做一次回滾,而當傳進又一次move時,上一次的move還沒作完
- * 就將被新的一次覆蓋,所以不用擔心產生抖動。所以此處給它設時間就是抓住它需要時間來完成回滾的目標,相當
- * 於給它一個時間的緩沖來實現移動,因為當你在移動時,實際是不需要回滾的,只有你釋放了手指還才需要回滾。
- * 注意,此時調用scrollTo已經將位置返回了0(可以把animation當成是模型,只有使用scrollTo才
- * 能真正觸發該移動,結果是已經知道了的,即移動到原點,而過程是TranslateAnimation參謀的,即
- * scrollTo在移動時會調用onScrollChange來實際移動,而onScrollChange則根據傳入的參數來移動
- * 而TranslateAnimation則可以控制該參數。可以把scrollTo先去掉,就可以發現new top 和
- * after scrollBy是一樣的值)。也就是new Top=0。所以每次迭代相減都是現在的e2減去最初的e2在y軸上的值,
- * 這樣通過scrollBy就可以將view移動到新的位置,而此時top也就又被寫成了新的滑動的位置(是滑動距離的一半位置)。
- * 11-19 23:51:11.101: V/onScroll(18396): after scrollBy top:0
- 11-19 23:51:11.101: V/onScroll(18396): new top:0
- 11-19 23:51:11.249: V/onScroll(18396): after scrollBy top:0
- 11-19 23:51:11.249: V/onScroll(18396): new top:0
- 11-19 23:51:11.288: V/onScroll(18396): after scrollBy top:-6
- 11-19 23:51:11.288: V/onScroll(18396): new top:0
- 11-19 23:51:11.319: V/onScroll(18396): after scrollBy top:-16
- 11-19 23:51:11.319: V/onScroll(18396): new top:0
- 11-19 23:51:11.358: V/onScroll(18396): after scrollBy top:-20
- 11-19 23:51:11.358: V/onScroll(18396): new top:0
- 11-19 23:51:11.374: V/onScroll(18396): after scrollBy top:-27
- 11-19 23:51:11.374: V/onScroll(18396): new top:0
- */
- am.setDuration(300);
- startAnimation(am);
- Log.v("onScroll","after scrollBy top:"+rect.top);
- scrollTo(0, 0);
- getLocalVisibleRect(rect);
- Log.v("onScroll", "new top:"+rect.top);
- }
- Log.d("getLastVisiblePosition()", getLastVisiblePosition()+"");
- Log.d("getCount()", getCount()+"");
- if(getLastVisiblePosition()==getCount()-1){
- int act = ev.getAction();
- if ((act == MotionEvent.ACTION_DOWN || act == MotionEvent.ACTION_CANCEL)
- && outBound) {
- outBound = false;
- }
- if (!gestureDetector.onTouchEvent(ev)) {
- outBound = false;
- } else {
- outBound = true;
- }
- if(outBound){
- Rect rect1 = new Rect();
- getLocalVisibleRect(rect1);
- TranslateAnimation am1 = new TranslateAnimation(0, 0, rect1.top, 0);
- am1.setDuration(300);
- startAnimation(am1);
- scrollTo(0, 0);
- }
- }
- return super.dispatchTouchEvent(ev);
- };
- }