ListView的反弹效果的实现(一)

来源:互联网 发布:中国怎么没抓达喇 知乎 编辑:程序博客网 时间:2024/06/05 17:38

借鉴一网友的原文,bug已经更改修复。

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
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 ElasticListView extends ListView {

 private boolean outBound = false;
 private int distance;
 private int firstOut;

 @SuppressLint("NewApi")
 public ElasticListView(Context c) {
  super(c);
  if(Build.VERSION.SDK_INT >=9)
   setOverScrollMode(View.OVER_SCROLL_NEVER);
 }

 @SuppressLint("NewApi")
 public ElasticListView(Context c, AttributeSet attrs) {
  super(c, attrs);
  if(Build.VERSION.SDK_INT >=9)
   setOverScrollMode(View.OVER_SCROLL_NEVER);
 }

 @SuppressLint("NewApi")
 public ElasticListView(Context c, AttributeSet attrs, int defStyle) {
  super(c, attrs, defStyle);
  if(Build.VERSION.SDK_INT >=9)
   setOverScrollMode(View.OVER_SCROLL_NEVER);
 }

 GestureDetector gestureDetector = new GestureDetector(
   new OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
     return false;
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY) {
     return false;
    }
    @Override
    public void onLongPress(MotionEvent e) {

    }

    /**
     * e1为此处为的ACTION_DOWN事件
     * (无论什么动作,起始都是该动作),而e2是触发调用onScroll的事件。而在此期间,可能已经
     * 触发了多次的onScroll,因为我们滑动过程可能比较长,一旦长于某个值,就会触发一次(即一个Move应该是由多个
     * move事件组成的,开头当然是个ACTION_DOWN事件),也就会发出一个移动的MotionEvent。但是期间开始此次scroll的e1是唯一的 。
     * 而distance是最近一次调用onScroll以来的距离(前一个e2和现在e2的距离:比如上次是
     * -30,这次是-60(比如向下拉), 那么 distanceY=-60-(-30)=-30)。
     */
    @Override
    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号
      * 进入该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来确定是否出了临界区。
      */
     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 || (firstView.getTop() == 0 && distanceY < 0)) && firstPos == 0) {
      distance = (int) (firstOut - e2.getRawY());// 此处应为负值,即view向下滑动
      /**
       * scrollBy中的值带有方向x若为正,则应该以view中该x点显示在新的原点上,即拿新的点去
       * *重合y轴,就好像整个布局被往左拉动。 y为正,则向上
       * 滑动|y|距离。负则相反。
       */
      scrollBy(0, distance / 2);
      return true;
     }
     if (lastView == null&& (outBound || (lastPos == itemCount - 1 && distanceY > 0))) {
      distance = (int) (firstOut - e2.getRawY());// 此处应为正直,因为view向上<span
      scrollBy(0,-distance / 2);
      return true;
     }
     return false;
    }
    @Override
    public void onShowPress(MotionEvent e) {

    }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
     return false;
    }
   });

 /**
  * 最早响应触屏事件,按下和释放响应两次
  */
 @Override
 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填的是坐标值(有<span
    */
   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也就又被写成了新的滑动的位置(是滑动距离的一半位置)。
    */
   am.setDuration(300);
   startAnimation(am);
   scrollTo(0, 0);
   getLocalVisibleRect(rect);
  }
  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);
 };
}

 

原创粉丝点击