android 自定义控件之下拉刷新源码详解
下拉刷新 是请求网络数据中经常会用的一种功能.
实现步骤如下:
1.新建项目 PullToRefreshDemo,定义下拉显示的头部布局pull_to_refresh_refresh.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:id="@+id/pull_to_refresh_head"
android:layout_height="60dip"
>
<LinearLayout
android:layout_width="200dip"
android:layout_height="60dip"
android:layout_centerInParent="true"
android:orientation="horizontal"
>
<RelativeLayout
android:layout_width="0dip"
android:layout_height="60dip"
android:layout_weight="3"
>
<ImageView
android:id="@+id/iv_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/arrow"
/>
<ProgressBar
android:id="@+id/pb"
android:layout_width="30dip"
android:layout_height="30dip"
android:layout_centerInParent="true"
android:visibility="gone"
/>
</RelativeLayout>
<LinearLayout
android:layout_width="0dip"
android:layout_height="60dip"
android:layout_weight="12"
android:orientation="vertical"
>
<TextView
android:id="@+id/tv_description"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:gravity="center_horizontal|bottom"
android:text="下拉可以刷新"
/>
<TextView
android:id="@+id/tv_update"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:gravity="center_horizontal|top"
android:text="上次更新于%1$s前"
/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
2.新建一个RefreshView继承自LinearLayout.
public class RefreshView extends LinearLayout implements OnTouchListener {
//下拉状态
public static final int STATUS_PULL_TO_REFRESH=0;
//释放立即刷新状态
public static final int STATUS_RELEASE_TO_REFRESH=1;
//正在刷新状态
public static final int STATUS_REFRESHING=2;
//刷新完成或未刷新状态
public static final int STATUS_REFRESH_FINISH=3;
//下拉时头部回滚的速度
public static final int SCROLL_SPEED=-20;
//一分钟的毫秒值,判断上次的更新时间
public static final long ONE_MINUTE=60*1000;
//一小时的毫秒值,用于判断上次的更新时间
public static final long ONE_HOUR=60*ONE_MINUTE;
//一天的毫秒值
public static final long ONE_DAY=24*ONE_HOUR;
//一月的毫秒值
public static final long ONE_MONTH=30*ONE_DAY;
//一年的毫秒值
public static final long ONE_YEAR=12*ONE_MONTH;
//上次更新时间的字符串常量,用来做SharedPreference的键值
public static final String UPDATE_AT="update_at";
//存储上次更新时间
private SharedPreferences mShared;
//下拉时显示的View
private View header;
//下拉刷新的ListView
private ListView lv;
//刷新时显示的进度条
private ProgressBar mProgressBar;
//指示下拉和释放的箭头
private ImageView arrow;
//指示下拉和释放的文字描述
private TextView tv_des;
//上次更新时间的文字描述
private TextView tv_update;
//下拉头的布局参数
private MarginLayoutParams headerLayoutParams;
//上次更新时间的毫秒数
private long lastUpdateTime;
//为了防止不同界面的下拉刷新与上次更新时间互相有冲突,使用id来做区分
private int mId=-1;
//下拉头的高度
private int hideHeaderHeight;
//标志当前是什么状态
private int currentStatus=STATUS_REFRESH_FINISH;
//记录上次的状态是什么,避免进行重复操作
private int lastStatus=currentStatus;
//手指按下时 的屏幕纵坐标
private float yDown;
//在被判断为滚动之前用户手指可以移动的最大值
private int touchSlop;
//判断已加载过一次layout,这里的onLayout的初始化只需加载一次
private boolean loadOnce;
//当前是否可以下拉,只有ListView滚到头才允许下拉
private boolean ableToPull;
//下拉刷新的回调接口
private PullToRefreshListener mListener;
public RefreshView(Context context, AttributeSet attrs) {
super(context, attrs);
mShared=PreferenceManager.getDefaultSharedPreferences(context);
header=LayoutInflater.from(context).inflate(R.layout.pull_to_refresh,null,true);
mProgressBar=(ProgressBar) header.findViewById(R.id.pb);
arrow=(ImageView) header.findViewById(R.id.iv_arrow);
tv_des=(TextView) header.findViewById(R.id.tv_description);
tv_update=(TextView) header.findViewById(R.id.tv_update);
touchSlop=ViewConfiguration.get(context).getScaledTouchSlop()*3; //得到 至少移动的距离
refreshUpdatedAtValue(); //更新文字描述
setOrientation(VERTICAL); //设置摆放方向
addView(header, 0);
}
//进行一些关键的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件
@Override
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; //设置布局的topMargin
header.setLayoutParams(headerLayoutParams);
lv=(ListView) getChildAt(1); //找到 listView 因为第一个Child是上拉头 所以第二个才是 ListView.
lv.setOnTouchListener(this);
loadOnce=true;
}
}
//给下拉刷新控件注册一个监听器
public void setOnRefreshListener(PullToRefreshListener mListener,int id){
this.mListener=mListener;
mId=id;
}
//更新下拉头中的信息
private void updateHeaderView(){
if(lastStatus!=currentStatus){
if(currentStatus==STATUS_PULL_TO_REFRESH){
tv_des.setText("下拉刷新");
arrow.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
rotateArrow();
}
else if(currentStatus==STATUS_RELEASE_TO_REFRESH){
tv_des.setText("释放刷新");
arrow.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
rotateArrow();
}
else if(currentStatus==STATUS_REFRESHING){
tv_des.setText("正在刷新中");
mProgressBar.setVisibility(View.VISIBLE);
arrow.clearAnimation(); //清除动画效果
arrow.setVisibility(View.GONE);
}
refreshUpdatedAtValue();
}
}
//根据当前的状态来旋转箭头
private void rotateArrow(){
float pivoX=arrow.getWidth()/2f;
float pivoY=arrow.getHeight()/2f;
float fromDegress=0f;
float toDegress=0f;
if(currentStatus==STATUS_PULL_TO_REFRESH){
fromDegress=180f;
toDegress=360f;
}
else{
fromDegress=0f;
toDegress=180f;
}
RotateAnimation animation=new RotateAnimation(fromDegress,toDegress,pivoX,pivoY);
animation.setDuration(100);
animation.setFillAfter(true);
arrow.startAnimation(animation);
}
//根据当前listView的滚动状态来设定 ableToPull 的值
//每次都需要在onTouch中的一个执行,这样可以判断出当前滚动的是listView,还是应该进行下拉
private void setIsAbleToPull(MotionEvent event){
View firstView=lv.getChildAt(0);
if(firstView!=null){
int firstVisiblePos=lv.getFirstVisiblePosition(); //获得listView顶头项的是该列数据的第几个
if(firstVisiblePos==0&&firstView.getTop()==0){
if(!ableToPull){
yDown=event.getRawY();
}
//如果首个元素的上边缘,距离父布局值为0,就说明 listView滚到了最顶部,此时允许下拉刷新
ableToPull=true;
}
else{
if(headerLayoutParams.topMargin!=hideHeaderHeight){
headerLayoutParams.topMargin=hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
ableToPull=false;
}
}
}
//当所有刷新的逻辑执行完成后,停止刷新, 并记录
public void finishRefreshing(){
currentStatus=STATUS_REFRESH_FINISH;
mShared.edit().putLong(UPDATE_AT+mId, System.currentTimeMillis()).commit();
new HideHeaderTask().execute();
}
//更新下拉头中上次更新时间的文字描述
private void refreshUpdatedAtValue(){
lastUpdateTime=mShared.getLong(UPDATE_AT+mId,-1); //从配置文件中取出上次更新的时间的毫秒数
long currentTime=System.currentTimeMillis(); //获得当前时间毫秒数
long timePassed=currentTime-lastUpdateTime; //中间相差的毫秒数
long timeIntoFormat;
String updateAtValue;
if(lastUpdateTime==-1){
updateAtValue="暂未更新过";
}
else if(timePassed<0){
updateAtValue="时间故障";
}
else if(timePassed<ONE_MINUTE){
updateAtValue="刚刚更新";
}
else if(timePassed<ONE_HOUR){
timeIntoFormat=timePassed/ONE_HOUR;
String value=timeIntoFormat+"分钟";
updateAtValue=String.format("上次更新于%1$s前",value);
}
else if(timePassed<ONE_DAY){
timeIntoFormat=timePassed/ONE_HOUR;
String value=timeIntoFormat+"小时";
updateAtValue=String.format("上次更新于%1$s前",value);
}
else if(timePassed<ONE_MONTH){
timeIntoFormat=timePassed/ONE_DAY;
String value=timeIntoFormat+"天";
updateAtValue=String.format("上次更新于%1$s前",value);
}
else if(timePassed<ONE_YEAR){
timeIntoFormat=timePassed/ONE_MONTH;
String value=timeIntoFormat+"月";
updateAtValue=String.format("上次更新于%1$s前",value);
}
else{
timeIntoFormat=timePassed/ONE_YEAR;
String value=timeIntoFormat+"年";
updateAtValue=String.format("上次更新于%1$s前",value);
}
tv_update.setText(updateAtValue);
}
//当listView被触摸时调用,其中处理了各种下拉刷新的具体逻辑
@Override
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<=0&&headerLayoutParams.topMargin<=hideHeaderHeight){
return false;
}
if(distance<touchSlop){
return false;
}
if(currentStatus!=STATUS_REFRESHING){
if(headerLayoutParams.topMargin>0){
currentStatus=STATUS_RELEASE_TO_REFRESH;
}
else{
currentStatus=STATUS_PULL_TO_REFRESH;
}
headerLayoutParams.topMargin=(distance/2)+hideHeaderHeight;
header.setLayoutParams(headerLayoutParams); //让 ListView可以弹动
}
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失去焦点,否则被点击的那一项会一直处于选中状态
lv.setPressed(false);
lv.setFocusable(false);
lv.setFocusableInTouchMode(false);
lastStatus=currentStatus;
return true;
}
}
return false;
}
//正在刷新的任务
class RefreshingTask extends AsyncTask<Void, Integer, Void>{
@Override
protected Void doInBackground(Void... params) {
int topMargin=headerLayoutParams.topMargin;
while(true){
topMargin=topMargin+SCROLL_SPEED;
if(topMargin<=0){
topMargin=0;
break;
}
publishProgress(topMargin);
sleep(10);
}
currentStatus=STATUS_REFRESHING;
publishProgress(0);
if(mListener!=null){
mListener.onRefresh(); //通知刷新
}
return null;
}
@Override
protected void onProgressUpdate(Integer... topMargin) {
updateHeaderView();
headerLayoutParams.topMargin=topMargin[0];
header.setLayoutParams(headerLayoutParams);
}
}
//隐藏下拉头的任务
class HideHeaderTask extends AsyncTask<Void, Integer, Integer>{
@Override
protected Integer doInBackground(Void... params) {
int topMargin=headerLayoutParams.topMargin;
while(true){
topMargin=topMargin+SCROLL_SPEED; //慢慢往回收缩
if(topMargin<=hideHeaderHeight){ //判断是不是回到了原位
topMargin=hideHeaderHeight;
break;
}
publishProgress(topMargin); //设置 收缩动作
sleep(10);
}
return topMargin;
}
@Override
protected void onProgressUpdate(Integer... values) {
headerLayoutParams.topMargin=values[0];
header.setLayoutParams(headerLayoutParams);
}
@Override
protected void onPostExecute(Integer result) {
headerLayoutParams.topMargin=result;
header.setLayoutParams(headerLayoutParams);
currentStatus=STATUS_REFRESH_FINISH;
}
}
/**
* 使当前线程睡眠指定的毫秒数。
*
* @param time
* 指定当前线程睡眠多久,以毫秒为单位
*/
private void sleep(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//下拉刷新的监听器
public interface PullToRefreshListener{
void onRefresh();
}
}
首先,在构造函数中动态添加了pull_to_refresh这个布局作为下拉头,然后将onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了Touch事件.
如果在ListView上进行滑动,onTouch就会执行,onTouch首先会用setIsAbleToPull方法判断ListView是否滚动到了最顶部,只有滚动到最顶部才会执行后面的代码,否则就是ListView的正常滚动,不作处理.当ListView滚动到最顶部,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,如果下拉的距离足够大,在松手后就会执行刷新操作,如果距离不够大,则会隐藏下拉头.
具体刷新方法操作在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh()方法.
具体使用方法如下:
3.在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.cy.pulltorefreshDemo.RefreshView
android:id="@+id/refresh_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ListView
android:id="@+id/lv"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="@android:color/transparent"
></ListView>
</com.cy.pulltorefreshDemo.RefreshView>
</RelativeLayout>
只要将需要刷新的ListView包含在 RefreshView中.
4.MainActivity.java
public class MainActivity extends Activity {
RefreshView refreshView;
ListView lv;
ArrayAdapter<String> adapter;
List<String> items=new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
items.add("A");
items.add("B");
refreshView=(RefreshView) findViewById(R.id.refresh_view);
lv=(ListView) findViewById(R.id.lv);
adapter=new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,items);
lv.setAdapter(adapter);
refreshView.setOnRefreshListener(new PullToRefreshListener() {
@Override
public void onRefresh() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
items.add("222");//自动添加 到 ListView中
refreshView.finishRefreshing();
}
}, 0);
}
}
就这样,一个完整的下拉刷新.
qq3061280@163.com