Android中100行代码实现可上下拉动的自定义ListView
转载请注明出处:http://blog.csdn.net/bettarwang/article/details/41634729
之前在网上也看到一些所谓的下拉刷新的例子,但是总感觉是把简单的事情复杂化了,动辄300多行甚至600多行的代码,其实主要就是对触摸事件作出反应嘛,根本用不着这么麻烦。下面先实现一个可上下拉动的ListView,再实现一个带有Header的可下拉刷新的ListView:
可上下拉动的ListView的源码如下:
/** * 可上下拉动的ListView * @author Bettar * */ public class RefreshableListView extends ListView { private static final String TAG="RefreshableListView"; private int touchSlop; private int initTopMargin; //private int initTopOfFirstChild; private boolean hasRecord=false; private float startY; private boolean isPulling=false; //private ViewGroup.LayoutParams params; private LinearLayout.LayoutParams params; public RefreshableListView(Context context, AttributeSet attrs) { super(context, attrs); //这样的话就可以将设置参数读入,从而不会与layout文件的设置产生冲突。 params=new LinearLayout.LayoutParams(context, attrs); initTopMargin=params.topMargin; this.setLayoutParams(params); touchSlop=ViewConfiguration.get(context).getTouchSlop(); } @Override public boolean onTouchEvent(MotionEvent event) { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: if(!hasRecord) { hasRecord=true; startY=event.getY(); Log.i(TAG,"ACTION_DOWN"); } break; case MotionEvent.ACTION_MOVE: float distance=event.getY()-startY; if(!isPulling) { if(!couldPull(distance)) { Log.i(TAG,"could not pull in ACTION_MOVE"); return false; } } isPulling=true; Log.i(TAG,"pull in ACTION_MOVE"); params.topMargin+=distance; this.setLayoutParams(params); this.setPressed(false); this.setFocusable(false); this.setFocusableInTouchMode(false); return true; case MotionEvent.ACTION_UP: Log.i(TAG,"ACTION_UP"); params.topMargin=initTopMargin; this.setLayoutParams(params); hasRecord=false; this.setFocusable(true); this.setFocusableInTouchMode(true); if(isPulling) { isPulling=false; //注意:拉伸后放起必须返回true,否则这个事件还会被其他的事件处理器读取,从而影响该类的外部操作,如setOnItemClickListener中的操作。 return true; } isPulling=false; break; } return super.onTouchEvent(event); } private boolean couldPull(float distance) { if(Math.abs(distance)<touchSlop) { return false; } if(distance>0) { Log.i(TAG,"getTop()"+this.getTop()); if(this.getFirstVisiblePosition()==0&&this.getChildAt(0).getTop()==0) { return true; } } else { if(this.getLastVisiblePosition()==this.getCount()-1) { return true; } } return false; } }
要注意的一个细节是ACTION_UP时的处理,如果是拉伸后放开手指的ACTION_UP,那么要返回true而不是false,否则会影响这个自定义ListView的正常使用,因为如果返回false的话则这整个过程由于有ACTION_DOWN和ACTION_UP,会被当作一次Click,从而影响造成额外的影响。
如果要加上一定的动画,也很简单,使用补间动画或者异步任务去实现,下面的代码使用了两种实现方式:
package com.android.customview; import android.content.Context; import android.os.AsyncTask; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.TranslateAnimation; import android.widget.AbsListView; import android.widget.LinearLayout; import android.widget.ListView; /** * 可上下拉动的ListView * @author Bettar * */ public class RefreshableListView extends ListView { private static final String TAG="RefreshableListView"; //0.5的话会感觉很粘滞,而1.0的话又感觉太滑,0.8是一个比较好的参数。 private static final float RATIO=0.8f; private static final int ANIM_DURATION=1000; private int touchSlop; private int initTopMargin; private int[]initLocation=new int[2]; private boolean hasRecord=false; private float startY; private boolean isPulling=false; //private ViewGroup.LayoutParams params; private LinearLayout.LayoutParams params; public RefreshableListView(Context context, AttributeSet attrs) { super(context, attrs); //params=this.getLayoutParams(); //这样的话就可以将设置参数读入,从而不会与layout文件的设置产生冲突。 params=new LinearLayout.LayoutParams(context, attrs); initTopMargin=params.topMargin; this.getLocationOnScreen(initLocation); //initTopOfFirstChild=this.getChildAt(0).getTop(); this.setLayoutParams(params); touchSlop=ViewConfiguration.get(context).getTouchSlop(); } @Override public boolean onTouchEvent(MotionEvent event) { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: if(!hasRecord) { hasRecord=true; startY=event.getY(); Log.i(TAG,"ACTION_DOWN"); } break; case MotionEvent.ACTION_MOVE: float distance=event.getY()-startY; if(!isPulling) { if(!couldPull(distance)) { Log.i(TAG,"could not pull in ACTION_MOVE"); return false; } } isPulling=true; Log.i(TAG,"pull in ACTION_MOVE"); params.topMargin=initTopMargin+(int)(distance*RATIO); this.setLayoutParams(params); this.setPressed(false); this.setFocusable(false); this.setFocusableInTouchMode(false); return true; case MotionEvent.ACTION_UP: Log.i(TAG,"ACTION_UP"); if(isPulling) { startTranslateAnimation(); //executeTranslateAnimation(); } //重设参数,注意如果是使用自定义动画,那么要将此处的reset();注释,等到异步任务执行完毕后再执行reset();否则参数会相互干扰。 reset(); if(isPulling) { isPulling=false; //注意:拉伸后放起必须返回true,否则这个事件还会被其他的事件处理器读取,从而影响该类的外部操作,如setOnItemClickListener中的操作。 return true; } isPulling=false; break; } return super.onTouchEvent(event); } private void reset() { params.topMargin=initTopMargin; this.setLayoutParams(params); hasRecord=false; this.setFocusable(true); this.setFocusableInTouchMode(true); } private void startTranslateAnimation() { int[]location=new int[2]; RefreshableListView.this.getLocationOnScreen(location); //测试发现location[0]==0而location[1]就是第一个Item上端距离顶部的距离。 Log.i(TAG,"location[0]="+location[0]+" location[1]="+location[1]); TranslateAnimation anim=new TranslateAnimation(location[0],initLocation[0],location[1],initLocation[1]); anim.setDuration(ANIM_DURATION); RefreshableListView.this.startAnimation(anim); } /** *这其实就相当于自己去实现动画了。 */ private void executeTranslateAnimation() { new TranslateTask(20).execute(); } /** * 如果是使用它的话就要将params的参数等放到异步任务执行完之后再完成,否则会现相互干扰的情况。 * @author Bettar * */ private class TranslateTask extends AsyncTask<Void,Integer,Integer> { //每次线程的睡眠时间 private int deltaSleepTime; private int deltaScrollY; public TranslateTask(int deltaSleepTime) { this.deltaSleepTime=deltaSleepTime; if(deltaSleepTime>0) { deltaScrollY=0-(params.topMargin-initTopMargin)/(ANIM_DURATION/deltaSleepTime); } else { deltaScrollY=params.topMargin>initTopMargin?-20:20; } Log.i(TAG,"deltaScrollY="+deltaScrollY); } @Override protected Integer doInBackground(Void...voidParams) { int topMargin=params.topMargin; while(true) { topMargin+=deltaScrollY; Log.i(TAG,"topMargin="+topMargin); if(deltaScrollY<0) { if(topMargin<0) { topMargin=0; break; } } else { if(topMargin>0) { topMargin=0; break; } } publishProgress(topMargin); try { Thread.sleep(deltaSleepTime); } catch(InterruptedException ex) { ex.printStackTrace(); } } publishProgress(0); return topMargin; } @Override protected void onProgressUpdate(Integer... values) { //values[0]对应上面publisProgress中的topMargin Log.i(TAG,"values[0] i.e topMargin="+values[0]); params.topMargin=values[0]; RefreshableListView.this.setLayoutParams(params); } @Override protected void onPostExecute(Integer result) { //执行完异步任务之后就可以进行参数重新设置了 reset(); } } /** * 判断是否可以开始拉动,如果是向下拉动,则要求第一个Item完全可见;如果是向上拉,则要求最后一个Item完全可见。 * @param distance * @return */ private boolean couldPull(float distance) { if(Math.abs(distance)<touchSlop) { return false; } if(distance>0) { Log.i(TAG,"getTop()"+this.getTop()); if(this.getFirstVisiblePosition()==0&&this.getChildAt(0).getTop()==0) //if(this.getFirstVisiblePosition()==0&&this.getChildAt(0).getTop()==initTopOfFirstChild) { return true; } } else { if(this.getLastVisiblePosition()==this.getCount()-1) { return true; } } return false; } }
相信到了这里,要做一个带下拉刷新头的ListView是极简单的,今天就先到这儿吧,下拉刷新的代码后面补上。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。