当前位置: 移动技术网 > 移动技术>移动开发>Android > 安卓(android)怎么实现下拉刷新

安卓(android)怎么实现下拉刷新

2019年07月24日  | 移动技术网移动技术  | 我要评论

这里我们将采取的方案是使用组合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方法,通知刷新结束。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网