【安卓筆記】下拉重新整理元件的使用及實現

rowandjj發表於2015-01-25
專案中如果需要實現下拉重新整理一般有以下幾個選擇:
1.使用開源庫Android-pullToRefresh。
2.使用support.v4包提供的SwipeRefreshLayout。
3.自己實現一個。

下面分別簡單介紹:
注:以listView下拉重新整理為例.
方案1:使用開源庫Android-pullToRefresh
1.下載Android-PullToRefresh開源庫(https://github.com/chrisbanes/Android-PullToRefresh)
2.將library工程匯入到eclipse中
3.建立一個新工程,並在properties->android選項中引用library庫
4.編寫程式碼

該庫提供了PullToRefreshListView,我們用它替換ListView,它在使用上跟ListView一致。
但是其提供了一個監聽下拉更新的介面,我們通過setOnRefreshListener註冊該監聽器,然後在onRefresh回撥方法中非同步更新資料即可,更新完成後需呼叫onRefreshComplete方法完成更新。
示例:
頁面佈局:
<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.example.flushlistview.MainActivity" >
    <com.handmark.pulltorefresh.library.PullToRefreshListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="#19000000"
        android:dividerHeight="4dp" >
    </com.handmark.pulltorefresh.library.PullToRefreshListView>
</RelativeLayout>

listView的item佈局:
<?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="match_parent"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#000"
        android:textSize="20sp" />
</LinearLayout>
介面邏輯:
package com.example.flushlistview;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
/**
 * @author Rowandjj
 *
 *使用pull-to-refresh庫實現下拉重新整理操作
 */
public class MainActivity extends Activity
{
	private PullToRefreshListView lv;
	private ArrayAdapter<String> adapter;
	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
	    lv = (PullToRefreshListView) findViewById(R.id.lv);
		List<String> list = new ArrayList<String>();
		list.add("張三");
		list.add("李四");
		list.add("王五");
		list.add("趙六");
		list.add("啊啊");
		list.add("呵呵");
		list.add("嘻嘻");
		list.add("嘿嘿");
		adapter = new ArrayAdapter<String>(this,
				R.layout.item, R.id.tv, list);
		lv.setAdapter(adapter);
		//實現重新整理介面
		lv.setOnRefreshListener(new OnRefreshListener<ListView>()
		{
			@Override
			public void onRefresh(PullToRefreshBase<ListView> refreshView)
			{
				Date date = new Date(System.currentTimeMillis());
				SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日hh時mm分ss秒",Locale.CHINA);
				String updateTime = format.format(date);
				
				refreshView.getLoadingLayoutProxy().setLastUpdatedLabel(updateTime);
				//非同步重新整理
				new UpdateTask().execute();
			}
			
		});
	}
	private class UpdateTask extends AsyncTask<Void, Void,List<String>>
	{
		@Override
		protected List<String> doInBackground(Void... params)
		{
			try
			{
				Thread.sleep(2000);
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			List<String> newData = new ArrayList<String>();
			newData.add("新資料1");
			newData.add("新資料2");
			newData.add("新資料3");
			return newData;
		}
		
		@Override
		protected void onPostExecute(List<String> result)
		{
			adapter.addAll(result);
			//重新整理完成
			lv.onRefreshComplete();
		}
		
	}
}
效果:


方案2:使用support.v4包提供的SwipeRefreshLayout
SwipeRefreshLayout是support.v4包提供的一個類,可以實現具有Material Design效果的下拉重新整理。
使用方式上也很簡單,只要將ListView/RecyclerView或者其他View放置在此layout之中,然後呼叫setOnRefreshListener註冊資料更新介面,並在onRefresh方法中處理資料更新事件即可。當資料更新完畢,呼叫setRefreshing並將引數置為false即可。
注:
1. SwipeRefreshLayout只能包裹一個子View。
2.如果你的support.v4包中沒有這個類,那麼需要更新之。

示例:
頁面佈局:
<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.example.swiperefreshlayoutdemo.MainActivity" >
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/activity_main_swipe_refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
        <ListView
            android:id="@+id/activity_main_listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
        </ListView>
    </android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>
item佈局:
<?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:orientation="vertical" >
    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="40dip"
        android:textColor="#000"
        android:gravity="center_vertical"
        android:textSize="18sp" />
</LinearLayout>
頁面邏輯:
package com.example.swiperefreshlayoutdemo;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class MainActivity extends Activity
{
	private ListView mListView;
	private SwipeRefreshLayout mRefreshLayout;
	private ArrayAdapter<String> mAdapter;
	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mListView = (ListView) findViewById(R.id.activity_main_listview);
		mRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.activity_main_swipe_refresh_layout);
		List<String> data = new ArrayList<String>();
		for (int i = 0; i < 15; i++)
		{
			data.add("這是資料" + i);
		}
		mAdapter = new ArrayAdapter<String>(this, R.layout.item, R.id.tv, data);
		mListView.setAdapter(mAdapter);
		mRefreshLayout.setOnRefreshListener(new OnRefreshListener()
		{
			@Override
			public void onRefresh()
			{
				new UpdateTask().execute();
			}
		});
	}
	private class UpdateTask extends AsyncTask<Void, Void, List<String>>
	{
		@Override
		protected List<String> doInBackground(Void... params)
		{
			try
			{
				Thread.sleep(2000);
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			List<String> newData = new ArrayList<String>();
			newData.add("新資料1");
			newData.add("新資料2");
			newData.add("新資料3");
			return newData;
		}
		@Override
		protected void onPostExecute(List<String> result)
		{
			mAdapter.addAll(result);
			//通知資料更新完畢
			mRefreshLayout.setRefreshing(false);
		}
	}
}
效果:

方案3:自己實現一個
要想實現一個下拉重新整理效果需要對android的事件分發、事件回撥機制有清晰的認識。
這裡我直接繼承自ListView,然後通過實現OnScrollListener監聽螢幕滾動事件,因為只有ListView滑動到首部才能下滑重新整理,另外,需要重寫onTouchEvent方法,根據手指位置動態更新佈局。
我們通過分析傳統的下拉重新整理,發現有四種狀態,1.正常態、2.下拉重新整理態、3.釋放重新整理態、4.重新整理態
當手指按下時,首先判斷當前listView的第一個item是否可見,如果不可見那麼不做任何處理,否則記下當前位置。
手指移動時,計算豎直方向的偏移量,然後根據偏移量改變當前狀態,並根據當前狀態更新佈局。這裡的佈局指的是listView的headerView,當然,預設情況下,這個view是隱藏的,我們可以設定其padding為負的headerview的高度。
手指鬆開時,判斷當前狀態,如果是重新整理態,那麼呼叫回撥介面處理更新邏輯。

先貼出程式碼:
package com.example.mypulltorefreshlistview.ui;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.example.mypulltorefreshlistview.R;
public class PullToRefreshListView extends ListView implements OnScrollListener
{
	private static final String TAG = "PullToRefreshListView";
	/**
	 *頂部佈局 
	 */
	private View mHeaderView;
	
	/**
	 * header的高度
	 */
	private int mHeaderHeight;
	
	/**
	 * 當前頁面已經滑到頂部
	 */
	private boolean flag;
	
	/**
	 * 初始滑動時的y座標
	 */
	private int mStartY;
	
	/**
	 * 正常狀態
	 */
	public static final int STATE_NORMAL = 0;
	/**
	 * 下拉重新整理狀態
	 */
	public static final int STATE_PULL_TO_REFRESH = 1;
	/**
	 * 釋放重新整理狀態
	 */
	public static final int STATE_RELEASE_TO_REFRESH = 2;
	/**
	 * 正在重新整理狀態
	 */
	public static final int STATE_REFRESH = 3;
	
	/**
	 * 當前狀態
	 */
	private int mCurrentState;
	
	
	private static final int DEFAULT_LENGTH = 70;
	
	private OnRefreshListener mRefreshListener;
	
	public PullToRefreshListView(Context context, AttributeSet attrs,
			int defStyle)
	{
		super(context, attrs, defStyle);
		init(context);
	}
	public PullToRefreshListView(Context context, AttributeSet attrs)
	{
		super(context, attrs);
		init(context);
	}
	public PullToRefreshListView(Context context)
	{
		super(context);
		init(context);
	}
	public void setOnRefreshListener(OnRefreshListener listener)
	{
		this.mRefreshListener = listener;
	}
	/**
	 * 初始化操作
	 * @param context
	 */
	private void init(Context context)
	{
		//新增headerview
		mHeaderView = LayoutInflater.from(context).inflate(R.layout.header_layout,null);
		this.addHeaderView(mHeaderView);
		//設定滾動監聽器
		this.setOnScrollListener(this);
		
		//通過設定padding將hider隱藏
		//注:因為此時無法獲得header的高度,所以放入MessageQueue中
		post(new HideHeaderAction());
	}
	
	
	/**
	 * 設定header的padding
	 * @param topPadding
	 */
	private void setHeaderTopPadding(int topPadding)
	{
		if(mHeaderView != null)
		{
			mHeaderView.setPadding(mHeaderView.getPaddingLeft(),topPadding,mHeaderView.getPaddingRight(),mHeaderView.getPaddingBottom());
		}
	}
	
	
	@Override
	public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount)
	{
		if(firstVisibleItem == 0)
			flag = true;
		else
			flag = false;
	}
	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState)
	{
	}
	@Override
	public boolean onTouchEvent(MotionEvent ev)
	{
		int action = ev.getAction();
		switch (action)
		{
		case MotionEvent.ACTION_DOWN:
			if(flag)
			{
				mStartY = (int) ev.getY();
			}
			break;
		case MotionEvent.ACTION_MOVE:
			performMove(ev);
			break;
		case MotionEvent.ACTION_UP:
			if(mCurrentState == STATE_RELEASE_TO_REFRESH)
			{
				mCurrentState = STATE_REFRESH;
				updateUIByState();
				//TODO 載入新資料
				if(mRefreshListener == null)
				{
					throw new RuntimeException("you must call setOnRefreshListener before...");
				}else
				{
					mRefreshListener.onRefresh(this);
				}
			}else if(mCurrentState == STATE_PULL_TO_REFRESH)
			{
				mCurrentState = STATE_NORMAL;
				flag = false;
				updateUIByState();
			}
			
			break;
		}
		return super.onTouchEvent(ev);
	}
	private void updateUIByState()
	{
		TextView tip = (TextView) findViewById(R.id.tip);
		ImageView arrow = (ImageView) findViewById(R.id.arrow);
		ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);
		Animation anim1 = AnimationUtils.loadAnimation(getContext(),R.anim.rotate_1);
		Animation anim2 = AnimationUtils.loadAnimation(getContext(),R.anim.rotate_2);
		
		switch (mCurrentState)
		{
		case STATE_NORMAL:
			setHeaderTopPadding(-mHeaderHeight);
			break;
		case STATE_PULL_TO_REFRESH:
			arrow.clearAnimation();
			arrow.setAnimation(anim1);
			arrow.setVisibility(View.VISIBLE);
			progressBar.setVisibility(View.GONE);
			tip.setText("下拉可以重新整理...");
			break;
		case STATE_RELEASE_TO_REFRESH:
			arrow.clearAnimation();
			arrow.setAnimation(anim2);
			arrow.setVisibility(View.VISIBLE);
			progressBar.setVisibility(View.GONE);
			tip.setText("鬆開可以重新整理...");
			break;
		case STATE_REFRESH:
			arrow.clearAnimation();
			setHeaderTopPadding(mHeaderHeight);
			arrow.setVisibility(View.GONE);
			progressBar.setVisibility(View.VISIBLE);
			tip.setText("正在重新整理...");
			break;
		default:
			break;
		}
	}
	
	private void performMove(MotionEvent ev)
	{
		if(!flag)
		{
			return;
		}
		int currY = (int) ev.getY();
		int deltaY = currY-mStartY;
		if(deltaY > mHeaderHeight+DEFAULT_LENGTH)
			deltaY = mHeaderHeight+DEFAULT_LENGTH;
		
		switch (mCurrentState)
		{
		case STATE_NORMAL:
			if(deltaY > 0)
			{
				mCurrentState = STATE_PULL_TO_REFRESH;
			}
			break;
		case STATE_PULL_TO_REFRESH:
			setHeaderTopPadding(deltaY-mHeaderHeight);
			updateUIByState();
			if(deltaY >= mHeaderHeight+DEFAULT_LENGTH)
			{
				mCurrentState = STATE_RELEASE_TO_REFRESH;
			}else if(deltaY <= 0)
			{
				mCurrentState = STATE_NORMAL;
			}
			break;
		case STATE_RELEASE_TO_REFRESH:
//			setHeaderTopPadding(deltaY-mHeaderHeight);
			updateUIByState();
			if(deltaY < mHeaderHeight+DEFAULT_LENGTH)
			{
				mCurrentState = STATE_PULL_TO_REFRESH;
			}else if(deltaY <= 0)
			{
				mCurrentState = STATE_NORMAL;
			}
			break;
		}
		
	}
	
	/**
	 * 更新完畢時呼叫
	 */
	public void refreshComplete()
	{
		TextView lastUpdateTime = (TextView) findViewById(R.id.last_update_time);
		SimpleDateFormat format = new SimpleDateFormat("MM-dd hh:mm",Locale.CHINA);
		String updateTime = format.format(new Date(System.currentTimeMillis()));
		lastUpdateTime.setText("更新於 "+updateTime);
		mCurrentState = STATE_NORMAL;
		updateUIByState();
	}
	
	public interface OnRefreshListener
	{
		public void onRefresh(PullToRefreshListView listView);
	}
	
	private class HideHeaderAction implements Runnable
	{
		@Override
		public void run()
		{
			//獲取header的高度
			mHeaderHeight = mHeaderView.getMeasuredHeight();//view在被渲染前無法獲得其寬高
			Log.d(TAG,"Headerheight:"+mHeaderHeight);
			//通過設定header的padding來隱藏header
			setHeaderTopPadding(-mHeaderHeight);
		}
	}
}
使用上跟上面差不多,也是需要新增監聽器,然後處理回撥方法,資料更新完呼叫refreshComplete。
當然,這個只是一個簡單的demo,效果可能不是很好,僅供參考~









相關文章