Android自定義ListView實現(xiàn)下拉刷新
首先呈上效果圖

當今APP,哪個沒有點滑動刷新功能,簡直就太落伍了。正因為需求多,因此自然而然開源的也就多。但是若想引用開源庫,則很麻煩,比如PullToRefreshView這個庫,如果把開源代碼都移植到項目中,這是件很繁瑣的事,如果用依賴功能的話,對于強迫癥的我,又很不爽?,F(xiàn)在也有各種自定義ListView實現(xiàn)PullToRefreshListView的控件,無非就是在header加入一個控件,通過setPadding的方式來改變顯示效果。效果已經(jīng)太out了,如意中發(fā)現(xiàn)google自帶的swiperefreshlayout實現(xiàn)的效果挺不錯,但是我發(fā)現(xiàn)這個控件在部分手機上的效果不一樣,估計和v7包相關。因此就有了這篇文章自定義這個喜歡的效果。
首先大概描述一下實現(xiàn)原理:
1、重寫ListView的onTouchEvent,在方法中根據(jù)手指滑動的距離與臨界值判斷,決定當前的狀態(tài),分為四個狀態(tài):RELEASE_TO_REFRESH、PULL_TO_REFRESH、REFRESHING、DONE四個狀態(tài),分別代表釋放刷新、拉動刷新、正在刷新、默認狀態(tài)。
2、重寫ListView的onDraw方法,根據(jù)不同的狀態(tài)值,顯示不同的圖形表示。
3、根據(jù)滑動距離不同,顯示不同的透明度、圓弧角度值、整體圖形的坐標等等。
4、圖形的變化分為兩種:1、手動觸發(fā),滑動一點距離就更新一點坐標。比如PULL_TO_REFRESH狀態(tài),適合在onTouchEvent中的ACTION_MOVE中觸發(fā)。2、動畫自動觸發(fā),比如REFRESHING狀態(tài)和DONE狀態(tài),適合在onTouchEvent中的ACTION_UP方法中觸發(fā),手指一松開就自動觸發(fā)動畫效果。
5、必須在設置了刷新監(jiān)聽器才可以滑動,否則就是一個普通的LIstView。
代碼很簡單,只有兩個文件,并且有很詳細的注釋:
PullToRefreshListView類:
package cc.wxf.view.pull;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;
/**
* Created by ccwxf on 2016/3/30.
*/
public class PullToRefreshListView extends ListView implements AbsListView.OnScrollListener {
public final static int RELEASE_TO_REFRESH = 0;
public final static int PULL_TO_REFRESH = 1;
public final static int REFRESHING = 2;
public final static int DONE = 3;
// 達到刷新條件的滑動距離
public final static int TOUCH_SLOP = 160;
// 判斷是否記錄了最開始按下時的Y坐標
private boolean isRecored;
// 記錄最開始按下時的Y坐標
private int startY;
// ListView第一個Item
private int firstItemIndex;
// 當前狀態(tài)
private int state;
// 是否可刷新,只有設置了監(jiān)聽器才能刷新
private boolean isRefreshable;
// 刷新標記
private PullMark mark;
private OnRefreshListener refreshListener;
private OnScrollButtomListener scrollButtomListener;
public PullToRefreshListView(Context context) {
super(context);
init(context);
}
public PullToRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
//關閉硬件加速,否則PullMark的陰影不會出現(xiàn)
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
setOnScrollListener(this);
mark = new PullMark(this);
state = DONE;
isRefreshable = false;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollButtomListener != null) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
if (view.getLastVisiblePosition() == view.getAdapter().getCount() - 1) {
scrollButtomListener.onScrollToButtom();
}
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
firstItemIndex = firstVisibleItem;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mark.onDraw(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
mark.setCenterX(width / 2);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isRefreshable) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
handleActionDown(event);
break;
case MotionEvent.ACTION_UP:
handleActionUp();
break;
case MotionEvent.ACTION_MOVE:
handleActionMove(event);
break;
default:
break;
}
return super.onTouchEvent(event);
}
private void handleActionMove(MotionEvent event) {
int tempY = (int) event.getY();
if (!isRecored && firstItemIndex == 0) {
isRecored = true;
startY = tempY;
}
if (state != REFRESHING && isRecored) {
if (state == RELEASE_TO_REFRESH) {
setSelection(0);
if ((tempY - startY < TOUCH_SLOP) && (tempY - startY) > 0) {
state = PULL_TO_REFRESH;
}
}
if (state == PULL_TO_REFRESH) {
setSelection(0);
if (tempY - startY >= TOUCH_SLOP) {
state = RELEASE_TO_REFRESH;
} else if (tempY - startY <= 0) {
state = DONE;
}
}
if (state == DONE) {
if (tempY - startY > 0) {
state = PULL_TO_REFRESH;
}
}
mark.change(state, tempY - startY);
}
}
private void handleActionUp() {
if (state == PULL_TO_REFRESH) {
state = DONE;
mark.changeByAnimation(state);
} else if (state == RELEASE_TO_REFRESH) {
state = REFRESHING;
mark.changeByAnimation(state);
onRefresh();
}
isRecored = false;
}
private void handleActionDown(MotionEvent event) {
if (firstItemIndex == 0 && !isRecored) {
isRecored = true;
startY = (int) event.getY();
}
}
private void onRefresh() {
if (refreshListener != null) {
refreshListener.onRefresh();
}
}
public void startRefresh() {
state = REFRESHING;
mark.changeByAnimation(state);
onRefresh();
}
public void stopRefresh() {
state = DONE;
mark.changeByAnimation(state);
}
public void setOnRefreshListener(OnRefreshListener refreshListener) {
this.refreshListener = refreshListener;
isRefreshable = true;
}
/**
* 刷新監(jiān)聽器
*/
public interface OnRefreshListener {
public void onRefresh();
}
public void setOnScrollButtomListener(OnScrollButtomListener scrollButtomListener) {
this.scrollButtomListener = scrollButtomListener;
}
/**
* 滑動到最低端觸發(fā)監(jiān)聽器
*/
public interface OnScrollButtomListener {
public void onScrollToButtom();
}
}
刷新標志類:
package cc.wxf.view.pull;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
/**
* Created by ccwxf on 2016/3/30.
*/
public class PullMark {
//背景面板的半徑、顏色
private static final int RADIUS_PAN = 40;
private static final int COLOR_PAN = Color.parseColor("#fafafa");
//面板陰影的半徑、顏色
private static final int RADIUS_SHADOW = 5;
private static final int COLOR_SHADOW = Color.parseColor("#d9d9d9");
//面板中間的圓弧的半徑、顏色、粗度、開始繪制角度
private static final int RADIUS_ARROWS = 20;
private static final int COLOR_ARROWS = Color.GREEN;
private static final int BOUND_ARROWS = 6;
private static final int START_ANGLE = 0;
// 開始繪制角度的變化率、總體繪制角度、總體繪制透明度
private static final int RATIO_SATRT_ANGLE = 3;
private static final int ALL_ANGLE = 270;
private static final int ALL_ALPHA = 255;
// 動畫的高度漸變比率、時間刷新間隔
private static final float RATIO_TOUCH_SLOP = 7f;
private static final long RATIO_ANIMATION_DURATION = 10;
private PullToRefreshListView listView;
// 中點的X、Y坐標、初始隱藏時的Y坐標
private float doneCenterY = -(RADIUS_PAN + RADIUS_SHADOW) / 2;
private float centerX;
private float centerY = doneCenterY;
// 開始繪制的角度、需要繪制的角度、透明度
private int startAngle = START_ANGLE;
private int sweepAngle = startAngle;
private int alpha;
// 弧度變化比率,根據(jù)總體高度與總體弧度角度的比例決定
private float radioAngle = ALL_ANGLE * 1.0f / PullToRefreshListView.TOUCH_SLOP;
// 透明度變化比率,根據(jù)總體高度與總體透明度的比例決定
private float radioAlpha = ALL_ALPHA * 1.0f / PullToRefreshListView.TOUCH_SLOP;
// PullToRefreshListView的狀態(tài)
private int state;
// 當前手指滑動的距離
private float mTouchLength;
// 是否啟動旋轉(zhuǎn)動畫
private boolean isRotateAnimation = false;
// 畫筆
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Handler handler = new Handler();
public PullMark(PullToRefreshListView listView) {
this.listView = listView;
}
/**
* 設置繪制的中點X坐標,在PullToRefreshListView的onMeasure中實現(xiàn)
* @param centerX
*/
public void setCenterX(int centerX){
this.centerX = centerX;
}
/**
* 表示一次普通的數(shù)據(jù)變化,在onTouchEvent中的ACTION_MOVE中觸發(fā)
* @param state
* @param mTouchLength
*/
public void change(int state, float mTouchLength){
this.state = state;
this.mTouchLength = mTouchLength;
// 改變繪制的Y坐標
centerY = doneCenterY + mTouchLength;
// 改變繪制的透明度
alpha = (int) (mTouchLength * radioAlpha);
if(alpha > ALL_ALPHA){
alpha = ALL_ALPHA;
}else if(alpha < 0){
alpha = 0;
}
//改變繪制的起始角度
startAngle = startAngle + RATIO_SATRT_ANGLE;
if(startAngle >= 360){
startAngle = 0;
}
//改變繪制的弧度角度
sweepAngle = (int) (mTouchLength * radioAngle);
if(sweepAngle > ALL_ANGLE){
sweepAngle = ALL_ANGLE;
}else if(sweepAngle < 0){
sweepAngle = 0;
}
listView.invalidate();
}
/**
* 表示一次動畫的變化,在onTouchEvent的ACTION_UP中或者手動startRefresh以及手動stopRefresh中觸發(fā)
* @param state
*/
public void changeByAnimation(final int state){
this.state = state;
if(state == PullToRefreshListView.DONE){
//結束旋轉(zhuǎn)動畫(關閉正在刷新的效果)
isRotateAnimation = false;
}
//慢慢變化到起始位置
handler.postDelayed(new RunnableMove(state), RATIO_ANIMATION_DURATION);
}
/**
* 啟動移動的處理
*/
public class RunnableMove implements Runnable{
private int state;
private int destination;
private float slop;
public RunnableMove(int state) {
this.state = state;
if(state == PullToRefreshListView.DONE){
destination = 0;
slop = RATIO_TOUCH_SLOP;
}else if(state == PullToRefreshListView.REFRESHING){
destination = PullToRefreshListView.TOUCH_SLOP;
slop = RATIO_TOUCH_SLOP * 5;
}
}
@Override
public void run() {
if(mTouchLength > destination){
mTouchLength -= slop;
change(state, mTouchLength);
handler.postDelayed(this, RATIO_ANIMATION_DURATION);
}else{
if(state == PullToRefreshListView.DONE){
// 直接將坐標初始化,否則會有一點點誤差
centerY = doneCenterY;
listView.invalidate();
}else if(state == PullToRefreshListView.REFRESHING){
//啟動旋轉(zhuǎn)的動畫效果
isRotateAnimation = true;
handler.postDelayed(new RunnableRotate(), RATIO_ANIMATION_DURATION);
}
}
}
}
/**
* 旋轉(zhuǎn)動畫的處理
*/
public class RunnableRotate implements Runnable{
@Override
public void run() {
if(isRotateAnimation){
//啟動動畫旋轉(zhuǎn)效果
startAngle = startAngle + RATIO_SATRT_ANGLE;
if(startAngle >= 360){
startAngle = 0;
}
listView.invalidate();
handler.postDelayed(this, RATIO_ANIMATION_DURATION);
}else{
//回到初始位置
handler.postDelayed(new RunnableMove(state), RATIO_ANIMATION_DURATION);
}
}
}
/**
* 繪制刷新圖標的標志
* @param mCanvas
*/
public void onDraw(Canvas mCanvas){
//繪制背景圓盤和陰影
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(COLOR_PAN);
mPaint.setShadowLayer(RADIUS_SHADOW, 0, 0, COLOR_SHADOW);
mCanvas.drawCircle(centerX, centerY, RADIUS_PAN, mPaint);
//繪制圓弧
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(COLOR_ARROWS);
mPaint.setStrokeWidth(BOUND_ARROWS);
mPaint.setAlpha(alpha);
mCanvas.drawArc(new RectF(centerX - RADIUS_ARROWS, centerY - RADIUS_ARROWS, centerX + RADIUS_ARROWS, centerY + RADIUS_ARROWS),
startAngle, sweepAngle, false, mPaint);
}
}
使用的時候,必須要設置了監(jiān)聽器才能有效的滑動:
final PullToRefreshListView listView = (PullToRefreshListView) findViewById(R.id.listView);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new String[]{
"測試1","測試2","測試3","測試4","測試5","測試6",
});
listView.setAdapter(adapter);
listView.setOnRefreshListener(new PullToRefreshListView.OnRefreshListener() {
@Override
public void onRefresh() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
listView.stopRefresh();
}
}, 2000);
}
});
兩個源代碼文件就搞定了,demo工程就不提供了,很簡單的。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Android-自定義控件之ListView下拉刷新的實現(xiàn)
- Android自定義漸變式炫酷ListView下拉刷新動畫
- Android ListView實現(xiàn)上拉加載下拉刷新和滑動刪除功能
- Android自定義listview布局實現(xiàn)上拉加載下拉刷新功能
- Android XListView下拉刷新和上拉加載更多
- Android ListView下拉刷新上拉自動加載更多DEMO示例
- Android實現(xiàn)上拉加載更多以及下拉刷新功能(ListView)
- Android ListView實現(xiàn)上拉加載更多和下拉刷新功能
- Android開發(fā)之ListView列表刷新和加載更多實現(xiàn)方法
- Android仿XListView支持下拉刷新和上劃加載更多的自定義RecyclerView
- Android使用ListView實現(xiàn)下拉刷新及上拉顯示更多的方法
相關文章
Android獲取應用程序名稱(ApplicationName)示例
本文以實例方式為大家介紹下獲取應用程序名稱(ApplicationName)的具體實現(xiàn),感興趣的各位可以參考下哈2013-06-06
android將圖片轉(zhuǎn)換存到數(shù)據(jù)庫再從數(shù)據(jù)庫讀取轉(zhuǎn)換成圖片實現(xiàn)代碼
有時候我們想把圖片存入到數(shù)據(jù)庫中,盡管這不是一種明智的選擇,但有時候還是不得以會用到,下面說說將圖片轉(zhuǎn)換成byte[]數(shù)組存入到數(shù)據(jù)庫中去,并從數(shù)據(jù)庫中取出來轉(zhuǎn)換成圖像顯示出來2013-11-11
vscode通過wifi調(diào)試真機的Flutter應用的教程
這篇文章主要介紹了vscode通過wifi調(diào)試真機的Flutter應用的教程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04
Android中協(xié)調(diào)滾動布局的實現(xiàn)代碼
這篇文章主要介紹了Android中協(xié)調(diào)滾動常用的布局實現(xiàn),類似這樣的協(xié)調(diào)滾動布局,當?shù)撞苛斜砘瑒拥臅r候,頂部的布局做響應的動作,我們都可以通過?AppBarLayout?和?MotionLayout?來實現(xiàn),本文通過實例代碼介紹的非常詳細,需要的朋友參考下吧2022-06-06

