Android ListView 实现上拉加载的示例代码

2020-11-05 10:25:52 浏览数 (1)

本文介绍了Android ListView 实现上拉加载的示例代码,分享给大家,具体如下:

我们先分析一下如何实现 ListView 上拉加载。

  • 当我们上拉的时候,会出现一个提示界面,即 ListView 的 Footer 布局。
  • ListView 要实现滚动,所以要监听 ListView 滚动事件,即 OnScrollListener() 事件。
  • 当我们开始滚动时,Footer 布局才慢慢显示出来,所以需要监听 ListView 的 onTouch() 事件。

实现思路

  1. 首先判断 ListView 加载时机,当 ListView 的 lastVisibleItem == totalItemCount 时表示当前处于 ListView 最底端,此时允许下拉。
  2. 自定义一个 FooterView,将 FooterView 添加到 ListView 底部,在上拉时候的显示和完成时候的隐藏。
  3. 定义一个加载接口,当上拉动作完成时候回调,用于标记状态并加载最新数据进行展示。

1、定义 Footer

Footer 要实现的效果:

第一次上拉时,Footer 逐渐显示,文字显示为下拉可以加载,箭头向上,进度条隐藏。

当松开加载的时候,箭头隐藏,进度条展示,文字改为正在加载。

1、Footer 加载时状态变化

定义一个如上图所示的 Footer 的 XML 文件 footer_layout.xml

代码语言:javascript复制
<?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="wrap_content"
  android:paddingBottom="10dp"
  android:paddingTop="10dp" 

  <LinearLayout
    android:id="@ id/layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_marginTop="10dp"
    android:gravity="center"
    android:orientation="vertical" 

    <TextView
      android:id="@ id/tv_tip"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="下拉可以刷新"
      android:textSize="12sp" / 
  </LinearLayout 

  <ImageView
    android:id="@ id/img_arrow"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginRight="10dp"
    android:layout_toLeftOf="@ id/layout"
    android:src="@drawable/pull_to_refresh_arrow" / 

  <ProgressBar
    android:id="@ id/progress"
    style="@style/progressBar_custom_drawable"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerVertical="true"
    android:layout_marginRight="10dp"
    android:layout_toLeftOf="@ id/img_arrow"
    android:visibility="gone"
    tools:visibility="visible" / 
</RelativeLayout 

2、初始化布局

定义一个 RefreshListView 类继承 ListView,重写构造函数,并将 Footer 添加到 ListView 中。

代码语言:javascript复制
public class RefreshListView extends ListView {
  private View header;
  private int headerHeight;//顶部布局高度
  private int firstVisibleItem;//当前第一个 Item 可见位置
  private float startY;//按下时开始的Y值
  private int scrollState;//当前滚动状态
  
  private View footer;
  private int footerHeight;//底部布局高度
  private float lastY;
  private boolean canLoadMoreEnabled;//是否允许加载更多
  
  public RefreshListView(Context context) {
    super(context);
    initView(context);
  }

  public RefreshListView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView(context);
  }

  public RefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView(context);
  }


  private void initView(Context context) {
    header = LayoutInflater.from(context).inflate(R.layout.header_layout, null);
    footer = LayoutInflater.from(context).inflate(R.layout.footer_layout, null);
    measureView(header);
    measureView(footer);
    //这里获取高度的时候需要先通知父布局header占用的空间
    headerHeight = header.getMeasuredHeight();
    footerHeight = footer.getMeasuredHeight();
    topPadding(-headerHeight);
    bottomPadding(-footerHeight);//用于隐藏 Footer
    this.addHeaderView(header);
    this.addFooterView(footer);
    this.setOnScrollListener(this);
  }
  
  /**
   * 设置 Footer 布局的下边距
   * 以隐藏 Footer
   * @param topPadding
   */
  private void bottomPadding(int bottomPadding) {
    footer.setPadding(footer.getPaddingLeft(), footer.getPaddingTop(),
        footer.getPaddingRight(),
        bottomPadding);
    footer.invalidate();
  }
}

3、实现上拉加载

给 ListView 设置监听

代码语言:javascript复制
public class RefreshListView extends ListView implements AbsListView.OnScrollListener {
  private int firstVisibleItem;//当前第一个 Item 可见位置
  private int scrollState;//当前滚动状态
  
  private void initView(Context context) {
    header = LayoutInflater.from(context).inflate(R.layout.header_layout, null);
    footer = LayoutInflater.from(context).inflate(R.layout.footer_layout, null);
    measureView(header);
    measureView(footer);
    //这里获取高度的时候需要先通知父布局header占用的空间
    headerHeight = header.getMeasuredHeight();
    footerHeight = footer.getMeasuredHeight();
    topPadding(-headerHeight);
    bottomPadding(-footerHeight);
    this.addHeaderView(header);
    this.addFooterView(footer);
    this.setOnScrollListener(this);
  }
  
  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState) {
    this.scrollState = scrollState;
  }

  @Override
  public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    this.firstVisibleItem = firstVisibleItem;
    this.lastVisibleItem = firstVisibleItem   visibleItemCount;
    this.totalItemCount = totalItemCount;
  }
}

加载的时机是判断lastVisibleItem == totalItemCount,而上拉事件我们需要重写 onTouchEvent() 事件,首先定义几个状态。

代码语言:javascript复制
private float lastY;

private static int state;//当前状态
private final static int NONE = 0;//正常状态
private final static int PULL = 1;//下拉状态
private final static int RELEASE = 2;//释放状态
private final static int REFRESHING = 3;//正在刷新状态

在 onTouchEvent 中,在 ACTION_DOWN 时,记录最开始的 Y 值,然后在 ACTION_MOVE 事件中实时记录移动距离 space,不断刷新 FooterView 的 bootomPadding,让它跟随滑动距离进行显示,继续滑动,当 space 大于了 FooterHeight 时,状态给为 RELEASE,表示可以释放进行刷新操作。

代码语言:javascript复制
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//最顶部
if (firstVisibleItem == 0) {//刷新
canRefreshEnabled = true;
startY = ev.getY();
} else if (lastVisibleItem == totalItemCount) {//加载更多
canLoadMoreEnabled = true;
lastY = ev.getY();
}
break;
case MotionEvent.ACTION_MOVE:
onMove(ev);
break;
case MotionEvent.ACTION_UP:
if (state == RELEASE) {//如果已经释放,则可以提示刷新数据
state = REFRESHING;
if (iRefreshListener != null) {
iRefreshListener.onRefresh();
}
if (iLoadMoreListener != null) {
iLoadMoreListener.onLoadMore();
}
} else if (state == PULL) {//如果是在下拉状态,不刷新数据
state = NONE;
}
if (canRefreshEnabled) {
refreshViewByState();
}
if (canLoadMoreEnabled) {
loadViewByState();
}
canLoadMoreEnabled = false;
canRefreshEnabled = false;
break;
}
return super.onTouchEvent(ev);
}
/**
* 判断移动过程中的操作
*
* @param ev
*/
private void onMove(MotionEvent ev) {
int tempY = (int) ev.getY();
int refreshSpace = (int) (tempY - startY);//向下移动的距离
int topPadding = refreshSpace - headerHeight;//在移动过程中不断设置 topPadding
int loadSpace = (int) (lastY - tempY);//向上移动的距离
int bottomPadding = loadSpace - footerHeight;//在移动过程中不断设置 bottomPadding
switch (state) {
case NONE:
//下拉移动距离大于0
if (refreshSpace   0) {
state = PULL; //状态变成下拉状态
refreshViewByState();
}
//上拉移动距离大于0
if (loadSpace   0) {
state = PULL;//状态变成下拉状态
loadViewByState();
}
break;
case PULL:
if (canRefreshEnabled) {
topPadding(topPadding);//在移动过程中不断设置 topPadding,让 Header 随着下拉动作慢慢显示
}
if (canLoadMoreEnabled) {
bottomPadding(bottomPadding);//在移动过程中不断设置 bottomPadding,让 Footer 随着上拉动作慢慢显示
}
//移动距离大于headerHeight并且正在滚动
if (canRefreshEnabled && refreshSpace   (headerHeight   30) && scrollState == SCROLL_STATE_TOUCH_SCROLL) {
state = RELEASE;//提示释放
refreshViewByState();
}
//移动距离大于footerHeight并且正在滚动
if (canLoadMoreEnabled && loadSpace   footerHeight   30 && scrollState == SCROLL_STATE_TOUCH_SCROLL) {
state = RELEASE;//提示释放
loadViewByState();//刷新footer布局
}
break;
case RELEASE:
if (canRefreshEnabled) {
topPadding(topPadding);
//移动距离小于headerHeight
if (refreshSpace < headerHeight   30) {
state = PULL;//提示下拉
} else if (refreshSpace <= 0) {
state = NONE;
}
refreshViewByState();//更新header
}
if (canLoadMoreEnabled) {
bottomPadding(bottomPadding);
//移动距离小于footerHeight
if (loadSpace < footerHeight   30) {
state = PULL;//提示下拉
} else if (loadSpace <= 0) {
state = NONE;
}
loadViewByState();//更新footer
}
break;
}
}

加载数据的时候,要根据状态不断改变 FooterView 的显示,箭头定义一个旋转动画让其跟随滑动距离实现旋转,进度条也设置了逐帧动画实现自定义进度条。

代码语言:javascript复制
private void loadViewByState() {
TextView tip = footer.findViewById(R.id.tv_tip);
ImageView arrow = footer.findViewById(R.id.img_arrow);
ProgressBar progressBar = footer.findViewById(R.id.progress);
progressBar.setBackgroundResource(R.drawable.custom_progress_bar);
AnimationDrawable animationDrawable = (AnimationDrawable) progressBar.getBackground();
//给箭头设置动画
RotateAnimation anim = new RotateAnimation(0, 180,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
RotateAnimation anim1 = new RotateAnimation(180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
anim.setDuration(200);
anim.setFillAfter(true);
anim1.setDuration(200);
anim1.setFillAfter(true);
switch (state) {
case NONE://正常,footer不显示
bottomPadding(-footerHeight);
arrow.clearAnimation();
break;
case PULL://下拉状态
arrow.setVisibility(VISIBLE);//箭头显示,进度条隐藏
progressBar.setVisibility(GONE);
if (animationDrawable.isRunning()) {
animationDrawable.stop();
}
tip.setText("上拉可以加载");
arrow.clearAnimation();
arrow.setAnimation(anim);//箭头向下
break;
case RELEASE://释放状态
arrow.setVisibility(VISIBLE);//箭头显示,进度条隐藏
progressBar.setVisibility(GONE);
if (animationDrawable.isRunning()) {
//停止动画播放
animationDrawable.stop();
}
tip.setText("松开开始加载");
arrow.clearAnimation();
arrow.setAnimation(anim);//箭头向上
break;
case REFRESHING://刷新状态
bottomPadding(50);
arrow.setVisibility(GONE);//箭头显示,进度条隐藏
progressBar.setVisibility(VISIBLE);
animationDrawable.start();
tip.setText("正在加载...");
arrow.clearAnimation();
break;
}
}

4、下拉刷新完成回调

当上拉加载完成时,我们需要实现数据的刷新,并且要通知 Adapter 刷新数据,这里我们定义一个监听接口实现回调即可。回调在 ACTION_UP 的 RELEASE 状态下进行注册。

代码语言:javascript复制
private ILoadMoreListener iLoadMoreListener;
public void setILoadMoreListener(ILoadMoreListener iLoadMoreListener) {
this.iLoadMoreListener = iLoadMoreListener;
}
public interface ILoadMoreListener {
void onLoadMore();
}

5、测试

代码语言:javascript复制
package com.dali.refreshandloadmorelistview;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import java.util.ArrayList;
public class MainActivity extends Activity implements RefreshListView.IRefreshListener, RefreshListView.ILoadMoreListener {
private ArrayList<ApkEntity  apk_list;
private ListAdapter adapter;
private RefreshListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setData();
showList(apk_list);
}
private void showList(ArrayList<ApkEntity  apk_list) {
if (adapter == null) {
listView = (RefreshListView) findViewById(R.id.listview);
listView.setIRefreshListener(this);
listView.setILoadMoreListener(this);
adapter = new ListAdapter(this, apk_list);
listView.setAdapter(adapter);
} else {
adapter.onDateChange(apk_list);
}
}
private void setData() {
apk_list = new ArrayList<ApkEntity ();
for (int i = 0; i < 10; i  ) {
ApkEntity entity = new ApkEntity();
entity.setName("默认数据"   i);
entity.setDes("这是一个神奇的应用");
entity.setInfo("50w用户");
apk_list.add(entity);
}
}
private void setRefreshData() {
for (int i = 0; i < 2; i  ) {
ApkEntity entity = new ApkEntity();
entity.setName("默认数据   刷新"   i);
entity.setDes("这是一个神奇的应用");
entity.setInfo("50w用户");
apk_list.add(0, entity);
}
}
private void setLoadData() {
for (int i = 0; i < 2; i  ) {
ApkEntity entity = new ApkEntity();
entity.setName("默认数据   加载"   (adapter.getCount()   i));
entity.setDes("这是一个神奇的应用");
entity.setInfo("50w用户");
apk_list.add(entity);
}
}
@Override
public void onRefresh() {
//添加刷新动画效果
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
//获取最新数据
setRefreshData();
//通知界面显示数据
showList(apk_list);
//通知 ListView 刷新完成
listView.refreshComplete();
}
}, 2000);
}
@Override
public void onLoadMore() {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
//获取最新数据
setLoadData();
//通知界面显示数据
showList(apk_list);
//通知 ListView 刷新完成
listView.loadMoreComplete();
}
}, 2000);
}
}

GitHub 源码

以上就是本文的全部内容,希望对大家的学习有所帮助。

0 人点赞