Android自定义控件实战——实现仿IOS下拉刷新上拉加载 PullToRefreshLayout

         下拉刷新控件,网上有很多版本,有自定义Layout布局的,也有封装控件的,各种实现方式的都有。但是很少有人告诉你具体如何实现的,今天我们就来一步步实现自己封装的 PullToRefreshLayout 完美的解决下拉刷新,上拉加载问题。

         首先来分析一下原理,为什么一下拉就可以拉出来一个布局,请看下图,从图中可以看到整个屏幕来说有可见部分,有隐藏部分,当我们手指在屏幕上下拉的时候滑动距离到一定程度了就会拉出 下拉头布局,这样就达到了下拉效果。那么具体代码如何实现待我慢慢像大家解析。


Android自定义控件实战——实现仿IOS下拉刷新上拉加载 PullToRefreshLayout_第1张图片

         1、想要实现  PullToRefreshLayout 下拉刷新控件那么我们就必须要有个容器,也就是如上图的容器,知道了需要什么那么我们就开始自定义一个容器。

          这里如果不会自定义控件的同学可以参考博客 http://blog.csdn.net/cscfas/article/details/51330505

/**
 * Created by ZQY on 2016/5/17.
 * 

* 这个是上拉加载和下拉刷新的 View *

* 注:这里的 android:orientation="vertical" 只能为这个值 */ public class PullToRefreshLayout extends LinearLayout { public PullToRefreshLayout(Context context) { super(context); initAnim(); } public PullToRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); initAnim(); } public PullToRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAnim(); } }


         2、有了容器,接下来就拉实现下拉头,上拉脚。LinearLayout 我们都用过线性布局嘛,在这里要注意 android:orientation=“vertical” 只能是垂直布局。这里重写了该控件,目的是在代码中动态添加布局到控件中,实现组合控件,就是PullToRefreshLayout ,这里调用了LinearLayout 的addView()  方法将布局添加到PullToRefreshLayout中。

             (1)、添加头部布局,这里也就是下拉头

   private void addHeaderView() {

        mHeaderView = mInflater.inflate(R.layout.refresh_header, this, false);

        mHeaderImageView = (ImageView) mHeaderView
                .findViewById(R.id.pull_to_refresh_image);
        mHeaderTextView = (TextView) mHeaderView
                .findViewById(R.id.pull_to_refresh_text);
        mHeaderUpdateTextView = (TextView) mHeaderView
                .findViewById(R.id.pull_to_refresh_updated_at);

        mHeaderUpdateTextView.setText(DataUtil.getRefreshCompleteTime());

        mHeaderProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);

        measureView(mHeaderView);

        mHeaderViewHeight = mHeaderView.getMeasuredHeight();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeaderViewHeight);

        //设置 topMargin 的值为负的 header View 高度,即将其隐藏在最上方
        params.topMargin = -(mHeaderViewHeight);

        //添加头部到布局
        addView(mHeaderView, params);
    }

             (2)、添加脚部布局,这里也就是 上拉脚

  
private void addFooterView() {
        mFooterView = mInflater.inflate(R.layout.refresh_footer, this, false);
        mFooterImageView = (ImageView) mFooterView
                .findViewById(R.id.pull_to_load_image);
        mFooterTextView = (TextView) mFooterView
                .findViewById(R.id.pull_to_load_text);
        mFooterProgressBar = (ProgressBar) mFooterView
                .findViewById(R.id.pull_to_load_progress);

        // 底部布局
        measureView(mFooterView);
        mFooterViewHeight = mFooterView.getMeasuredHeight();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                mFooterViewHeight);

        /**
         * 	int top = getHeight();
         params.topMargin=getHeight();//在这里getHeight()==0,但在onInterceptTouchEvent()方法里getHeight()已经有值了,不再是0;

         getHeight()什么时候会赋值,稍候再研究一下
         由于是线性布局可以直接添加,只要AdapterView的高度是MATCH_PARENT,那么footer view就会被添加到最后,并隐藏
         */
        addView(mFooterView, params);

    }

             看完以上代码你肯定会想就这么简单嘛!当然不是,细心的同学会发现两个函数都有调用 measureView()函数,它是干嘛的呢!下面就来看下这个函数,这个函数看起来代码和注释很多,这里的功能无非就是计算子控件在父控件中的大小。

 private void  measureView(View child) {

        /**
         * child.getLayoutParams();
         *
         * 返回  该视图的布局参数
         *
         * 此视图的父视图指定如何安排它的供应参数
         *

         */
        ViewGroup.LayoutParams p = child.getLayoutParams();


        if (p == null) {
            /**
             * 用指定的 宽度和高度 创建一组新的布局参数
             *
             * @param width 宽度,或者 {@link #WRAP_CONTENT},
             *        {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
             *        API Level 8),或一个固定大小的像素
             * @param height  高度,或者 {@link #WRAP_CONTENT},
             *        {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
             *        API Level 8), 或一个固定大小的像素
             */
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        /**
         * 是否measureChildren困难的部分:搞清楚MeasureSpec传递给特定的子控件。这种方法计算出正确的MeasureSpec一个子视图中的一维(高度或宽度)。
         * 目标是信息从我们MeasureSpec与子控件的的LayoutParams结合,以获得最佳的可能结果。例如,如果这个观点知道它的大小(因为它MeasureSpec有整整模式),
         * 子控件在其的LayoutParams已经表示,它想成为的尺寸与父控件一样,父控件应让子控件布置给精确的尺寸。

         * @param spec 该视图的要求
         * @param padding  该视图为当前维的填充和利润(如果适用)
         *
         * @param childDimension  希望为子控件设置的尺寸
         * @return  MeasureSpec   一个MeasureSpec整数为孩子
         *
         */
        int childWidthSpec=ViewGroup.getChildMeasureSpec(0,0+0,p.width);


        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {

            /**
             *
             创建基于所提供的大小和模式的量度规范。该模式必须是下列之一:
             UNSPECIFIED
             EXACTLY
             AT_MOST

             * @param size 该措施说明书的大小
             * @param mode 该措施规范的模式
             * @return 基于规模和模式的措施规范
             */
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
                    MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0,
                    MeasureSpec.UNSPECIFIED);
        }

        /**
         * 这就是所谓的大一个视图应该如何。父控件 约束信息的宽度和高度参数。

         一个视图的实际测量工作是在onMeasure(int,int),称为该方法。因此,只有onMeasure(int,int)可以而且必须由子类重写。

         @param widthMeasureSpec 横向空间的需求添加到的父控件大小
         @param heightMeasureSpec 垂直间距需求添加到的父控件大小
         */
        child.measure(childWidthSpec, childHeightSpec);

    }

         3、知道了 下拉头,上拉脚 怎么实现了,接下来就看在哪里加入到 PullToRefreshLayout控件中,又是如何实现动画的。请看下面代码。

             (1)、这里动画实现的是刷新箭头的方向旋转,最后一行 addHeaderView() 实现了头部的添加。

  /**
     * 初始化动画
     */
    private void initAnim() {

        //加载所有的动画,我们需要的代码,而不是通过 XML
        mFlipAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF,
                0.5f, Animation.RELATIVE_TO_SELF, 0.5f);

        //设置动画 均速
        mFlipAnimation.setInterpolator(new LinearInterpolator());

        /**
         * 动画应该持续多久,持续时间不能为负
         *
         @param durationMillis
          *  @throws java.lang.IllegalArgumentException  如果 durationMillis < 0
         *  @attr 参考 R.styleable #Animation_duration
         */
        mFlipAnimation.setDuration(250);


        /**
         * 如果 fillafter 是 true ,这个动画进行改造将坚持当它完成。
         * 默认为 false ,如果不设置。
         *
         *请注意,这适用于个别动画,当使用 {@link android.view.animation.AnimationSet AnimationSet} 链动画
         *
         * @param fillAfter  如果动画结束后,动画应该应用它的转换
         * @attr ref android.R.styleable#Animation_fillAfter
         *
         * @see #setFillEnabled(boolean)
         */
        mFlipAnimation.setFillAfter(true);


        /**
         *构造函数使用时建立一个rotateanimation 对象
         *
         *
         * @param fromDegrees 在动画开始时应用旋转偏移。
         *
         * @param toDegrees   在动画结束时应用旋转偏移。
         *
         * @param pivotXType 指定如何pivotxvalue应解释。什么之中的一个
         *        Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
         *        Animation.RELATIVE_TO_PARENT.
         *
         * @param pivotXValue  X坐标的对象被旋转的点,指定一个绝对数量,0是左边缘。这个值可以是绝对数如果pivotxtype是绝对的,或一个百分比(1是100%)否则。
         *
         *
         * @param pivotYType 指定如何pivotyvalue应解释。什么之中的一个
         *        Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
         *        Animation.RELATIVE_TO_PARENT.
         *
         * @param pivotYValue  X坐标的对象被旋转的点,指定一个绝对数量,0是左边缘。这个值可以是绝对数如果pivotxtype是绝对的,或一个百分比(1是100%)否则。
         */
        mReverseFlipAnimation = new RotateAnimation(-180, 0,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);


        //设置此动画的加速曲线。默认为线性插值。 这里是匀速
        mReverseFlipAnimation.setInterpolator(new LinearInterpolator());

        mReverseFlipAnimation.setDuration(250);
        mReverseFlipAnimation.setFillAfter(true);

        mInflater = LayoutInflater.from(getContext());

        // header view 在此添加,保证是第一个添加到linearlayout的最上端
        addHeaderView();
    }

             (2)、知道了头部如何加入PullToRefreshLayout中,那么底部是如何添加的呢!其实底部的加入是有技巧的,接下来请看代码。onFihishInflate()  看到@Override 你就知道这个函数是 LinearLayout 提供,那么它有何作用呢,它的作用就是在所有的XML和头部布局都添加了的情况下加入 脚部布局。

    /**
     *
     完成 填充 XML格式的视图。这就是所谓的 UI填充 的最后阶段,所有子视图已被添加之后。

     即使子类覆盖onFinishInflate,他们应始终确保调用超级方法,使我们得到调用。 既必须调用  super.onFinishInflate();
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        // footer view 在此添加保证添加到linearlayout中的最后

        addFooterView();

        initContentAdapterView();
    }

             (3)、在上面的代码中你会看到 initContentAdapterView()  这个函数,你会想它又是什么鬼,它有什么作用呢?请看代码。

如果你有了解过,我的上一篇博客:http://blog.csdn.net/cscfas/article/details/51330505 ;那么你就知道在自定义控件中,如果XML布局中引入了控件,会加载该自定义控件的第二个构造函数,那么addHeaderView() 会被加载到布局中,PullToRefreshLayout 在xml 中加入的布局也会被添加到控件中。该布局可以包裹 ListView 和 GridView 及 ScrollView 控件。

   /**
     *
     * 初始化 adapterview像ListView,GridView等;或init ScrollView
     */
    private void initContentAdapterView(){

        int count=getChildCount();

        if (count<3)
            throw new IllegalArgumentException(
                    "this layout must contain 3 child views,and AdapterView or ScrollView must in the second position!");


        View  view=null;

        for (int i=0;i){

                System.out.println("the type is AdapterView");

                mAdapterView=(AdapterView)view;
            }

            if (view instanceof  ScrollView){

                System.out.println("thie type is ScrollView");

                mScrollView= (ScrollView) view;
            }
        }


        if (mAdapterView==null&&mScrollView==null){

            throw new IllegalArgumentException(
                    "must contain a AdapterView or ScrollView in this layout!");
        }
    }


         4、接下来看下项目中用到的常量和变量注释,这对阅读后续代码有帮助。

   /**
     * 下拉刷新
     */
    private static final int PULL_TO_REFRESH = 2;

    /**
     * 释放刷新
     */
    private static final int RELEASE_TO_REFRESH = 3;

    /**
     * 刷新
     */
    private static final int REFRESHING = 4;


    /**
     * 上拉加载
     */
    private static final int PULL_UP_STATE = 10;
    /**
     * 下拉刷新
     */
    private static final int PULL_DOWN_STATE = 11;


    /**
     * 最后Y轴距离
     */
    private int mLastMotionY;

    /**
     * 锁定
     */
    private boolean mLock;


         5、了解了布局如何实现,接下来就到了手势如何实现,也就是我们下拉为什么可以拉出 下拉头,这里涉及到手势相关的概念,如果不了解手可以参考博客:http://blog.csdn.net/cscfas/article/details/51372342

               这里就不讲事件是如何拦截,如何分发的了,我们重点来看如下代码,这里在 ACTION_DOWN时并没有拦截事件只是记录下了 Y轴坐标,为什么呢?因为PullToRefreshLayout 是属于 ViewGroup 容器型的控件,如果ACTION_DOWN 直接被拦截了那么 ListVeiw 和 GridView 中的 item点击事件及 ScrollView中点击事件和长按事件将无法触发。

    /**
     * 事件拦截
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int y = (int) ev.getRawY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:   //手指按下时记录 Y轴坐标

                // 首先拦截down事件,记录y坐标
                mLastMotionY = y;

                break;
            case MotionEvent.ACTION_MOVE:  //滑动时 拿到移动距离 判断是否拦截手势

                // deltaY > 0 是向下运动,< 0是向上运动
                int deltaY = y - mLastMotionY;
                if (isRefreshViewScroll(deltaY)) {

//				System.out.println("正在移动:返回true");
                    return true;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }

        return false;
    }

               细心的同学会发现在 ACTION_MOVE 中有调用 isRefreshViewScroll() 函数,那么它又有什么功能呢!仔细看代码会发现它返回了一个 boolean 类型的值,是它控制这事件是否拦截,看到这里你是不是觉得它至关重要,那么就来分析一下它的结构吧!

                mAdapterView 这个控件从何而来,有认真看过上面代码你就应该知道了。那么它是何方神圣呢?它就是 适配器填充控件后得到的结果,AdapterView 是适配器和控件的组合,这里主要是拿到AdapterView中的子控件,也就是ListView或 GridView中的Item,通过获取子控件的状态来动态设置 是否要拦截手势,以及设置 mPullState 状态。

                mScrollView 控件也是同理,拿到子控件的状态来判断是否要拦截事件。具体代码都有注释请看代码,这里就不详解了。

  /**
     * 是否应该到了父View,即PullToRefreshView滑动
     *
     * @param deltaY
     *            , deltaY > 0 是向下运动,< 0是向上运动
     * @return
     */
    private boolean isRefreshViewScroll(int deltaY) {

        // 当头部状态是 刷新 或 底部状态是刷新时 返回 false 不拦截
        if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
            return false;
        }


        //对于ListView和GridView
        if (mAdapterView != null) {
            // 子view(ListView or GridView)滑动到最顶端
            if (deltaY > 0) {

                View child = mAdapterView.getChildAt(0);
                if (child == null) {

                    //设置状态为下拉刷新
                    mPullState = PULL_DOWN_STATE;

                    //设置状态为拦截
                    return true;
                }

                // 适配中 第一个控件高度为 0 且 第一个控件可见
                if (mAdapterView.getFirstVisiblePosition() == 0
                        && child.getTop() == 0) {

                    //设置状态为下拉刷新
                    mPullState = PULL_DOWN_STATE;

                    return true;
                }


                int top = child.getTop();
                int padding = mAdapterView.getPaddingTop();
                if (mAdapterView.getFirstVisiblePosition() == 0
                        && Math.abs(top - padding) <= 8) {//这里之前用3可以判断,但现在不行,还没找到原因
                    mPullState = PULL_DOWN_STATE;
                    return true;
                }

            } else if (deltaY < 0) {  //如果移动的距离为 负值

                //获取适配中最后一个控件
                View lastChild = mAdapterView.getChildAt(mAdapterView
                        .getChildCount() - 1);

                if (lastChild == null) {
                    mPullState = PULL_UP_STATE;
                    // 如果mAdapterView中没有数据,不拦截
                    return true;
                }
                // 最后一个子view的Bottom小于父View的高度说明mAdapterView的数据没有填满父view,
                // 等于父View的高度说明mAdapterView已经滑动到最后
                if (lastChild.getBottom() <= getHeight()
                        && mAdapterView.getLastVisiblePosition() == mAdapterView
                        .getCount() - 1) {
                    mPullState = PULL_UP_STATE;
                    return true;
                }
            }
        }

        // 对于ScrollView
        if (mScrollView != null) {
            // 子scroll view滑动到最顶端
            View child = mScrollView.getChildAt(0);

            //当移动距离为 正值  且滚动条没有滚动
            if (deltaY > 0 && mScrollView.getScrollY() == 0) {
                mPullState = PULL_DOWN_STATE;  //设置状态为下拉刷新
                return true;
            } else if (deltaY < 0
                    && child.getMeasuredHeight() <= getHeight()
                    + mScrollView.getScrollY()) {
                mPullState = PULL_UP_STATE;   //设置为上拉加载

                return true;
            }
        }
        return false;
    }




         6、接下来就要见证奇迹了,就是具体如何实现 下拉刷新上拉加载更多效果的业务了,还记得上面我们有讲到手势拦截吧!如果你了解手势就知道被拦截后会执行什么函数,那就是 onTouchEvent() 函数了。

              (1)、首先看下 ACTION_MOVE 这里我们来计算用户手指在屏幕上的滑动距离,还记得在 onInterceptTounchEvent()中已经对 mPullState 状态做过改变,这里开始就通过判断当前状态是下拉还是上拉来处理 HeaderView 和 FootView的显示及动画效果。

    /*
	 * 如果在onInterceptTouchEvent()方法中没有拦截(即onInterceptTouchEvent()方法中 return false)
	 *
	 * 则由PullToRefreshView 的子View来处理;否则由下面的方法来处理(即由PullToRefreshView自己来处理)
	 */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mLock) {   //当处于锁定状态时
            return true;
        }

        //拿到Y轴坐标
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:    //手指按下时触发  ACTION_DOWN
                // onInterceptTouchEvent已经记录
                // mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:  //手指在屏幕上滑动时触发  ACTION_MOVE

                //拿到用户滑动的距离
                int deltaY = y - mLastMotionY;

                if (mPullState == PULL_DOWN_STATE) {   //如果当前状态处于下拉刷新 PULL_DOWN_STATE  那么执行 headerPrepareToRefresh() 函数实现刷新效果
                    // PullToRefreshView执行下拉
                    Log.i(TAG, " pull down!parent view move!");
                    headerPrepareToRefresh(deltaY);
                    // setHeaderPadding(-mHeaderViewHeight);
                } else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载 PULL_UP_STATE

                    if (pullUpLoad) {   //判断用户是否启用上拉加载

                        // PullToRefreshView执行上拉
                        Log.i(TAG, "pull up!parent view move!");

                        footerPrepareToRefresh(deltaY);
                    }
                }
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: //当事件被取消时

                //获取当前header view 的topMargin 值
                int topMargin = getHeaderTopMargin();

                if (mPullState == PULL_DOWN_STATE) {  //如果当前状态是下拉刷新

                    if (topMargin >= 0) {
                        // 开始刷新
                        headerRefreshing();
                    } else {
                        // 还没有执行刷新,重新隐藏
                        setHeaderTopMargin(-mHeaderViewHeight);
                    }
                } else if (mPullState == PULL_UP_STATE) {  //如果当前状态处于上拉加载

                    if (pullUpLoad) {

                        if (Math.abs(topMargin) >= mHeaderViewHeight
                                + mFooterViewHeight) {
                            // 开始执行footer 刷新
                            footerRefreshing();
                        } else {
                            // 还没有执行刷新,重新隐藏
                            setHeaderTopMargin(-mHeaderViewHeight);
                        }
                    }

                }
                break;
        }
        return super.onTouchEvent(event);
    }

              (2)、处理下拉或上拉布局被拉出效果,接下来看 headPrepareToRefresh() 和 footerPrepareToRefresh() 这两个函数实现了上拉及下拉效果 ,这里要注意 mHeaderState、mFooterState 的状态改变,它决定这是否释放刷新

  /**
     * header 准备刷新,手指移动过程,还没有释放
     *
     * @param deltaY
     *            ,手指滑动的距离
     */
    private void headerPrepareToRefresh(int deltaY) {
        int newTopMargin = changingHeaderViewTopMargin(deltaY);

        // 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态
        if (newTopMargin >= 0 && mHeaderState != RELEASE_TO_REFRESH) {

            mHeaderTextView.setText(R.string.pull_to_refresh_release_label);
            mHeaderUpdateTextView.setVisibility(View.VISIBLE);
            mHeaderImageView.clearAnimation();
            mHeaderImageView.startAnimation(mFlipAnimation);

            //改变状态为释放刷新
            mHeaderState = RELEASE_TO_REFRESH;

        } else if (newTopMargin < 0 && newTopMargin > -mHeaderViewHeight) {// 拖动时没有释放
            mHeaderImageView.clearAnimation();
            mHeaderImageView.startAnimation(mFlipAnimation);
            mHeaderTextView.setText(R.string.pull_to_refresh_pull_label);
            mHeaderState = PULL_TO_REFRESH;
        }
    }

 /**
     * footer 准备刷新,手指移动过程,还没有释放 移动footer view高度同样和移动header view
     * 高度是一样,都是通过修改header view的topmargin的值来达到
     *
     * @param deltaY
     *            ,手指滑动的距离
     */
    private void footerPrepareToRefresh(int deltaY) {
        int newTopMargin = changingHeaderViewTopMargin(deltaY);
        // 如果header view topMargin 的绝对值大于或等于header + footer 的高度
        // 说明footer view 完全显示出来了,修改footer view 的提示状态
        if (Math.abs(newTopMargin) >= (mHeaderViewHeight + mFooterViewHeight)
                && mFooterState != RELEASE_TO_REFRESH) {
            mFooterTextView
                    .setText(R.string.pull_to_refresh_footer_release_label);
            mFooterImageView.clearAnimation();
            mFooterImageView.startAnimation(mFlipAnimation);
            mFooterState = RELEASE_TO_REFRESH;
        } else if (Math.abs(newTopMargin) < (mHeaderViewHeight + mFooterViewHeight)) {
            mFooterImageView.clearAnimation();
            mFooterImageView.startAnimation(mFlipAnimation);
            mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label);
            mFooterState = PULL_TO_REFRESH;
        }
    }

              (3)、仔细阅读上面代码,会发现所有的判断跟随着这个 headerPrepareToRefresh() 函数的返回值决定,接下来看下这个函数。判断当前 mPullState 状态 及 拉动距离是否大于设置距离,动态返回 TopMargin 及拉出的距离

  /**
     * 修改Header view top margin的值
     *
     * @description
     * @param deltaY
     */
    private int changingHeaderViewTopMargin(int deltaY) {

        LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();

        float newTopMargin = params.topMargin + deltaY * 0.4f;

        //这里对上拉做一下限制,因为当前上拉后然后不释放手指直接下拉,会把下拉刷新给触发了
        //表示如果是在上拉后一段距离,然后直接下拉
        if(deltaY>0&&mPullState == PULL_UP_STATE&&Math.abs(params.topMargin) <= mHeaderViewHeight){
            return params.topMargin;
        }
        //同样地,对下拉做一下限制,避免出现跟上拉操作时一样的bug
        if(deltaY<0&&mPullState == PULL_DOWN_STATE&&Math.abs(params.topMargin)>=mHeaderViewHeight){
            return params.topMargin;
        }
        params.topMargin = (int) newTopMargin;
        mHeaderView.setLayoutParams(params);

        /**
         * 无效整个视图。如果视图是可见的,
         *
         *  {@link #onDraw(android.graphics.Canvas)} 将在某个时候被调用
         *
         *  这必须从UI线程调用。从非UI线程,致电致电
         *
         *  {@link #postInvalidate()}.
         */
        invalidate();
        return params.topMargin;
    }

              (4)、ACTION_UP、ACTION_CANCEL 处理释放刷新和取消执行刷新,首先拿到 topMargin 既拉动的距离,通过判断拉动距离和 mPullState 状态来决定是释放刷新还是取消执行刷新。


            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: //当事件被取消时

                //获取当前header view 的topMargin 值
                int topMargin = getHeaderTopMargin();

                if (mPullState == PULL_DOWN_STATE) {  //如果当前状态是下拉刷新

                    if (topMargin >= 0) {
                        // 开始刷新
                        headerRefreshing();
                    } else {
                        // 还没有执行刷新,重新隐藏
                        setHeaderTopMargin(-mHeaderViewHeight);
                    }
                } else if (mPullState == PULL_UP_STATE) {  //如果当前状态处于上拉加载

                    if (pullUpLoad) {

                        if (Math.abs(topMargin) >= mHeaderViewHeight
                                + mFooterViewHeight) {
                            // 开始执行footer 刷新
                            footerRefreshing();
                        } else {
                            // 还没有执行刷新,重新隐藏
                            setHeaderTopMargin(-mHeaderViewHeight);
                        }
                    }

                }
                break;

 
  

              (5)、headerRefreshing() 、footerRefreshing() 释放刷新,这里将 Runnable 添加到UI线程中,延迟1500 毫秒达到,下拉头或上拉脚停顿效果,这里主要回调监听接口,该接口是调用 PullToRefreshLayout 控件的 Activity或FrangMent 中实现。

             

    /**
     *  下拉头释放刷新
     *
     */
    private void headerRefreshing() {
        mHeaderState = REFRESHING;
        setHeaderTopMargin(0);
        mHeaderImageView.setVisibility(View.GONE);
        mHeaderImageView.clearAnimation();
        mHeaderImageView.setImageDrawable(null);
        mHeaderProgressBar.setVisibility(View.VISIBLE);
        mHeaderTextView.setText(R.string.pull_to_refresh_refreshing_label);
        if (mOnHeaderRefreshListener != null) {


            /**
             * 使Runnable被添加到消息队列,经过规定的时间之后运行。
             *
             * 运行将运行在用户界面线程。既UI线程中
             */
            this.postDelayed(new Runnable() {

                @Override
                public void run() {
                    mOnHeaderRefreshListener.onHeaderRefresh(PullToRefreshView.this);
                }
            }, 1500);
        }
    }

    /**
     * 底部释放刷新
     */
    private void footerRefreshing() {
        mFooterState = REFRESHING;
        int top = mHeaderViewHeight + mFooterViewHeight;
        setHeaderTopMargin(-top);
        mFooterImageView.setVisibility(View.GONE);
        mFooterImageView.clearAnimation();
        mFooterImageView.setImageDrawable(null);
        mFooterProgressBar.setVisibility(View.VISIBLE);
        mFooterTextView
                .setText(R.string.pull_to_refresh_footer_refreshing_label);
        if (mOnFooterRefreshListener != null) {
            this.postDelayed(new Runnable() {

                @Override
                public void run() {
                    mOnFooterRefreshListener.onFooterRefresh(PullToRefreshView.this);
                }
            }, 1500);

        }
    }

              (5)、注意在刷新失败的时候会执行 setHeaderMargin() 该函数作用主要是实现布局的隐藏

 /**
     * 设置header view 的topMargin的值
     *
     * @description
     * @param topMargin
     *            ,为0时,说明header view 刚好完全显示出来; 为-mHeaderViewHeight时,说明完全隐藏了
     */
    private void setHeaderTopMargin(int topMargin) {
        LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
        params.topMargin = topMargin;
        mHeaderView.setLayoutParams(params);
        invalidate();
    }


         7、以上步骤基本实现了整个下拉刷新,上拉加载的功能,但是美中不足,刷新完成后我们还需要隐藏我们的布局,下面的代码是更新完后恢复初始化状态

  /**
     * header view 完成更新后恢复初始状态
     *
     * @description hylin 2012-7-31上午11:54:23
     */
    public void onHeaderRefreshComplete() {
        setHeaderTopMargin(-mHeaderViewHeight);
        mHeaderImageView.setVisibility(View.VISIBLE);
        mHeaderImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow);
        mHeaderTextView.setText(R.string.pull_to_refresh_pull_label);
        mHeaderProgressBar.setVisibility(View.GONE);

        mHeaderState = PULL_TO_REFRESH;
    }


    /**
     * footer view 完成更新后恢复初始状态
     */
    public void onFooterRefreshComplete() {
        setHeaderTopMargin(-mHeaderViewHeight);
        mFooterImageView.setVisibility(View.VISIBLE);
        mFooterImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow_up);
        mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label);
        mFooterProgressBar.setVisibility(View.GONE);

        mFooterState = PULL_TO_REFRESH;
    }

         8、以上基本实现了下拉刷新上拉加载,博客也写累了,剩余的功能我就不贴代码了,可以参看Demo

         下载地址:http://download.csdn.net/detail/cscfas/9524306

      

你可能感兴趣的:(Android,自定义控件)