转载请注明出处:http://blog.youkuaiyun.com/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是极简单的,今天就先到这儿吧,下拉刷新的代码后面补上。