安卓(android)怎么实现下拉刷新
这里我们将采取的方案是使用组合view的方式,先自定义一个布局继承自linearlayout,然后在这个布局中加入下拉头和listview这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有listview了。然后对listview的touch事件进行监听,如果当前listview已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏。那我们现在就来动手实现一下,新建一个项目起名叫pulltorefreshtest,先在项目中定义一个下拉头的布局文件pull_to_refresh.xml,代码如下所示:
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/pull_to_refresh_head" android:layout_width="fill_parent" android:layout_height="dip" > <linearlayout android:layout_width="dip" android:layout_height="dip" android:layout_centerinparent="true" android:orientation="horizontal" > <relativelayout android:layout_width="dip" android:layout_height="dip" android:layout_weight="" > <imageview android:id="@+id/arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerinparent="true" android:src="@drawable/arrow" /> <progressbar android:id="@+id/progress_bar" android:layout_width="dip" android:layout_height="dip" android:layout_centerinparent="true" android:visibility="gone" /> </relativelayout> <linearlayout android:layout_width="dip" android:layout_height="dip" android:layout_weight="" android:orientation="vertical" > <textview android:id="@+id/description" android:layout_width="fill_parent" android:layout_height="dip" android:layout_weight="" android:gravity="center_horizontal|bottom" android:text="@string/pull_to_refresh" /> <textview android:id="@+id/updated_at" android:layout_width="fill_parent" android:layout_height="dip" android:layout_weight="" android:gravity="center_horizontal|top" android:text="@string/updated_at" /> </linearlayout> </linearlayout> </relativelayout>
•在这个布局中,我们包含了一个下拉指示箭头,一个下拉状态文字提示,和一个上次更新的时间。当然,还有一个隐藏的旋转进度条,只有正在刷新的时候我们才会将它显示出来。布局中所有引用的字符串我们都放在strings.xml中,如下所示:
<?xml version="." encoding="utf-"?> <resources> <string name="app_name">pulltorefreshtest</string> <string name="pull_to_refresh">下拉可以刷新</string> <string name="release_to_refresh">释放立即刷新</string> <string name="refreshing">正在刷新…</string> <string name="not_updated_yet">暂未更新过</string> <string name="updated_at">上次更新于%$s前</string> <string name="updated_just_now">刚刚更新</string> <string name="time_error">时间有问题</string> </resources>
•然后新建一个refreshableview继承自linearlayout,代码如下所示:
public class refreshableview extends linearlayout implements ontouchlistener { //下拉状态 public static final int status_pull_to_refresh = ; //释放立即刷新状态 public static final int status_release_to_refresh = //正在刷新状态 public static final int status_refreshing = ; //刷新完成或未刷新状态 public static final int status_refresh_finished = ; //下拉头部回滚的速度 public static final int scroll_speed = -; //一分钟的毫秒值,用于判断上次的更新时间 public static final long one_minute = * ; //一小时的毫秒值,用于判断上次的更新时间 public static final long one_hour = * one_minute; //一天的毫秒值,用于判断上次的更新时间 public static final long one_day = * one_hour; //一月的毫秒值,用于判断上次的更新时间 public static final long one_month = * one_day; //一年的毫秒值,用于判断上次的更新时间 public static final long one_year = * one_month; //上次更新时间的字符串常量,用于作为sharedpreferences的键值 private static final string updated_at = "updated_at"; //下拉刷新的回调接口 private pulltorefreshlistener mlistener; //用于存储上次更新时间 private sharedpreferences preferences; //下拉头的view private view header; //需要去下拉刷新的listview private listview listview; //刷新时显示的进度条 private progressbar progressbar; //指示下拉和释放的箭头 private imageview arrow; //指示下拉和释放的文字描述 private textview description; //上次更新时间的文字描述 private textview updateat; //下拉头的布局参数 private marginlayoutparams headerlayoutparams; //上次更新时间的毫秒值 private long lastupdatetime; //为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分 private int mid = -; //下拉头的高度 private int hideheaderheight; //当前处理什么状态,可选值有status_pull_to_refresh,status_release_to_refresh,status_refreshing 和 status_refresh_finished private int currentstatus = status_refresh_finished;; //记录上一次的状态是什么,避免进行重复操作 private int laststatus = currentstatus; //手指按下时的屏幕纵坐标 private float ydown; //在被判定为滚动之前用户手指可以移动的最大值。 private int touchslop; //是否已加载过一次layout,这里onlayout中的初始化只需加载一次 private boolean loadonce; //当前是否可以下拉,只有listview滚动到头的时候才允许下拉 private boolean abletopull; //下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。 public refreshableview(context context, attributeset attrs) { super(context, attrs); preferences = preferencemanager.getdefaultsharedpreferences(context); header = layoutinflater.from(context).inflate(r.layout.pull_to_refresh, null, true); progressbar = (progressbar) header.findviewbyid(r.id.progress_bar); arrow = (imageview) header.findviewbyid(r.id.arrow); description = (textview) header.findviewbyid(r.id.description); updateat = (textview) header.findviewbyid(r.id.updated_at); touchslop = viewconfiguration.get(context).getscaledtouchslop(); refreshupdatedatvalue(); setorientation(vertical); addview(header, ); } //进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给listview注册touch事件。 protected void onlayout(boolean changed, int l, int t, int r, int b) { super.onlayout(changed, l, t, r, b); if (changed && !loadonce) { hideheaderheight = -header.getheight(); headerlayoutparams = (marginlayoutparams) header.getlayoutparams(); headerlayoutparams.topmargin = hideheaderheight; listview = (listview) getchildat(); listview.setontouchlistener(this); loadonce = true; } } //当listview被触摸时调用,其中处理了各种下拉刷新的具体逻辑。 public boolean ontouch(view v, motionevent event) { setisabletopull(event); if (abletopull) { switch (event.getaction()) { case motionevent.action_down: ydown = event.getrawy(); break; case motionevent.action_move: float ymove = event.getrawy(); int distance = (int) (ymove - ydown); // 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件 if (distance <= && headerlayoutparams.topmargin <= hideheaderheight) { return false; } if (distance < touchslop) { return false; } if (currentstatus != status_refreshing) { if (headerlayoutparams.topmargin > ) { currentstatus = status_release_to_refresh; } else { currentstatus = status_pull_to_refresh; } // 通过偏移下拉头的topmargin值,来实现下拉效果 headerlayoutparams.topmargin = (distance / ) + hideheaderheight; header.setlayoutparams(headerlayoutparams); } break; case motionevent.action_up: default: if (currentstatus == status_release_to_refresh) { // 松手时如果是释放立即刷新状态,就去调用正在刷新的任务 new refreshingtask().execute(); } else if (currentstatus == status_pull_to_refresh) { // 松手时如果是下拉状态,就去调用隐藏下拉头的任务 new hideheadertask().execute(); } break; } // 时刻记得更新下拉头中的信息 if (currentstatus == status_pull_to_refresh || currentstatus == status_release_to_refresh) { updateheaderview(); // 当前正处于下拉或释放状态,要让listview失去焦点,否则被点击的那一项会一直处于选中状态 listview.setpressed(false); listview.setfocusable(false); listview.setfocusableintouchmode(false); laststatus = currentstatus; // 当前正处于下拉或释放状态,通过返回true屏蔽掉listview的滚动事件 return true; } } return false; } //给下拉刷新控件注册一个监听器。 //为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。 public void setonrefreshlistener(pulltorefreshlistener listener, int id) { mlistener = listener; mid = id; } // 当所有的刷新逻辑完成后,记录调用一下,否则你的listview将一直处于正在刷新状态。 public void finishrefreshing() { currentstatus = status_refresh_finished; preferences.edit().putlong(updated_at + mid, system.currenttimemillis()).commit(); new hideheadertask().execute(); } //根据当前listview的滚动状态来设定 {@link #abletopull}的值,每次都需要在ontouch中第一个执行,这样可以判断出当前应该是滚动listview,还是应该进行下拉。 private void setisabletopull(motionevent event) { view firstchild = listview.getchildat(); if (firstchild != null) { int firstvisiblepos = listview.getfirstvisibleposition(); if (firstvisiblepos == && firstchild.gettop() == ) { if (!abletopull) { ydown = event.getrawy(); } // 如果首个元素的上边缘,距离父布局值为,就说明listview滚动到了最顶部,此时应该允许下拉刷新 abletopull = true; } else { if (headerlayoutparams.topmargin != hideheaderheight) { headerlayoutparams.topmargin = hideheaderheight; header.setlayoutparams(headerlayoutparams); } abletopull = false; } } else { // 如果listview中没有元素,也应该允许下拉刷新 abletopull = true; } } //更新下拉头中的信息。 private void updateheaderview() { if (laststatus != currentstatus) { if (currentstatus == status_pull_to_refresh) { description.settext(getresources().getstring(r.string.pull_to_refresh)); arrow.setvisibility(view.visible); progressbar.setvisibility(view.gone); rotatearrow(); } else if (currentstatus == status_release_to_refresh) { description.settext(getresources().getstring(r.string.release_to_refresh)); arrow.setvisibility(view.visible); progressbar.setvisibility(view.gone); rotatearrow(); } else if (currentstatus == status_refreshing) { description.settext(getresources().getstring(r.string.refreshing)); progressbar.setvisibility(view.visible); arrow.clearanimation(); arrow.setvisibility(view.gone); } refreshupdatedatvalue(); } } //根据当前的状态来旋转箭头。 private void rotatearrow() { float pivotx = arrow.getwidth() / f; float pivoty = arrow.getheight() / f; float fromdegrees = f; float todegrees = f; if (currentstatus == status_pull_to_refresh) { fromdegrees = f; todegrees = f; } else if (currentstatus == status_release_to_refresh) { fromdegrees = f; todegrees = f; } rotateanimation animation = new rotateanimation(fromdegrees, todegrees, pivotx, pivoty); animation.setduration(); animation.setfillafter(true); arrow.startanimation(animation); } //刷新下拉头中上次更新时间的文字描述。 private void refreshupdatedatvalue() { lastupdatetime = preferences.getlong(updated_at + mid, -); long currenttime = system.currenttimemillis(); long timepassed = currenttime - lastupdatetime; long timeintoformat; string updateatvalue; if (lastupdatetime == -) { updateatvalue = getresources().getstring(r.string.not_updated_yet); } else if (timepassed < ) { updateatvalue = getresources().getstring(r.string.time_error); } else if (timepassed < one_minute) { updateatvalue = getresources().getstring(r.string.updated_just_now); } else if (timepassed < one_hour) { timeintoformat = timepassed / one_minute; string value = timeintoformat + "分钟"; updateatvalue = string.format(getresources().getstring(r.string.updated_at), value); } else if (timepassed < one_day) { timeintoformat = timepassed / one_hour; string value = timeintoformat + "小时"; updateatvalue = string.format(getresources().getstring(r.string.updated_at), value); } else if (timepassed < one_month) { timeintoformat = timepassed / one_day; string value = timeintoformat + "天"; updateatvalue = string.format(getresources().getstring(r.string.updated_at), value); } else if (timepassed < one_year) { timeintoformat = timepassed / one_month; string value = timeintoformat + "个月"; updateatvalue = string.format(getresources().getstring(r.string.updated_at), value); } else { timeintoformat = timepassed / one_year; string value = timeintoformat + "年"; updateatvalue = string.format(getresources().getstring(r.string.updated_at), value); } updateat.settext(updateatvalue); } //正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器。 class refreshingtask extends asynctask<void, integer, void> { protected void doinbackground(void... params) { int topmargin = headerlayoutparams.topmargin; while (true) { topmargin = topmargin + scroll_speed; if (topmargin <= ) { topmargin = ; break; } publishprogress(topmargin); sleep(); } currentstatus = status_refreshing; publishprogress(); if (mlistener != null) { mlistener.onrefresh(); } return null; } protected void onprogressupdate(integer... topmargin) { updateheaderview(); headerlayoutparams.topmargin = topmargin[]; header.setlayoutparams(headerlayoutparams); } } //隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。 class hideheadertask extends asynctask<void, integer, integer> { protected integer doinbackground(void... params) { int topmargin = headerlayoutparams.topmargin; while (true) { topmargin = topmargin + scroll_speed; if (topmargin <= hideheaderheight) { topmargin = hideheaderheight; break; } publishprogress(topmargin); sleep(); } return topmargin; } protected void onprogressupdate(integer... topmargin) { headerlayoutparams.topmargin = topmargin[]; header.setlayoutparams(headerlayoutparams); } protected void onpostexecute(integer topmargin) { headerlayoutparams.topmargin = topmargin; header.setlayoutparams(headerlayoutparams); currentstatus = status_refresh_finished; } } //使当前线程睡眠指定的毫秒数。指定当前线程睡眠多久,以毫秒为单位 private void sleep(int time) { try { thread.sleep(time); } catch (interruptedexception e) { e.printstacktrace(); } } //下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调。 public interface pulltorefreshlistener { //刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。 void onrefresh(); } }
•这个类是整个下拉刷新功能中最重要的一个类,注释已经写得比较详细了,我再简单解释一下。首先在refreshableview的构造函数中动态添加了刚刚定义的pull_to_refresh这个布局作为下拉头,然后在onlayout方法中将下拉头向上偏移出了屏幕,再给listview注册了touch事件。之后每当手指在listview上滑动时,ontouch方法就会执行。在ontouch方法中的第一行就调用了setisabletopull方法来判断listview是否滚动到了最顶部,只有滚动到了最顶部才会执行后面的代码,否则就视为正常的listview滚动,不做任何处理。当listview滚动到了最顶部时,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,下拉的距离设定为手指移动距离的1/2,这样才会有拉力的感觉。如果下拉的距离足够大,在松手的时候就会执行刷新操作,如果距离不够大,就仅仅重新隐藏下拉头。
•具体的刷新操作会在refreshingtask中进行,其中在doinbackground方法中回调了pulltorefreshlistener接口的onrefresh方法,这也是大家在使用refreshableview时必须要去实现的一个接口,因为具体刷新的逻辑就应该写在onrefresh方法中,后面会演示使用的方法。
•另外每次在下拉的时候都还会调用updateheaderview方法来改变下拉头中的数据,比如箭头方向的旋转,下拉文字描述的改变等。更加深入的理解请大家仔细去阅读refreshableview中的代码。
现在我们已经把下拉刷新的所有功能都完成了,接下来就要看一看如何在项目中引入下拉刷新了。打开或新建activity_main.xml作为程序主界面的布局,加入如下代码:
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".mainactivity" > <com.example.pulltorefreshtest.refreshableview android:id="@+id/refreshable_view" android:layout_width="fill_parent" android:layout_height="fill_parent" > <listview android:id="@+id/list_view" android:layout_width="fill_parent" android:layout_height="fill_parent" > </listview> </com.example.pulltorefreshtest.refreshableview> </relativelayout>
•可以看到,我们在自定义的refreshableview中加入了一个listview,这就意味着给这个listview加入了下拉刷新的功能,就是这么简单!然后我们再来看一下程序的主activity,打开或新建mainactivity,加入如下代码:
public class mainactivity extends activity { refreshableview refreshableview; listview listview; arrayadapter<string> adapter; string[] items = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l" }; protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); requestwindowfeature(window.feature_no_title); setcontentview(r.layout.activity_main); refreshableview = (refreshableview) findviewbyid(r.id.refreshable_view); listview = (listview) findviewbyid(r.id.list_view); adapter = new arrayadapter<string>(this, android.r.layout.simple_list_item_, items); listview.setadapter(adapter); refreshableview.setonrefreshlistener(new pulltorefreshlistener() { public void onrefresh() { try { thread.sleep(); } catch (interruptedexception e) { e.printstacktrace(); } refreshableview.finishrefreshing(); } }, ); } }
•可以看到,我们通过调用refreshableview的setonrefreshlistener方法注册了一个监听器,当listview正在刷新时就会回调监听器的onrefresh方法,刷新的具体逻辑就在这里处理。而且这个方法已经自动开启了线程,可以直接在onrefresh方法中进行耗时操作,比如向服务器请求最新数据等,在这里我就简单让线程睡眠3秒钟。另外在onrefresh方法的最后,一定要调用refreshableview中的finishrefreshing方法,这个方法是用来通知refreshableview刷新结束了,不然我们的listview将一直处于正在刷新的状态。
•不知道大家有没有注意到,setonrefreshlistener这个方法其实是有两个参数的,我们刚刚也是传入了一个不起眼的0。那这第二个参数是用来做什么的呢?由于refreshableview比较智能,它会自动帮我们记录上次刷新完成的时间,然后下拉的时候会在下拉头中显示距上次刷新已过了多久。这是一个非常好用的功能,让我们不用再自己手动去记录和计算时间了,但是却存在一个问题。
•如果当前我们的项目中有三个地方都使用到了下拉刷新的功能,现在在一处进行了刷新,其它两处的时间也都会跟着改变!因为刷新完成的时间是记录在配置文件中的,由于在一处刷新更改了配置文件,导致在其它两处读取到的配置文件时间已经是更改过的了。
•那解决方案是什么?就是每个用到下拉刷新的地方,给setonrefreshlistener方法的第二个参数中传入不同的id就行了。这样各处的上次刷新完成时间都是单独记录的,相互之间就不会再有影响。
•让我们来运行一下,看看效果吧。
•效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入listview下拉刷新功能只需三步:
1.在activity的布局文件中加入自定义的refreshableview,并让listview包含在其中。
2.在activity中调用refreshableview的setonrefreshlistener方法注册回调接口。
3.在onrefresh方法的最后,记得调用refreshableview的finishrefreshing方法,通知刷新结束。
推荐阅读