如何实现例如iOS的listview 的弹性效果

来源:互联网 发布:驻韩大使 间谍 知乎 编辑:程序博客网 时间:2024/05/16 04:35
首先介绍一下实现的基本原理。主要的实现机制是为ListView添加一个headerView,  该headerView的原始高度为0,监听触摸事件,根据下拉的距离动态改变headerView的高度,并且让headerView及时重绘,在放开手指时,重新设置headerView的高度为0,这样的话listView就会回弹到原始状态。  实现  下面以代码的形式介绍实现机制:  1 首先创建一个PullListView,  继承自SDK中的ListView类,在构造方法中创建一个HeaderView,设置HeaderView的高度为0,并且调用addHeaderView方法添加HeaderView。代码如下所示:  /**  * 构造函数  * @param context  */  public PullListView(Context context) {  super(context);  setOnScrollListener(this);  //创建PullListView的headerview  headView = new View(this.getContext());  headView.setBackgroundColor(Color.WHITE);  headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));  this.addHeaderView(headView);  }  2  覆盖父类ListView的onTouchEvent方法, 监听下拉手势。  在ACTION_DOWN事件中判断是否已经滑动到顶部。如果滑动到顶部,则记录下来手势的起点状态,如果在按下时,没有滑动到顶部,也就是第一个Item不可见,那么就不记录这个状态,还是让listview执行它默认的行为。代码如下所示:  switch (event.getAction()) {  case MotionEvent.ACTION_DOWN:  if (firstItemIndex == 0 ) {  isRecored = true;  startY = (int) event.getY();  }  break;  这里的firstItemIndex是成员变量,表示第一个可见的Item的索引是不是为0,  如果为0就表示已经滑动到顶部,再继续下拉时就可以显示弹性效果。PullListView实现了OnScrollListener接口, 在构造方法中设置本身的OnScrollListener,监听滚动事件,并且根据滚动事件改变firstItemIndex的值。代码如下:  public class PullListView extends ListView implements OnScrollListener{  public PullListView(Context context) {  super(context);  setOnScrollListener(this);  public void onScroll(AbsListView view, int firstVisiableItem,  int visibleItemCount, int totalItemCount) {  firstItemIndex = firstVisiableItem;  }  public void onScrollStateChanged(AbsListView view, int scrollState) {  currentScrollState = scrollState;  }  在ACTION_MOVE事件中,监听下拉手势。并且只有记录下了起点状态才能执行相关逻辑,如果没有记录下起点状态,那么再次监听随着移动,是否滑动到顶点,如果在MOVE事件的过程中,ListView滑动到了第一个条目,那么同样记录下起点状态,如果再继续下滑,就可以执行弹性效果的相关逻辑。执行弹性效果的相关逻辑之前,还要判断是不是向下滑动,如果是向上滑动的,则不执行任何操作。在下滑的过程中计算滑动的距离,随着下滑距离的增加,改变headView的高度,并且请求重绘。相关代码如下:  case MotionEvent.ACTION_MOVE:  if (!isRecored && firstItemIndex == 0 ) {  isRecored = true;  startY = (int) event.getY();  }  if(!isRecored){  break;  }  int tempY = (int) event.getY();  int moveY = tempY - startY;  if(moveY < 0){  break;  }  headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT,  (int)(moveY * PULL_FACTOR)));  headView.invalidate();  break;  }  PULL_FACTOR是一个因子,它被定义成一个float类型的常量,值为0.6。这个因子的作用是实现下拉时ListView跟随手指移动的延迟效果,例如向下滑动了100像素,  那么ListView并不会向下移动100像素,  而是移动60像素。这样的话就有了一个延迟,在下拉时就感觉不那么生硬。  在ACTION_UP事件中监听手指离开屏幕的操作,在离开屏幕时,  设置headView的高度为0,并且请求重绘,  那么ListView就回弹到初始状态。优化之前的代码如下所示:  case MotionEvent.ACTION_CANCEL:  case MotionEvent.ACTION_UP:  if(!isRecored){  break;  }  headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));  headView.invalidate();  isRecored = false;  break;  这样的话能实现回弹效果,  但是headView的高度从一个较大的值瞬间变成0,同样让用户感觉生硬,容易闪瞎用户的眼  。那么怎么解决这个问题呢?我们知道ACTION_MOVE事件时按一定的频率触发的,所以在ACTION_MOVE中能够多次改变headview的高度,并且它的高度是逐渐增加的,这样就有平滑的效果,能够让ListView跟随用户手指的移动而移动。但是ACTION_UP事件在整个手势期间只会触发一次,所以无法达到渐变的效果。那么我们只能模拟这种渐变效果。在这里,  我使用的是Java  5线程并发库中的可调度线程池(ScheduledExecutorService)。该类能够按一定的频率重复多次执行一个任务。在按一定的频率执行任务时,  每次都会使用一个预定义的Handler对象发送消息,并且在处理消息时,递减headview的高度并重绘,等到headview的高度递减到0时,就停掉这个周期性的任务。相关代码如下:  private Handler handler = new Handler(){  @Override  public void handleMessage(Message msg) {  super.handleMessage(msg);  AbsListView.LayoutParams params = (LayoutParams) headView.getLayoutParams();  params.height -= PULL_BACK_REDUCE_STEP;  headView.setLayoutParams(params);  headView.invalidate();  if(params.height <= 0){  schedulor.shutdownNow();  }  }  };  case MotionEvent.ACTION_CANCEL:  case MotionEvent.ACTION_UP:  if(!isRecored){  break;  }  //headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));  //headView.invalidate();  schedulor = Executors.newScheduledThreadPool(1);  schedulor.scheduleAtFixedRate(new Runnable() {  @Override  public void run() {  handler.obtainMessage().sendToTarget();  Log.i("testFixedRate", "xxxxxxxxxx");  }  }, 0, PULL_BACK_TASK_PERIOD, TimeUnit.NANOSECONDS);  isRecored = false;  break;  这里定义了两个常量:PULL_BACK_REDUCE_STEP和PULL_BACK_TASK_PERIOD。 PULL_BACK_REDUCE_STEP表示headview高度每次递减的像素数,这里定义为1; PULL_BACK_TASK_PERIOD表示间隔多长时间递减一次headview的高度,这里定义为700,注意单位是纳秒。也就是说,  在回弹时,每间隔700纳秒递减一次headview的高度,每次递减1个像素。这两个值是经过测试而设定的,如果设置不恰当,会使回弹过快或过慢,  并且在回弹的过程中出现一跳一跳的卡顿现象。  这里为什么要用handler呢?因为任务是在子线程中调度的,而在子线程中不能操作view,也就是不能设置view的宽度,所以要用一个handler在主线程中处理。  上面就是该PullListView的所有实现。因为代码并不是很多,所以在下面给出所有代码。  全部代码  import java.util.concurrent.Executors;  import java.util.concurrent.ScheduledExecutorService;  import java.util.concurrent.TimeUnit;  import android.content.Context;  import android.graphics.Color;  import android.os.Handler;  import android.os.Message;  import android.util.AttributeSet;  import android.view.MotionEvent;  import android.view.View;  import android.widget.AbsListView;  import android.widget.AbsListView.OnScrollListener;  import android.widget.ListView;  /**  * 下拉时具有弹性的ListView  * @author zhangjg  * @date Dec 21, 2013 4:54:29 PM  */  public class PullListView extends ListView implements OnScrollListener{  private static final String TAG = "PullListView";  //下拉因子,实现下拉时的延迟效果  private static final float PULL_FACTOR = 0.6F;  //回弹时每次减少的高度  private static final int PULL_BACK_REDUCE_STEP = 1;  //回弹时递减headview高度的频率, 注意以纳秒为单位  private static final int PULL_BACK_TASK_PERIOD = 700;  //记录下拉的起始点  private boolean isRecored;  //记录刚开始下拉时的触摸位置的Y坐标  private int startY;  //第一个可见条目的索引  private int firstItemIndex;  //用于实现下拉弹性效果的headView  private View headView;  private int currentScrollState;  //实现回弹效果的调度器  private ScheduledExecutorService  schedulor;  //实现回弹效果的handler,用于递减headview的高度并请求重绘  private Handler handler = new Handler(){  @Override  public void handleMessage(Message msg) {  super.handleMessage(msg);  AbsListView.LayoutParams params = (LayoutParams) headView.getLayoutParams();  //递减高度  params.height -= PULL_BACK_REDUCE_STEP;  headView.setLayoutParams(params);  //重绘  headView.invalidate();  //停止回弹时递减headView高度的任务  if(params.height <= 0){  schedulor.shutdownNow();  }  }  };  /**  * 构造函数  * @param context  */  public PullListView(Context context) {  super(context);  init();  }  /**  * 构造函数  * @param context  * @param attr  */  public PullListView(Context context, AttributeSet attr) {  super(context, attr);  init();  }  /**  * 初始化  */  private void init() {  //监听滚动状态  setOnScrollListener(this);  //创建PullListView的headview  headView = new View(this.getContext());  //默认白色背景,可以改变颜色, 也可以设置背景图片  headView.setBackgroundColor(Color.WHITE);  //默认高度为0  headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT, 0));  this.addHeaderView(headView);  }  /**  * 覆盖onTouchEvent方法,实现下拉回弹效果  */  @Override  public boolean onTouchEvent(MotionEvent event) {  switch (event.getAction()) {  case MotionEvent.ACTION_DOWN:  //记录下拉起点状态  if (firstItemIndex == 0 ) {  isRecored = true;  startY = (int) event.getY();  }  break;  case MotionEvent.ACTION_CANCEL:  case MotionEvent.ACTION_UP:  if(!isRecored){  break;  }  //以一定的频率递减headview的高度,实现平滑回弹  schedulor = Executors.newScheduledThreadPool(1);  schedulor.scheduleAtFixedRate(new Runnable() {  @Override  public void run() {  handler.obtainMessage().sendToTarget();  }  }, 0, PULL_BACK_TASK_PERIOD, TimeUnit.NANOSECONDS);  isRecored = false;  break;  case MotionEvent.ACTION_MOVE:  if (!isRecored && firstItemIndex == 0 ) {  isRecored = true;  startY = (int) event.getY();  }  if(!isRecored){  break;  }  int tempY = (int) event.getY();  int moveY = tempY - startY;  if(moveY < 0){  isRecored = false;  break;  }  headView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.FILL_PARENT,  (int)(moveY * PULL_FACTOR)));  headView.invalidate();  break;  }  return super.onTouchEvent(event);  }  public void onScroll(AbsListView view, int firstVisiableItem,  int visibleItemCount, int totalItemCount) {  firstItemIndex = firstVisiableItem;  }  public void onScrollStateChanged(AbsListView view, int scrollState) {  currentScrollState = scrollState;  }
0 0