自定义ListView实现下拉刷新上拉加载功能
2017-05-18 13:58
399 查看
1,概述
本案例主要继承自ListView,通过添加"头布局"以及"尾布局",然后再监听onTouchEvent事件,实现AbsListView.OnScrollListener接口,来监听滑动到顶部以及底部等状态来隐藏与显示view来达到效果的,Github地址:PullRefreshListView2,实现步骤
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欢迎给我留言
相关文章推荐
- Android 自定义ListView 实现下拉刷新 上拉加载功能
- Android UI--自定义ListView(实现下拉刷新+加载更多)
- Android实现上拉加载更多以及下拉刷新功能(ListView)
- Android UI--自定义ListView(实现下拉刷新+加载更多)
- 安卓自定义ListView实现上拉加载、下拉刷新
- Android UI--自定义ListView(实现下拉刷新+加载更多)
- 自定义ListView实现下拉刷新和分页加载(效果类似知乎)
- 自定义listview实现上拉加载下拉刷新
- android 自定义listview——实现上拉刷新下拉加载的功能
- Android中自定义ListView实现上拉加载更多和下拉刷新
- 自定义布局实现listview上拉加载下拉刷新
- 自定义ListView实现下拉刷新,上拉加载更多
- android ListView上拉加载更多 下拉刷新功能实现(采用pull-to-refresh)
- 关于自定义listview,整合下拉刷新上拉加载功能,以及item侧滑功能,并且解决滑动冲突
- 使用SwipeRefreshLayout和自定义listview实现下拉刷新上啦加载更多
- android ListView上拉加载更多 下拉刷新功能实现(采用pull-to-refresh)
- Android之实现ListView的“下拉刷新”、“上拉加载”、“自动加载”功能(二)
- Android UI--自定义ListView(实现下拉刷新+加载更多)
- 自定义ExpandableListView下拉刷新功能简单实现(这里主要说自定义可下拉的功能)
- 自定义ListView下拉刷新上拉加载功能(面试)