PullToLoadListView 繼承 ListView 並實作 OnScrollListener,支援頂部下拉刷新、底部上拉載入、狀態機制管理、指示器動畫、彈性回彈效果。
private static final int STATE_NON = 0; // 閒置
private static final int STATE_PULL_TO_REFRESH = 1; // 下拉/上拉中
private static final int STATE_RELEASE_TO_REFRESH = 2; // 釋放觸發
private static final int STATE_REFRESHING = 3; // 載入中
ProgressBar、箭頭圖示、提示文字RotateAnimation 箭頭旋轉、ResetAnimation (Thread) 回彈平滑隱藏onTouchEvent 計算位移向量,根據 firstVisibleItem、lastVisibleItem 判斷觸發區域private void init(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
headerView = inflater.inflate(R.layout.view_pull_header, null);
footerView = inflater.inflate(R.layout.view_pull_footer, null);
headerProgressBar = headerView.findViewById(R.id.progressbar);
headerImageArrow = headerView.findViewById(R.id.arrow);
headerText = headerView.findViewById(R.id.text);
// 箭頭旋轉動畫
headerArrowAnim = new RotateAnimation(0, -180,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
headerArrowAnim.setDuration(300);
headerArrowAnim.setFillAfter(true);
measureView(headerView);
measureView(footerView);
headerHeight = headerView.getMeasuredHeight();
footerHeight = footerView.getMeasuredHeight();
// 初始隱藏在頂部/底部外
headerView.setPadding(0, -headerHeight, 0, 0);
footerView.setPadding(0, -footerHeight, 0, 0);
addHeaderView(headerView, null, false);
addFooterView(footerView, null, false);
super.setOnScrollListener(this);
}
private void handleMoveHeaderEvent(MotionEvent ev) {
headerIsHandling = true;
float tempY = ev.getRawY();
float vector = (tempY - prevY) / 2; // 阻尼係數 0.5
prevY = tempY;
if (vector > 0) { // 向下拉
int newPadding = Math.min((int)(headerView.getPaddingTop() + vector), headerHeight / 2);
headerView.setPadding(0, newPadding, 0, 0);
if (state != STATE_REFRESHING) {
changeHeaderViewByState(newPadding > 0 ? STATE_RELEASE_TO_REFRESH : STATE_PULL_TO_REFRESH);
}
} else { // 向上推回
if (state == STATE_RELEASE_TO_REFRESH || state == STATE_PULL_TO_REFRESH) {
int newPadding = Math.max((int)(headerView.getPaddingTop() + vector), -headerHeight);
headerView.setPadding(0, newPadding, 0, 0);
if (newPadding <= -headerHeight) {
changeHeaderViewByState(STATE_NON);
headerIsHandling = false;
} else if (newPadding <= 0) {
changeHeaderViewByState(STATE_PULL_TO_REFRESH);
}
}
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
prevY = ev.getRawY();
break;
case MotionEvent.ACTION_UP:
if (state == STATE_RELEASE_TO_REFRESH) {
if (headerIsHandling) {
changeHeaderViewByState(STATE_REFRESHING);
if (onLoadingListener != null) onLoadingListener.onLoadNew();
}
if (footerIsHandling) {
changeFooterViewByState(STATE_REFRESHING);
if (onLoadingListener != null) onLoadingListener.onLoadMore();
}
} else if (state == STATE_PULL_TO_REFRESH) {
// 未達閾值,回彈重置
resetHeader(); resetFooter();
headerIsHandling = footerIsHandling = false;
}
break;
}
return super.onTouchEvent(ev);
}
static class ResetAnimation extends Thread {
static final int DURATION = 600, INTERVAL = 5;
View view; int originalHeight, paddingTop; boolean run = false;
public void run() {
run = true;
int total = originalHeight * 2 + paddingTop;
int steps = DURATION / INTERVAL;
int piece = total / steps;
for (int t = 0; t < steps; t++) {
final int nextPadding = paddingTop - t * piece;
view.post(() -> { view.setPadding(0, nextPadding, 0, 0); view.postInvalidate(); });
try { sleep(INTERVAL); } catch (InterruptedException ignored) {}
}
run = false;
}
}
public interface OnLoadingListener {
void onLoadNew(); // 下拉刷新
void onLoadMore(); // 上拉載入
}
// Activity 中
PullToLoadListView listView = findViewById(R.id.list);
listView.setOnLoadingListener(new PullToLoadListView.OnLoadingListener() {
@Override public void onLoadNew() {
// 請求第一頁資料,完成後呼叫 listView.loadCompleted()
}
@Override public void onLoadMore() {
// 請求下一頁資料,完成後呼叫 listView.loadCompleted()
}
});
<!-- view_pull_header.xml -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="center" android:padding="10dp">
<ProgressBar android:id="@+id/progressbar" android:layout_width="20dp" android:layout_height="20dp" />
<ImageView android:id="@+id/arrow" android:layout_width="20dp" android:layout_height="20dp" android:src="@drawable/arrow_down" />
<TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下拉刷新" />
</LinearLayout>
AndroidX 推薦使用 SwipeRefreshLayout (下拉刷新) 搭配 RecyclerView + OnScrollListener (上拉載入),或採用 SmartRefreshLayout 第三方庫功能更豐富。