欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-02-29 11:48:16
这里我们将采取的方案是使用组合view的方式,先自定义一个布局继承自linearlayout,然后在这个布局中加入下拉头和listview这两个子元素,并让这两个子元素纵向...

这里我们将采取的方案是使用组合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就行了。这样各处的上次刷新完成时间都是单独记录的,相互之间就不会再有影响。

•让我们来运行一下,看看效果吧。

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

•效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入listview下拉刷新功能只需三步:

1.在activity的布局文件中加入自定义的refreshableview,并让listview包含在其中。

2.在activity中调用refreshableview的setonrefreshlistener方法注册回调接口。

3.在onrefresh方法的最后,记得调用refreshableview的finishrefreshing方法,通知刷新结束。