Android适配任意View下拉刷新上拉加载,或上下弹性拽动的ViewGroup控件
来源:互联网 发布:java开发实战宝典 pdf 编辑:程序博客网 时间:2024/05/24 07:31
前言
相信各位做Android开发的童鞋都有碰到过产品经理要求页面做成像IOS那样可以上下弹性拽动,或者页面有上拉刷新下拉加载的要求。今天在这里开源一套自己在使用的满足此要求的控件框架。
当年在封装此框架时有参考过csdn其他前辈的代码,不过博客地址找不到了,就只能在此声明了~。
简单展示效果图:
实现思路
实现一个包含三层视图的ViewGroup:
HeaderView是下拉刷新时的动画效果展示区域,ContentView是内容展示区,FooterView是上拉加载时的动画效果展示区域。当你仅仅指向将此ViewGroup作为一个带弹性的控件时,HeaderView与FooterView做空实现就行。
由于此ViewGroup设计初衷为适用于任意View,所以利用断层思维,给HeaderView、ContentView、FooterView分别设计一个IRefresh、IPable、ILoadMore接口,实际的下拉效果、内容展示的容器、上拉加载控件分别实现对应的接口即可。
代码
首先看IReFresh:
public interface IRefresh { /** * 下拉布局初始状态 */ void onRefreshInit(); /** * 释放刷新状态 */ void onReleaseToRefresh(); /** * 正在刷新状态 */ void onRefreshing(); /** * 刷新成功 */ void onRefreshSuccess(); /** * 刷新失败 */ void onRefreshFailed(); /** * 刷新完毕 */ void onRefreshDone();}
这里我设计了6种刷新时可能会遇到的状态,应该是包含了所有可能遇到的状态。
接着看ILoadMore:
public interface ILoadMore { /** * 上拉布局初始状态 */ void onLoadMoreInit(); /** * 释放加载状态 */ void onReleaseToLoad(); /** * 正在加载状态 */ void onLoading(); /** * 加载成功 */ void onLoadMoreSuccess(); /** * 加载失败 */ void onLoadMoreFailed(); /** * 加载完毕 */ void onLoadMoreDone();}
对应的也是6种状态。
接着再看IPable:
public interface IPable { /** * 是否能下拉 * */ boolean canPullDown(); /** * 是否能上拉 * */ boolean canPullUp(); /** * 是否消费此次触碰事件 * */ void consumeEvent(boolean isConsume);}
这是ContentView的需要的基本操作,接口consumeEvent可能不太好理解,大家可以理解成如果isConsume为true,那么此次触碰事件不再会向ContentView的子child传递了。
有些时候,产品经理可能会有这样的要求:如果数据是做了分页的,当ListView(GridView、RecyclerView等)滑动到底部时,自动加载下一页的数据,而不需要用户再手动拽一次加载更多,所以我在这里又实现了一个接口IPableImp:
public interface IPableImp extends IPable { /** * 设置当内容页面滚动到底部时,是否自动加载 * */ void setAutoPullUp(PableGroup pullLayout, boolean canAutoPullUp); /** * 设置是否在加载中 * */ void setIsPullUping(boolean isPullUping);}
接着来看ViewGroup的实现:
/** * 自定义的布局,用来管理三个子控件,其中一个是下拉头,一个是包含内容的pullableView(可以是实现IPable接口的的任何View), 还有一个上拉头 * 如果头部样式需要修改,重写一个View继承IRefresh、底部样式继承ILoadMore * 使用时根据 * {@link #allowPullDown} * {@link #allowPullUp} * {@link #needToRefresh} * {@link #needToLoadMore} * 四个方法配合实际页面效果编写 * create by Hankin on 2016/4/1. * @email hankin.huan@gmail.com */public class PableGroup extends FrameLayout { public static final int INIT = 0; public static final int RELEASE_TO_REFRESH = 1; public static final int REFRESHING = 2; public static final int RELEASE_TO_LOAD = 3; public static final int LOADING = 4; public static final int DONE = 5; private int state = INIT; private OnRefreshListener mListener; public static final int SUCCEED = 0; public static final int FAIL = 1; private float lastY; public float pullDownY = 0; private float pullUpY = 0; private float refreshDist = 200; private float loadmoreDist = 200; private LayoutTimer timer; public float MOVE_SPEED = 8; private boolean isLayout = false; private boolean isTouch = false; private float radio = 2; private View refreshView; private View loadmoreView; private View pullableView; private int mEvents; private boolean canPullDown = true; private boolean canPullUp = true; private boolean isAllowPullDown = true; private boolean isAllowPullUp = true; private boolean isNeedToRefresh = true; private boolean isNeedToLoadMore = true; private final int RECOVERRADIO = 3; private OnPullDownListener mOnPullDownListener; private LayoutHandler updateHandler; private Runnable doneR = new Runnable() { @Override public void run() { changeState(DONE); hide(); } }; private class LayoutHandler extends Handler { WeakReference<Context> weakReference ; public LayoutHandler(Context context){ weakReference = new WeakReference<>(context); } @Override public void handleMessage(Message msg) { MOVE_SPEED = (float) (8 + 5 * Math.tan(Math.PI / 2 / getMeasuredHeight() * (pullDownY + Math.abs(pullUpY)))); if (!isTouch) { if (state == REFRESHING && pullDownY <= refreshDist&&isNeedToRefresh) { pullDownY = refreshDist; timer.cancel(); } else if (state == LOADING && -pullUpY <= loadmoreDist&&isNeedToLoadMore) { pullUpY = -loadmoreDist; timer.cancel(); } if ((state==REFRESHING&&!isNeedToRefresh)||(state==LOADING&&!isNeedToLoadMore)) changeState(INIT); } if (pullDownY > 0) pullDownY -= MOVE_SPEED; else if (pullUpY < 0) pullUpY += MOVE_SPEED; if (pullDownY < 0) { pullDownY = 0; if (state != REFRESHING && state != LOADING) changeState(INIT); timer.cancel(); requestLayout(); } if (pullUpY > 0) { pullUpY = 0; if (state != REFRESHING && state != LOADING) changeState(INIT); timer.cancel(); requestLayout(); } requestLayout(); if (pullDownY + Math.abs(pullUpY) == 0) timer.cancel(); } } public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } public PableGroup(Context context) { super(context); initView(context); } public PableGroup(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public PableGroup(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(context); } private void initView(Context context) { updateHandler = new LayoutHandler(context); timer = new LayoutTimer(updateHandler); } private void hide() { timer.schedule(RECOVERRADIO); } public void refreshFinish(int refreshResult, int delay) { switch (refreshResult) { case SUCCEED: ((IRefresh)refreshView).onRefreshSuccess(); break; case FAIL: default: ((IRefresh)refreshView).onRefreshFailed(); break; } if (pullDownY > 0) { updateHandler.postDelayed(doneR, delay); } else { updateHandler.post(doneR); } } public void loadmoreFinish(int refreshResult, int delay) { switch (refreshResult) { case SUCCEED: ((ILoadMore)loadmoreView).onLoadMoreSuccess(); break; case FAIL: default: ((ILoadMore)loadmoreView).onLoadMoreFailed(); break; } if (pullUpY < 0) { updateHandler.postDelayed(doneR, delay); } else { updateHandler.post(doneR); } } private void changeState(int to) { state = to; switch (state) { case INIT: ((IRefresh)refreshView).onRefreshInit(); ((ILoadMore)loadmoreView).onLoadMoreInit(); break; case RELEASE_TO_REFRESH: ((IRefresh)refreshView).onReleaseToRefresh(); break; case REFRESHING: ((IRefresh)refreshView).onRefreshing(); break; case RELEASE_TO_LOAD: ((ILoadMore)loadmoreView).onReleaseToLoad(); break; case LOADING: ((ILoadMore)loadmoreView).onLoading(); break; case DONE: ((IRefresh)refreshView).onRefreshDone(); ((ILoadMore)loadmoreView).onLoadMoreDone(); break; } } private void releasePull() { canPullDown = true; canPullUp = true; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: lastY = ev.getY(); timer.cancel(); mEvents = 0; releasePull(); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: mEvents = -1; break; case MotionEvent.ACTION_MOVE: if (mEvents == 0) { if ((pullDownY > 0 || (((IPable) pullableView).canPullDown() && canPullDown && state != LOADING))&&isAllowPullDown) { pullDownY = pullDownY + (ev.getY() - lastY) / radio; if (pullDownY < 0) { pullDownY = 0; canPullDown = false; canPullUp = true; } if (pullDownY > getMeasuredHeight()) pullDownY = getMeasuredHeight(); if (state == REFRESHING) { isTouch = true; } } else if ((pullUpY < 0 || (((IPable) pullableView).canPullUp() && canPullUp && state != REFRESHING)) && isAllowPullUp) { pullUpY = pullUpY + (ev.getY() - lastY) / radio; if (pullUpY > 0) { pullUpY = 0; canPullDown = true; canPullUp = false; } if (pullUpY < -getMeasuredHeight()) pullUpY = -getMeasuredHeight(); if (state == LOADING) { isTouch = true; } } else releasePull(); } else mEvents = 0; lastY = ev.getY(); radio = (float) (2 + 2 * Math.tan(Math.PI / 2 / getMeasuredHeight() * (pullDownY + Math.abs(pullUpY)))); if (pullDownY > 0 || pullUpY < 0) requestLayout(); if (pullDownY > 0) { if (pullDownY <= refreshDist && (state == RELEASE_TO_REFRESH || state == DONE)) { changeState(INIT); } if (pullDownY >= refreshDist && state == INIT) { changeState(RELEASE_TO_REFRESH); } } else if (pullUpY < 0) { if (-pullUpY <= loadmoreDist && (state == RELEASE_TO_LOAD || state == DONE)) { changeState(INIT); } if (-pullUpY >= loadmoreDist && state == INIT) { changeState(RELEASE_TO_LOAD); } } if ((pullDownY + Math.abs(pullUpY)) > 0) {// ev.setAction(MotionEvent.ACTION_DOWN); ev.setAction(MotionEvent.ACTION_CANCEL); ((IPable)pullableView).consumeEvent(true); } else { ((IPable)pullableView).consumeEvent(false); } if (mOnPullDownListener!=null) mOnPullDownListener.onPullDown(pullDownY); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (pullDownY > refreshDist || -pullUpY > loadmoreDist) { isTouch = false; } if (state == RELEASE_TO_REFRESH) { changeState(REFRESHING); if (mListener != null) mListener.onRefresh(this); } else if (state == RELEASE_TO_LOAD) { changeState(LOADING); if (mListener != null) mListener.onLoadMore(this); } hide(); default: break; } super.dispatchTouchEvent(ev); return true; } private class AutoRefreshAndLoadTask extends AsyncTask<Integer, Float, String> { @Override protected String doInBackground(Integer... params) { while (pullDownY < 4 / 3 * refreshDist) { pullDownY += MOVE_SPEED; publishProgress(pullDownY); try { Thread.sleep(params[0]); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } @Override protected void onPostExecute(String result) { changeState(REFRESHING); if (mListener != null) mListener.onRefresh(PableGroup.this); hide(); } @Override protected void onProgressUpdate(Float... values) { if (pullDownY > refreshDist) changeState(RELEASE_TO_REFRESH); requestLayout(); } } public void autoRefresh() { if (isAllowPullDown) { AutoRefreshAndLoadTask task = new AutoRefreshAndLoadTask(); task.execute(5); } } public void autoLoad() { if (isAllowPullUp) { pullUpY = -loadmoreDist; requestLayout(); changeState(LOADING); if (mListener != null) mListener.onLoadMore(this); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!isLayout) { refreshView = getChildAt(0); if (!(refreshView instanceof IRefresh)) throw new IllegalArgumentException("the header view must implement hankin.pablegroup.pable.interf.IRefresh."); pullableView = getChildAt(1); loadmoreView = getChildAt(2); if (!(loadmoreView instanceof ILoadMore)) throw new IllegalArgumentException("the footer view must implement hankin.pablegroup.pable.interf.ILoadMore."); isLayout = true; refreshDist = ((ViewGroup) refreshView).getChildAt(0) .getMeasuredHeight(); loadmoreDist = ((ViewGroup) loadmoreView).getChildAt(0) .getMeasuredHeight(); } refreshView.layout(0, (int) (pullDownY + pullUpY) - refreshView.getMeasuredHeight(), refreshView.getMeasuredWidth(), (int) (pullDownY + pullUpY)); pullableView.layout(0, (int) (pullDownY + pullUpY), pullableView.getMeasuredWidth(), (int) (pullDownY + pullUpY) + pullableView.getMeasuredHeight()); loadmoreView.layout(0, (int) (pullDownY + pullUpY) + pullableView.getMeasuredHeight(), loadmoreView.getMeasuredWidth(), (int) (pullDownY + pullUpY) + pullableView.getMeasuredHeight() + loadmoreView.getMeasuredHeight()); } public void allowPullDown(boolean isAllowPullDown){ this.isAllowPullDown = isAllowPullDown; } public void allowPullUp(boolean isAllowPullUp){ this.isAllowPullUp = isAllowPullUp; if (loadmoreView!=null) loadmoreView.setVisibility(isAllowPullUp? View.VISIBLE: View.INVISIBLE); } public void needToRefresh(boolean isNeedToRefresh){ this.isNeedToRefresh = isNeedToRefresh; } public void needToLoadMore(boolean isNeedToLoadMore){ this.isNeedToLoadMore = isNeedToLoadMore; } private class LayoutTimer { private Handler handler; private Timer timer; private LayoutTask mTask; public LayoutTimer(Handler handler) { this.handler = handler; timer = new Timer(); } public void schedule(long period) { cancel(); mTask = new LayoutTask(handler); timer.schedule(mTask, 0, period); } public void cancel() { if (mTask != null) { mTask.cancel(); mTask = null; } } private class LayoutTask extends TimerTask { private Handler handler; public LayoutTask(Handler handler) { this.handler = handler; } @Override public void run() { handler.obtainMessage().sendToTarget(); } } } public interface OnRefreshListener { void onRefresh(PableGroup pableGroup); void onLoadMore(PableGroup pableGroup); } public void setOnPullDownListener(OnPullDownListener onPullDownListener){ this.mOnPullDownListener = onPullDownListener; } public interface OnPullDownListener{ void onPullDown(float pullDown); }}
要理解此控件的实现过程我觉得有两点基础需要了解:
1、touch事件的分发机制。
2、ViewGroup的绘制原理。
代码我就不一句一句的讲解了,我个人觉得,作为一个程序猿,代码比文字更具表达能力,而且这里包含的设计理念在实现思路中也讲过了。
实在看不懂实现过程也没有关系,因为我已经将此控件封装的很好使用了,在Demo里我实现了三种ContentView,分别是LinearLayout、ListView、ScrollView。还有两种HeaderView与FooterView,代码都是非常简洁易懂。大家可以借鉴我代码里的方式,implement IRefresh或ILoadMore或IPable,实现想要的效果的HeaderView、FooterView、ContentView。
使用时,直接在布局文件里:
<hankin.pablegroup.pable.PableGroup android:id="@+id/pg_main" android:layout_width="match_parent" android:layout_height="match_parent"> <hankin.pablegroup.pable.head.EmptyHView android:layout_width="match_parent" android:layout_height="wrap_content"/> <hankin.pablegroup.pable.pview.PLinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:background="@color/ffffff"> ......... </hankin.pablegroup.pable.pview.PLinearLayout> <hankin.pablegroup.pable.foot.EmptyFView android:layout_width="match_parent" android:layout_height="wrap_content"/> </hankin.pablegroup.pable.PableGroup>
……处,就按照LinearLayout的方式来写布局文件即可。如果没有特殊的要求,在activity中都不需要获取HeaderView、ContentView、FooterView的对象。只需要获取PableGroup的对象利用这四个函数:
allowPullDown //是否允许页面可以向下拉动allowPullUp //是否允许页面可以向上拉动needToRefresh //是否需要下拉刷新操作,在空实现的HeaderView中,设置为false即可实现页面可以向下拽动,然后回弹needToLoadMore //是否需要上拉加载操作,在空实现的FooterView中,设置为false即可实现页面可以向上拽动,然后回弹
项目结构:
但是有一个封装的工具类在Demo里没有使用:
public class PableUtils { private static void finishRefresh(PableGroup pullLayout, IPableImp pullable, boolean isSuccess, boolean canAutoPullUp, boolean allowPullUp){ pullLayout.refreshFinish(isSuccess ? PableGroup.SUCCEED : PableGroup.FAIL, isSuccess ? 800 : 0); setLoadMore(pullLayout, pullable, canAutoPullUp, allowPullUp); } private static void finishLoadMore(PableGroup pullLayout, IPableImp pullable, boolean isSuccess, boolean canAutoPullUp, boolean allowPullUp) { pullLayout.loadmoreFinish(isSuccess? PableGroup.SUCCEED: PableGroup.FAIL, 0); setLoadMore(pullLayout, pullable, canAutoPullUp, allowPullUp); } private static void setLoadMore(PableGroup pullLayout, IPableImp pullable, boolean canAutoPullUp, boolean allowPullUp){ pullable.setIsPullUping(false); pullable.setAutoPullUp(pullLayout, canAutoPullUp); pullLayout.allowPullUp(allowPullUp); } /** * 刷新完成与加载完成不要同时调用,否则布局会立马还原 * @param isRefresh 是否刷新操作 * @param pullLayout * @param pullable * @param isSuccess 是否成功 * @param canAutoPullUp 是否允许页面滑动至底部时,自动加载 * @param allowPullUp 是否允许加载操作 */ public static void finish(boolean isRefresh, PableGroup pullLayout, IPableImp pullable, boolean isSuccess, boolean canAutoPullUp, boolean allowPullUp){ if (isRefresh) finishRefresh(pullLayout, pullable, isSuccess, canAutoPullUp, allowPullUp); else finishLoadMore(pullLayout, pullable, isSuccess, canAutoPullUp, allowPullUp); }}
这个是在刷新或加载操作完后,使用PableUtils.finish函数就可以了,参数意思在注释里写的很清楚了。
没有csdn会员,资源不能设置0分下载~~~
源码下载
github下载
- Android适配任意View下拉刷新上拉加载,或上下弹性拽动的ViewGroup控件
- Android所有View通用下拉刷新上拉加载控件
- Android 中所有View的上拉加载下拉刷新
- 自个儿写Android的下拉刷新/上拉加载控件
- 支持任意View下拉刷新/下拉加载更多的控件
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android(自定义控件)下拉刷新上拉加载,所有View通用.(直接拿来用)
- Android开发笔记-下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用!
- Android下拉刷新上拉加载控件,对所有View通用
- Android下拉刷新上拉加载控件,对所有View通用!
- Python变量和数据类型
- C语言——main函数的参数列表
- Python(21):用web.py搭一个服务端(python3)
- 基于用户投票的排名算法(三):Stack Overflow
- 上机一 G D&C--玲珑数
- Android适配任意View下拉刷新上拉加载,或上下弹性拽动的ViewGroup控件
- 中文分词器
- jQuery-学习二-CSS和盒子模型
- Coder(线段树+多棵线段树)
- 类的静态成员
- 无人驾驶龙虎榜
- LeetCode84 Largest Rectangle in Histogram
- leetcode Add to List 3. Longest Substring
- PHP、JAVA、NET 编程技术对比分析