ListView下拉刷新的实现
来源:http://blog.csdn.net/guolin_blog/article/details/9255575
自定义布局类:RefreshableView
import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.RotateAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; /** * 带下拉刷新的布局 */ public class RefreshableView extends LinearLayout implements View.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_FINISHED = 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; /** * 上次更新时间的字符串常量,用于作为SharedPreferences的键值 * 由UPDATED_AT + mId组成为key */ private static final String UPDATED_AT = "updated_at"; /** * 避免同一个文件出现一样的key而覆盖掉了其他下拉刷新界面的时间 * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突(),使用id来做区分 */ private int mId = -1; /** * 下拉刷新的回调接口 */ private PullToRefreshListener mListener; /** * 用于存储上次更新时间,这里文件名定义的:updateTime,记录上一次时间的key=updateTa */ private SharedPreferences preferences; /** * 下拉头的View */ private View header; /** * 需要带下拉刷新的ListView */ private ListView listView; /** * 刷新时显示的进度条 */ private ProgressBar progressBar; /** * 指示下拉和释放的箭头 */ private ImageView arrow; /** * 指示下拉和释放的文字描述 */ private TextView tv_description; /** * 上次更新时间的文字描述 */ private TextView tv_updateAt; /** * 下拉头的布局参数 */ private MarginLayoutParams headerLayoutParams; /** * 上次更新时间的毫秒值 */ private long lastUpdateTime; /** * 下拉头的高度,负数 */ 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; /** * 在被判定为滚动之前用户手指可以移动的最大值。这里为16 * 表示滑动的时候,手的移动距离要大于这个值才能开始移动控件,否则就不触发该控件移动 */ private int touchSlop; /** * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次 */ private boolean loadOnce=false; /** * 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉 */ private boolean ableToPull; /** * 当下拉的距离大于或者等于这个数就更新 */ private int scrollY; /** * 在xml布局文件中引用该类所调用的构造方法 */ public RefreshableView(Context context, AttributeSet attrs) { super(context, attrs); //初始化控件 header=LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true); arrow= (ImageView) header.findViewById(R.id.arrow); progressBar= (ProgressBar) header.findViewById(R.id.progressBar); tv_description= (TextView) header.findViewById(R.id.description); tv_updateAt= (TextView) header.findViewById(R.id.updated_time); preferences=context.getSharedPreferences("updateTime",Context.MODE_PRIVATE); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); Log.i("tag","移动的最大值:"+touchSlop);//16 //初始化下拉头布局中 距离上次刷新时间的值 refreshTextViewUpdateAtValue(); setOrientation(VERTICAL);//设置该布局方向为垂直方向 addView(header,0);//添加下拉头布局到该布局中,0代表第一个子控件 } /** * 确定该布局中子视图的位置,只让该方法执行一次 * 由于下拉头在构造函数中添加到该布局中了,这里需要修改位置 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if(changed && !loadOnce){ headerLayoutParams= (MarginLayoutParams) header.getLayoutParams(); hideHeaderHeight= -header.getHeight();//布局中的高度取反,设置下拉头距离屏幕的高度 scrollY=hideHeaderHeight-hideHeaderHeight/2;//只有滑动值大于这个高度就认为可以刷新 Log.i("tag","滑动值:"+scrollY+"本来的:"+hideHeaderHeight);//-110,-220 //这样就把下拉的头布局放到了屏幕外上面 headerLayoutParams.topMargin=hideHeaderHeight; listView = (ListView) getChildAt(1); listView.setOnTouchListener(this);//为listView设置触摸监听 loadOnce=true;//已经加载了 } } /** * 给下拉刷新控件注册一个监听器。 * * @param listener * 监听器的实现。 * @param id * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。 */ public void setOnRefreshListener(PullToRefreshListener listener, int id) { mListener = listener; mId = id; } /** * listView的触摸监听 */ @Override public boolean onTouch(View v, MotionEvent event) { setIsAbleToPull(event);//设置ableToPull的值,可不可以下拉 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 ){ //Log.i("tag","当前为向上滑:"+distance); return false;//不需要显示下拉头,所以return,不执行下面的下拉头逻辑操作,如果返回true,ListView会滚动不了 } if(distance < touchSlop){ return false;//滑动距离太小也不执行下面对下拉头的操作 } if(currentStatus != STATUS_REFRESHING){//不是正在刷新的状态 if(headerLayoutParams.topMargin >= scrollY){ //显示了下拉头,设置当前状态为释放立即刷新 currentStatus=STATUS_RELEASE_TO_REFRESH; }else{ currentStatus = STATUS_PULL_TO_REFRESH;//下拉开始显示出来的状态 } //通过偏移下拉头的topMargin值,来实现下拉效果 int margin=(distance / 2) + hideHeaderHeight; if(margin>0){ margin=0;//避免下拉的高度过大 } headerLayoutParams.topMargin = margin; header.setLayoutParams(headerLayoutParams); } break; case MotionEvent.ACTION_UP://松手后根据当前状态执行对应的操作 if(currentStatus == STATUS_RELEASE_TO_REFRESH){ currentStatus=STATUS_REFRESHING;//正在刷新 //松手,开始执行释放立即刷新的任务 new UpdateTask().execute(); }else if(currentStatus == STATUS_PULL_TO_REFRESH){ //正在下拉的状态,松手后需要隐藏下拉头的显示 new HideHeaderTask().execute(); } break; } //改变下拉头布局中的控件显示信息 updateHeaderView(); if(currentStatus == STATUS_PULL_TO_REFRESH || currentStatus== STATUS_REFRESHING){ // 当前正处于下拉或释放状态,要让ListView失去焦点,屏蔽掉单击事件,不然松手会触发listView的单击事件 listView.setPressed(false);//不可按压 listView.setFocusable(false); listView.setFocusableInTouchMode(false); lastStatus = currentStatus; // 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件 return true; } } return false; } /** * 更新下拉头中的显示信息。 */ private void updateHeaderView() { if (lastStatus != currentStatus) { if (currentStatus == STATUS_PULL_TO_REFRESH) { //正在下拉 tv_description.setText("下拉可以刷新"); arrow.setVisibility(View.VISIBLE); progressBar.setVisibility(View.GONE); rotateArrow();//执行动画 } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) { tv_description.setText("释放立即刷新"); arrow.setVisibility(View.VISIBLE); progressBar.setVisibility(View.GONE); rotateArrow(); } else if (currentStatus == STATUS_REFRESHING) { tv_description.setText("正在刷新..."); progressBar.setVisibility(View.VISIBLE); arrow.clearAnimation(); arrow.setVisibility(View.GONE); } refreshTextViewUpdateAtValue();//更新时间 }else{ //Log.i("tag","跟记录的上一次状态相同,"+currentStatus); } } /** * 根据当前的状态来旋转箭头。 */ private void rotateArrow() { float pivotX = arrow.getWidth() / 2f; float pivotY = arrow.getHeight() / 2f; float fromDegrees = 0f; float toDegrees = 0f; if (currentStatus == STATUS_PULL_TO_REFRESH) { fromDegrees = 180f; toDegrees = 360f;//正在下拉.360到180度旋转,逆时针180度 } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) { fromDegrees = 0f; toDegrees = 180f;//释放立即刷新,从180到0度,顺时针180度 } RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY); animation.setDuration(100); animation.setFillAfter(true);//保持为运行后的状态 arrow.startAnimation(animation); } /** * 根据当前ListView的滚动状态来设定 {@link #ableToPull}的值, * 每次都需要在onTouch方法中第一个执行,这样可以判断出当前下拉头可不可以进行下拉。 */ private void setIsAbleToPull(MotionEvent event) { View firstChild = listView.getChildAt(0); if(firstChild != null){//存在子条目 int firstVisiblePosition = listView.getFirstVisiblePosition();//当前屏幕显示的第一个item的position值 if(firstVisiblePosition==0 && firstChild.getTop()==0){ if(! ableToPull){ yDown=event.getRawY();//当前为不可下拉状态才获取按下的纵坐标 } //如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新 ableToPull=true; }else{ if(headerLayoutParams.topMargin != hideHeaderHeight){ //说明下拉头已经被拉出,需要恢复成原样隐藏了 headerLayoutParams.topMargin=hideHeaderHeight; header.setLayoutParams(headerLayoutParams); } ableToPull=false; } }else{ //如果ListView中没有元素,也应该允许下拉刷新 ableToPull = true; } } /** * 刷新下拉头布局中距离上次更新时间的值 */ private void refreshTextViewUpdateAtValue() { lastUpdateTime=preferences.getLong(UPDATED_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_MINUTE;//得到分钟数 updateAtValue="上次更新于"+ timeIntoFormat +"分钟前"; }else if(timePassed < ONE_DAY){//大于一小时,小于一天 timeIntoFormat=timePassed / ONE_HOUR;//得到小时数 updateAtValue="上次更新于"+ timeIntoFormat +"小时前"; }else if(timePassed < ONE_MONTH){//大于一天,小于一个月 timeIntoFormat=timePassed / ONE_DAY;//得到天数 updateAtValue="上次更新于"+ timeIntoFormat +"天前"; }else if(timePassed < ONE_YEAR){//大于一个月,小于一年 timeIntoFormat=timePassed / ONE_MONTH;//得到月数 updateAtValue="上次更新于"+ timeIntoFormat +"个月前"; }else{//大于一年 timeIntoFormat=timePassed / ONE_YEAR;//得到年数 updateAtValue="上次更新于"+ timeIntoFormat +"年前"; } tv_updateAt.setText(updateAtValue);//设置更新时间值 } /** * 执行下拉头刷新的任务 */ class UpdateTask extends AsyncTask<Void,Integer,Void> { @Override protected Void doInBackground(Void... params) { int topMargin = headerLayoutParams.topMargin; if(topMargin>scrollY){ topMargin=scrollY; } publishProgress(topMargin);//让下拉头与屏幕重合 mySleep(2000);//模拟刷新2秒 if (mListener != null) { mListener.onRefresh();//回调接口方法,刷新完成 } currentStatus=STATUS_REFRESH_FINISHED;//刷新完成的状态 preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();//记录这次的刷新时间 //开始隐藏 while(true){ if(topMargin<= hideHeaderHeight){ break; } topMargin=topMargin + SCROLL_SPEED;//每次减20 publishProgress(topMargin);//刷新位置 mySleep(50); } lastStatus = STATUS_REFRESH_FINISHED;//刷新完成 publishProgress(hideHeaderHeight);//刚好隐藏 return null; } @Override protected void onProgressUpdate(Integer... values) { headerLayoutParams.topMargin = values[0]; header.setLayoutParams(headerLayoutParams);//更新下拉头的位置 } private void mySleep(int time){ try { Thread.currentThread().sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 隐藏下拉头的任务 */ class HideHeaderTask extends AsyncTask<Void,Integer,Void> { @Override protected Void doInBackground(Void... params) { int topMargin = headerLayoutParams.topMargin; while(true){ if(topMargin<= hideHeaderHeight){ break; } topMargin=topMargin + SCROLL_SPEED;//每次减20 publishProgress(topMargin);//刷新进度 mySleep(50); } return null; } @Override protected void onProgressUpdate(Integer... values) { headerLayoutParams.topMargin = values[0]; header.setLayoutParams(headerLayoutParams);//更新下拉头的位置 } @Override protected void onPostExecute(Void aVoid) { headerLayoutParams.topMargin = hideHeaderHeight; header.setLayoutParams(headerLayoutParams);//设置为刚好隐藏 lastStatus = STATUS_REFRESH_FINISHED;//设置前一次状态为刷新完成或者未刷新状态 } private void mySleep(int time){ try { Thread.currentThread().sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 下拉刷新的接口回调 */ public interface PullToRefreshListener { /** * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的,你可以不必另开线程来进行耗时操作。 */ void onRefresh(); } }
下拉刷新的下拉头布局文件:pull_to_refresh.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="bottom|center_horizontal" android:background="#ffcccc"> <!--左边的图片--> <LinearLayout android:orientation="vertical" android:layout_width="wrap_content" android:gravity="center|bottom" android:layout_marginBottom="10dp" android:layout_height="100dp"> <ImageView android:id="@+id/arrow" android:src="@drawable/arrow" android:layout_width="40dp" android:layout_height="40dp" /> <ProgressBar android:id="@+id/progressBar" android:visibility="gone" style="@android:style/Widget.ProgressBar.Inverse" android:layout_width="40dp" android:layout_height="40dp" /> </LinearLayout> <!--右边的文字--> <LinearLayout android:orientation="vertical" android:gravity="center|bottom" android:layout_marginBottom="10dp" android:layout_width="wrap_content" android:layout_height="100dp"> <TextView android:id="@+id/description" android:text="更新描述信息" android:textSize="15sp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/updated_time" android:text="更新时间" android:textSize="15sp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
=============================使用方法:=========================================
布局文件activity_main.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:layout_height="match_parent" tools:context="com.ts.listviewpulltorefresh.MainActivity"> <com.ts.listviewpulltorefresh.RefreshableView android:id="@+id/pull_refresh" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"/> </com.ts.listviewpulltorefresh.RefreshableView> </RelativeLayout>
MainActivity.java
/** * LIstView下拉刷新 */ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final RefreshableView pullRefreshView = (RefreshableView) findViewById(R.id.pull_refresh); ListView listView= (ListView) pullRefreshView.findViewById(R.id.listView); ArrayList<String> list=new ArrayList<String>(); for (int i = 0; i < 15; i++) { list.add("item"+i); } ArrayAdapter<String> adapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list); //设置刷新完成的监听 pullRefreshView.setOnRefreshListener(new RefreshableView.PullToRefreshListener() { @Override public void onRefresh() { Log.i("tag","刷新完成"); } },1);//1为区分不同刷新界面的id //单击事件 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Log.i("tag","单击了ListView----"+position); } }); listView.setAdapter(adapter); } }
效果图: