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下载

阅读全文
0 0