您的位置:首页 > 移动开发 > Android开发

自定义ListView实现下拉刷新上拉加载功能

2017-05-18 13:58 399 查看

1,概述

本案例主要继承自ListView,通过添加"头布局"以及"尾布局",然后再监听onTouchEvent事件,实现AbsListView.OnScrollListener接口,来监听滑动到顶部以及底部等状态来隐藏与显示view来达到效果的,Github地址:PullRefreshListView


2,实现步骤

1,创建“头布局”与“尾布局”

HeaderView:
<?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="center"
android:orientation="horizontal">

<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginRight="5dp"
android:layout_marginTop="10dp">

<ImageView
android:id="@+id/refresh__header_arrow"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:src="@mipmap/refresh_arrow" />

<ImageView
android:id="@+id/refresh_header_load"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/refresh_loding"
android:visibility="invisible" />
</FrameLayout>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:gravity="center_horizontal"
android:orientation="vertical">

<TextView
android:id="@+id/refresh_header_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下拉刷新"
android:textColor="#777"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
显示效果如下:



FooterView:

<?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="center"
android:orientation="horizontal">

<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginRight="5dp">

<ImageView
android:id="@+id/refresh__header_arrow"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center"
android:visibility="invisible"
android:src="@mipmap/refresh_arrow" />

<ImageView
android:id="@+id/refresh_footer_load"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/refresh_loding"
android:visibility="visible" />
</FrameLayout>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:gravity="center_horizontal"
android:orientation="vertical">

<TextView
android:id="@+id/refresh_footer_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加载中..."
android:textColor="#777"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>


显示效果如下:



2:创建PullRefreshListView

public class PullRefreshListView extends ListView implements AbsListView.OnScrollListener {
/**
* 顶部刷控件
*/
private View mHeader;//顶部刷新控件
private ImageView refreshArrow, refreshHeaderLoad;
private TextView tvRefreshHeaderStatus;
private int mHeaderHeight;//顶部刷新控件的高度
private int mMinTopPadding = 30;//最小下拉padding
private int mMaxTopPadding = 0;//最大下拉padding

/**
* 底部加载控件
*/
private View mFooter;//顶部刷新控件
private ImageView refreshFooterLoad;
private int mFooterHeight;//底部刷新控件的高度

private int mMaxBottomPadding = 0;//最大上拉padding

private int mMinBottomPadding = 30;//最小上拉padding
}


在构造方法中初始化这些控件:

/**
* 初始化顶部刷新控件
*
* @param context 上下文对象
*/
private void initHeaderView(Context context) {
mHeader = LayoutInflater.from(context).inflate(R.layout.view_refresh_header_normal, null);
refreshArrow = (ImageView) mHeader.findViewById(R.id.refresh__header_arrow);
refreshHeaderLoad = (ImageView) mHeader.findViewById(R.id.refresh_header_load);
tvRefreshHeaderStatus = (TextView) mHeader.findViewById(R.id.refresh_header_status);
measureView(mHeader);
mHeaderHeight = mHeader.getMeasuredHeight();
mMinTopPadding = -mHeaderHeight;
setTopPadding(mMinTopPadding);
addHeaderView(mHeader);
}

/**
* 设置顶部刷新控件的padding,来控制它的显示与隐藏
*
* @param topPadding 距离顶部的内边距
*/
private void setTopPadding(int topPadding) {
if (mHeader != null && topPadding <= mMaxTopPadding && topPadding >= mMinTopPadding) {
mHeader.setPadding(mHeader.getPaddingLeft(), topPadding, mHeader.getPaddingRight(), mHeader.getPaddingBottom());
mHeader.invalidate();
}
}


其中比较重要的方法measureView(mHeader);只有测量之后,才能得到控件的高,具体方法如下:
/**
* 测量子控件,告诉父控件它占多大的高度和宽度
*
* @param child 要测量的view
*/
private void measureView(View child) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
if (lp == null) {
lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, MeasureSpec.UNSPECIFIED);
int heightSpec;
int tempHeight = lp.height;
if (tempHeight > 0) {
heightSpec = MeasureSpec.makeMeasureSpec(tempHeight, MeasureSpec.EXACTLY);
} else {
heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(widthSpec, heightSpec);
}


得到控件的高度之后,通过setTopPadding方法来实现显示与隐藏控件的效果,初始传入的数值为控件高度的负数,FooterView的初始化方法一样

3,监听onTouchEvent事件

在监听触摸事件之前,我们先定义下拉拖动过程中的集中状态,使用枚举类型
public enum RefreshStatus {
IDLE,  //无状态
PULL_DOWN,  //开始下拉状态
RELEASE_REFRESH,  //释放更新状态
REFRESHING   //刷新
4000
中状态
}


初始化时为IDLE状态,接下来重点部分来了
/**
* 如果listview消费了这个事件,就不能滑动了
*
* @param ev 事件
* @return true:消费这个事件
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (RefreshUtils.isAbsListViewToTop(this)) {
mDownY = (int) ev.getY();
isRemark = true;
}
break;
case MotionEvent.ACTION_MOVE:
if (handleAtMove(ev)) {
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (STATE == RefreshStatus.RELEASE_REFRESH) {
STATE = RefreshStatus.REFRESHING;
refreshHeaderStatus();
} else if (STATE == RefreshStatus.PULL_DOWN) {
STATE = RefreshStatus.IDLE;
refreshHeaderStatus();
}
isRemark = false;
mDownY = 0;
break;
}
return super.onTouchEvent(ev);
}


在按下时,我们先不记录按下的位置,因为这时候ListView可能并没有到达最顶端,
判断是否到达最顶端:
/**
* absListView的子类是否已经下拉到最顶部
*
* @param absListView absListView
* @return false
*/
public static boolean isAbsListViewToTop(AbsListView absListView) {
if (absListView != null) {
int firstChildTop = 0;
if (absListView.getChildCount() > 0) {
// 如果AdapterView的子控件数量不为0,获取第一个子控件的top
firstChildTop = absListView.getChildAt(0).getTop() - absListView.getPaddingTop();
}
//第一个child显示的下标以及距离顶部的高度都为0
if (absListView.getFirstVisiblePosition() == 0 && firstChildTop == 0) {
return true;
}
}
return false;
}


在滑动过程中,当达到最顶端时我们再开始记录按下的Y坐标,使用isRemark来标记,主要在handleAtMove(ev)方法:
/**
* 处理移动
*
* @param ev 事件
*/
private boolean handleAtMove(MotionEvent ev) {
if (!isRemark) {//如果不是在最顶端滑动的,当滑动到最顶端是,再来计算
if (RefreshUtils.isAbsListViewToTop(this)) {
mDownY = (int) ev.getY();
isRemark = true;
}
return false;
}
int tempY = (int) ev.getY();
int fy = (int) ((tempY - mDownY) / RATIO);//设置一个下拉系数,造成下拉比较困难的感觉
int padding = fy - mHeaderHeight;
//箭头滑动中的状态变化
switch (STATE) {
case IDLE:
if (fy > 0) {
STATE = RefreshStatus.PULL_DOWN;
refreshHeaderStatus();
}
break;
case PULL_DOWN:
setTopPadding(padding);
if (padding > mMaxTopPadding) {//当下拉到一定高度,状态变成可释放更新状态
STATE = RefreshStatus.RELEASE_REFRESH;
refreshHeaderStatus();
} else if (fy <= 0) {
STATE = RefreshStatus.IDLE;
isRemark = false;
refreshHeaderStatus();
}
break;
case RELEASE_REFRESH:
setTopPadding(padding);
if (padding <= mMaxTopPadding) {//当下拉到一定高度,状态变成可释放更新状态
STATE = RefreshStatus.PULL_DOWN;
refreshHeaderStatus();
}
break;
}
if (fy > 0 && RefreshUtils.isAbsListViewToTop(this)) {
//ACTION_DOWN 时没有消费此事件,那么子空间会处于按下状态,这里设置ACTION_CANCEL,

// 使子控件取消按下状态,否则子控件会执行长按事件

ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
return true;// 当前事件被我们处理并消费--这个特别重要--消费这个事件,listView将无法拖动,这个时候顶部刷新控件的topPadding不断在变大,所以看起来像在下拉的感觉
} else {
return false;
}


当达到顶端且开始下拉的时候,消费这个事件,这个时候ListView处于不可拖动状态,fy大于0,接着我们计算下拉的距离,给HeaderView设置变化的toppadding,这个时候HeaderView慢慢显示出来,同时把ListView向下挤,所以显示出整体往下移的效果。
刚开始下拉的是,进入PULL_DOWN状态,同时刷新UI,下拉的距离达到最大可下拉距离的时候,状态变成RELEASE_REFRESH,接下来往回拉的时候又变成PULL_DOWN状态,继续上拉变成初始状态

各种状态刷新UI的方法:
/**
* 刷新顶部刷新控件的状态
*/
private void refreshHeaderStatus() {
switch (STATE) {
case IDLE:
hiddenRefreshHeaderView();
break;
case PULL_DOWN:
showTopArrowDown();
tvRefreshHeaderStatus.setText(mPullDownRefreshText);
break;
case RELEASE_REFRESH:
showTopArrowUp();
tvRefreshHeaderStatus.setText(mReleaseRefreshText);
break;
case REFRESHING:
setTopPadding(mMaxTopPadding);
showTopLoadView();
tvRefreshHeaderStatus.setText(mRefreshingText);
if (onRefreshCallBack != null) {
onRefreshCallBack.refreshing();
}
break;
}
}


箭头转换动画:
/**
* 初始化箭头动画
*/
private void initAnimation() {
mUpAnim = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mUpAnim.setDuration(200);
mUpAnim.setFillAfter(true);
mDownAnim = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mDownAnim.setDuration(200);
mDownAnim.setFillAfter(true);
}

/**
* 显示顶部箭头向上动画
*/
private void showTopArrowUp() {
refreshArrow.clearAnimation();
refreshArrow.setVisibility(VISIBLE);
refreshHeaderLoad.clearAnimation();
refreshHeaderLoad.setVisibility(GONE);
refreshArrow.startAnimation(mUpAnim);
}


执行动画:
/**
* 显示顶部箭头向下动画
*/
private void showTopArrowDown() {
refreshArrow.clearAnimation();
refreshArrow.setVisibility(VISIBLE);
refreshHeaderLoad.clearAnimation();
refreshHeaderLoad.setVisibility(GONE);
refreshArrow.startAnimation(mDownAnim);
}

/**
* 显示顶部刷新时的动画
*/
private void showTopLoadView() {
refreshArrow.clearAnimation();
refreshArrow.setVisibility(GONE);
refreshHeaderLoad.clearAnimation();
refreshHeaderLoad.setVisibility(VISIBLE);
AnimationDrawable animationDrawable = (AnimationDrawable) refreshHeaderLoad.getDrawable();
animationDrawable.start();
}

/**
* 显示顶部刷新时的动画
*/
private void showBottomLoadView(boolean flag) {
if (flag) {
refreshFooterLoad.clearAnimation();
refreshFooterLoad.setVisibility(VISIBLE);
AnimationDrawable animationDrawable = (AnimationDrawable) refreshFooterLoad.getDrawable();
animationDrawable.start();
} else {
refreshFooterLoad.clearAnimation();
}
}


以上属于下拉刷新部分,上拉加载部分如下:

4,监听滚动接口的变化:

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (isLoadingMore) {//如果正在进行加载更多操作,直接返回
return;
}
boolean isMachScreen;//listView中的条目是否铺满屏幕,不足一屏不允许上拉加载更多

isMachScreen = totalItemCount > visibleItemCount;
Log.e(TAG, "------滑动状态-------" + scrollState);
if (scrollState == SCROLL_STATE_FLING || scrollState == SCROLL_STATE_TOUCH_SCROLL) {
isLoadingMore = isLoadMoreEnable && isMachScreen && RefreshUtils.isAbsListViewToBottom(this);
Log.e(TAG, "------是否可以上拉加载-------" + isLoadingMore);
if (isLoadingMore && onRefreshCallBack != null) {
setBottomPadding(mMaxBottomPadding);
onRefreshCallBack.loading();
}
}
}


是否滚动到最底部:
/**
* absListView的子类是否已经上拉到最底部
*
* @param absListView absListView
* @return false
*/
public static boolean isAbsListViewToBottom(AbsListView absListView) {
//第一步,已经滚动到最后一个子控件
if (absListView == null || absListView.getAdapter() == null || absListView.getAdapter().getCount() <= 0
|| absListView.getLastVisiblePosition() != absListView.getAdapter().getCount() - 1) {
return false;
}

//        View child = absListView.getChildAt(absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition());
//        if(absListView.getHeight() == child.getBottom()){
//            return true;
//        }
return true;
}


最后的效果是,在滚动的时候,只要一滚动到最底端,就显示加载更多布局,同时调用加载更多接口



以上是下拉刷新上拉加载的所有方法,全部代码在GitHub里面,有任何bug欢迎给我留言
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息