android-pulltorefresh 源码解析

项目地址:https://github.com/chrisbanes/Android-PullToRefresh

介绍几个主要的成员变量

(1)下拉刷新有四种状态:

  • 点击刷新,list的item个数无法填满整个屏幕出现下面效果

  • 正在下拉,header出现,箭头向下,提示“下拉刷新”

  • header正在下拉,箭头向上,提示“松开刷新”

  • 手指已经松开,header位于顶部,箭头隐藏,progressbar旋转,提示“载入中”

代码中四种状态标识:

    //点击刷新
    private static final int TAP_TO_REFRESH = 1; 
    //正在下拉,header出现,箭头向下,提示“下拉刷新”)
    private static final int PULL_TO_REFRESH = 2;
    //header正在下拉,箭头向上,提示“松开刷新”
    private static final int RELEASE_TO_REFRESH = 3; 
    //(手指已经松开,header位于顶部,箭头隐藏,progressbar旋转,提示“正在刷新”)
    private static final int REFRESHING = 4;

  private int mRefreshState;  //刷新状态的标签,它的value是上面四个中的一个(1~4)

 (2)List 中的header,其实是个RelativeLayout

   /**
     * ListView需要有一个header,用来显示刷新提示。
     * header中有一个指示箭头,一个使箭头旋转的动画,一个刷新时显示的progressbar,
     * 提示“下拉刷新/松开刷新”的TextView,提示“加载中”TextView
     */
    private RelativeLayout mRefreshView; //header
    private TextView mRefreshViewText; 
    private ImageView mRefreshViewImage;//箭头
    private ProgressBar mRefreshViewProgress; 
    private TextView mRefreshViewLastUpdated;

 

(3)构造函数

自定义控件的时候要写两个参数的构造函数。否则会报错 android.view.InflateException: Binary XML file line #: Error inflating class com.XXXX . stackoverflow 上有个人这么解释“it maybe due to the fact that since we are adding the custom View class in the XML file, we are setting several attributes to it in the XML, which need to be processed at the time of instantiation”  http://stackoverflow.com/questions/3739661/android-error-inflating-class  

有知道原因的,请指教~

 public PullToRefreshListView(Context context) {
        super(context);
        init(context);
    }

    public PullToRefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public PullToRefreshListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
   

 (4)init 函数

    我们来看看构造函数调用的init函数干了什么。

private void init(Context context) {
        //1、 Load all of the animations we need in code rather than through XML
    	//header中箭头旋转动画设置,从0 到-180
        mFlipAnimation = new RotateAnimation(0, -180,     
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        mFlipAnimation.setInterpolator(new LinearInterpolator());
        mFlipAnimation.setDuration(250);
        mFlipAnimation.setFillAfter(true);
        //header中箭头旋转动画设置-180 到0
        mReverseFlipAnimation = new RotateAnimation(-180, 0,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
        mReverseFlipAnimation.setDuration(250);
        mReverseFlipAnimation.setFillAfter(true);
        
//2、加载xml布局 mInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); //加载header布局,获得子View的引用 mRefreshView = (RelativeLayout) mInflater.inflate( R.layout.pull_to_refresh_header, this, false); mRefreshViewText = (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_text); mRefreshViewImage = (ImageView) mRefreshView.findViewById(R.id.pull_to_refresh_image); mRefreshViewProgress = (ProgressBar) mRefreshView.findViewById(R.id.pull_to_refresh_progress); mRefreshViewLastUpdated = (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_updated_at); mRefreshViewImage.setMinimumHeight(50);//箭头的最低高度 //3、为header设置onClickListener mRefreshView.setOnClickListener(new OnClickRefreshListener()); //4、得到Header的原始pading,resetHeaderPadding()方法隐藏Header的时候,用到 mRefreshOriginalTopPadding 还原初始的TopPadding ,隐藏header
//根据xml文件,Header的原始pading=10dip mRefreshOriginalTopPadding = mRefreshView.getPaddingTop(); //5、初始化mRefreshState mRefreshState = TAP_TO_REFRESH; //6、把mRefreshView添加在ListView 最上方 addHeaderView(mRefreshView); //7、监听 super.setOnScrollListener(this); //8、 measure measureView(mRefreshView); mRefreshViewHeight = mRefreshView.getMeasuredHeight(); }

 (5)measur关于onMeasure有兴趣看,很长zishi http://blog.csdn.net/qinjuning/article/details/7110211

/**
     * 测量header的长宽
     * @param child
     */
    private void measureView(View child) {
    	//得到子view的LayoutParams;View通过LayoutParams类告诉其父视图它想要的大小(即,长度和宽度)
        ViewGroup.LayoutParams p = child.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.FILL_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        //header的宽度  getChildMeasureSpec()return a MeasureSpec integer for the child 
        // 第一个参数spec =0,mode是UNSPECIFIED(未指定),父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小
        int childWidthSpec = ViewGroup.getChildMeasureSpec(0,
                0 + 0, p.width);
        //header 的高度
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {//View的具体值(an exact size) 总是大于0的,,FILL_PARENT/MATCH_PARENT值为 -1 ,WRAP_CONETENT值为-2
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    }

 

 1)void android.view.View.measure(int widthMeasureSpec, int heightMeasureSpec)  这个方法用来测量出view的大小。父view使用width参数和height参数来提供constraint信息。实际上,view的测量工作在 onMeasure(int, int)方法中完成。因此,只有onMeasure(int, int)方法可以且必须被重写。onMeasure在measure函数中被调用。参数widthMeasureSpec提供view的水平空间的规格说明,参数heightMeasureSpec提供 view的垂直空间的规格说明

 2)ViewGroup.LayoutParams ViewGroup.LayoutParams 见此文章http://blog.csdn.net/qinjuning/article/details/8051811

 3)getChildMeasureSpec()

 Does the hard part of measureChildren: figuring out the MeasureSpec to pass to a particular child. This method figures out the right MeasureSpec for one dimension (height or width) of one child view. The goal is to combine information from our MeasureSpec with the LayoutParams of the child to get the best possible results. For example, if the this view knows its size (because its MeasureSpec has a mode of EXACTLY), and the child has indicated in its LayoutParams that it wants to be the same size as the parent, the parent should ask the child to layout given an exact size.

 (6)setOnScrollListener 滚动监听,是滑动监听的重点

public class PullToRefreshListView extends ListView implements OnScrollListener 

 PullToRefreshListView继承了OnScrollListene。官方文档里对AbsListView.OnScrollListener的解释:

Interface definition for a callback to be invoked when the list or grid has been scrolled

所以PullToRefreshListView滚动时候,会回调这个接口的方法

 代码:onScroll()是触控滚动刷新的核心代码~

 /**
      * 滚动时一直回调,直到停止滚动时才停止回调。单击时回调一次。 
      * @param AbsListView
      * @param int 当前能看见的第一个列表项ID(从0开始) 
      * @param int  当前能看见的列表项个数(小半个也算)  
      * @param int  列表项总数
      */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) {
        // When the refresh view is completely visible, change the text to say
        // "Release to refresh..." and flip the arrow drawable.
    	//1、用户通过触控滚动,并且手指没有离开屏幕。
        if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL 
                && mRefreshState != REFRESHING) {            //而且状态不是REFRESHING
        	//1.1、firstVisibleItem为0,屏幕上一直有可见item
            if (firstVisibleItem == 0) {
                mRefreshViewImage.setVisibility(View.VISIBLE); //箭头可见
                //1.1.1  下拉至少大于20 px,或者header的头部离屏幕顶边距离大于0px ,才能出现“松手以刷新”
                if ((mRefreshView.getBottom() >= mRefreshViewHeight + 20 //getBottom() return Bottom position of this view relative to its parent.
                        || mRefreshView.getTop() >= 0)     // getTop() return Top position of this view relative to its parent.
                        && mRefreshState != RELEASE_TO_REFRESH) {
                    mRefreshViewText.setText(R.string.pull_to_refresh_release_label);//松开刷新
                    mRefreshViewImage.clearAnimation();
                    mRefreshViewImage.startAnimation(mFlipAnimation);
                    mRefreshState = RELEASE_TO_REFRESH;
                 //1.1.2 下拉小于20px 并且状态不是下拉刷新
                } else if (mRefreshView.getBottom() < mRefreshViewHeight + 20
                        && mRefreshState != PULL_TO_REFRESH) {
                    mRefreshViewText.setText(R.string.pull_to_refresh_pull_label);//下拉刷新
                    if (mRefreshState != TAP_TO_REFRESH) {
                        mRefreshViewImage.clearAnimation();
                        mRefreshViewImage.startAnimation(mReverseFlipAnimation);
                    }
                    mRefreshState = PULL_TO_REFRESH;
                }
             //1.2 没有可见item,header变为“点击刷新”
            } else {
            	Log.d(TAG,"firstVisibleItem的position不为0 ");
                mRefreshViewImage.setVisibility(View.GONE);
                resetHeader();
            }
        //2、用户之前通过触控滚动并执行了快速滚动(手指离开屏幕前快速滑动了一下)
        } else if (mCurrentScrollState == SCROLL_STATE_FLING 
                && firstVisibleItem == 0           // header还没出现
                && mRefreshState != REFRESHING) {
            setSelection(1); //将list定位在第一行
            mBounceHack = true; //手指触控在屏幕时候,此变量为true
        //3、手指离开屏幕前快速滑动了一下,手指还在屏幕,
        } else if (mBounceHack && mCurrentScrollState == SCROLL_STATE_FLING) { 
            setSelection(1);//将list在没有滚动时定位在第一行,将处于0位置的Header隐藏
        }

        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(view, firstVisibleItem,
                    visibleItemCount, totalItemCount);
        }
    }

    /**
     * 当列表视图或网格视图正在滚动是执行的回调函数。如果视图正在滚动, 系统会在渲染下一帧之前调用该方法。
     * 就是说,会在调用任何 getView(int, View, ViewGroup)方法之前调用。
     */
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        mCurrentScrollState = scrollState;

        if (mCurrentScrollState == SCROLL_STATE_IDLE) { //视图不在滚动状态
        	//手指离开时候,此变量设为flase 
            mBounceHack = false;
        }

        if (mOnScrollListener != null) {
            mOnScrollListener.onScrollStateChanged(view, scrollState);
        }
    }

 

1)PullToRefreshListView有一个成员变量标记滚动状态,取值(0~2)SCROLL_STATE_IDLE,SCROLL_STATE_TOUCH_SCROLL,SCROLL_STATE_FLING

private int mCurrentScrollState;

 2)

//第一次记录下startY后置为true,当手指松开是置为false。用于确保一个拖拽过程中只记录一次startY  
    private boolean mBounceHack; 

 3)resetHeader ()

 /**
     * Resets the header to the original state.
     */
    private void resetHeader() {
        if (mRefreshState != TAP_TO_REFRESH) {
            mRefreshState = TAP_TO_REFRESH;

            resetHeaderPadding();//list为空,所以header 的topPadding还原为原来的值.10dip

            // Set refresh view text to the pull label
            mRefreshViewText.setText(R.string.pull_to_refresh_tap_label);//点击刷新
            // Replace refresh drawable with arrow drawable
            mRefreshViewImage.setImageResource(R.drawable.ic_pulltorefresh_arrow);
            // Clear the full rotation animation
            mRefreshViewImage.clearAnimation();
            // Hide progress bar and arrow.
            mRefreshViewImage.setVisibility(View.GONE); //隐藏箭头
            mRefreshViewProgress.setVisibility(View.GONE); //隐藏进度条
        }
    }

 (7)点击刷新,OnClickRefreshListener也是一个重点,点击刷新视图时调用。List的Item没有填充满屏幕,不可拖动list。效果见博客第一张图。

在init()中,mRefreshView.setOnClickListener(new OnClickRefreshListener()); new一个OnClickRefreshListener对象,并把它设置为header的点击监听器。

我们看看OnClickRefreshListener这个内部类代码。onClick调用了两个函数,一个是prepareForRefresh(),视header觉上改变;一个是onRefresh(),list 内容改变,本demo是item个数增多;

 /**
     * Invoked when the refresh view is clicked on. This is mainly used when
     * there's only a few items in the list and it's not possible to drag the
     * list.
     */
    private class OnClickRefreshListener implements OnClickListener {

        @Override
        public void onClick(View v) {
            if (mRefreshState != REFRESHING) { 
                prepareForRefresh();
                onRefresh();
            }
        }

    }

  1)prepareForRefresh();改变header视图

/**
     *   显示进度条和“Loading”的提示
     */
    public void prepareForRefresh() {
        resetHeaderPadding(); 

        mRefreshViewImage.setVisibility(View.GONE); //不显示箭头
        // We need this hack, otherwise it will keep the previous drawable.
        mRefreshViewImage.setImageDrawable(null);
        mRefreshViewProgress.setVisibility(View.VISIBLE);//显示进度条

        // Set refresh view text to the refreshing label
        mRefreshViewText.setText(R.string.pull_to_refresh_refreshing_label);//loading提示

        mRefreshState = REFRESHING; //标记状态
    }

 2)onRefresh(),改变内容

    /**
     * 刷新,调用OnRefreshListener接口的回调函数 onRefresh()
     */
    public void onRefresh() {
        Log.d(TAG, "onRefresh");

        if (mOnRefreshListener != null) {
            mOnRefreshListener.onRefresh(); //留意
        }
    }

 private OnRefreshListener mOnRefreshListener; 成员变量mOnRefreshListener是OnRefreshListener的对象。OnRefreshListener是个内部接口,其中onRefresh是个回调函数,当列表刷新的时候调用。

 /**
     * Interface definition for a callback to be invoked when list should be
     * refreshed.
     */
    public interface OnRefreshListener {
        /**
         * Called when the list should be refreshed.
         * <p>
         * A call to {@link PullToRefreshListView #onRefreshComplete()} is
         * expected to indicate that the refresh has completed.
         */
        public void onRefresh();
    }

 同时PullToRefreshListView向外提供setOnRefreshListener()可以设置mOnRefreshListener,我们再外部使用PullToRefreshListView的时候需要自己实现一个

OnRefreshListener 。
/**
     * Register a callback to be invoked when this list should be refreshed.
     * 
     * @param onRefreshListener The callback to run.
     */
    public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
        mOnRefreshListener = onRefreshListener;
    }

 (8)最后再看看触控事件处理

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int y = (int) event.getY(); //手指触摸位置的Y值  
        mBounceHack = false;

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP: //手指抬起
                if (!isVerticalScrollBarEnabled()) {
                    setVerticalScrollBarEnabled(true);
                }
                //1.屏幕上又可见的item,而且状态不是REFRESHING
                if (getFirstVisiblePosition() == 0 && mRefreshState != REFRESHING) {
                    //1.1、手指抬起前,状态是“松手以刷新”
                    if ((mRefreshView.getBottom() >= mRefreshViewHeight
                            || mRefreshView.getTop() >= 0)
                            && mRefreshState == RELEASE_TO_REFRESH) { 
                        // Initiate the refresh
                        mRefreshState = REFRESHING; //状态改为REFRESHING
                        prepareForRefresh();//
                        onRefresh(); //刷新
                    } else if (mRefreshView.getBottom() < mRefreshViewHeight //
                            || mRefreshView.getTop() <= 0) {
                        // Abort refresh and scroll down below the refresh view
                        resetHeader();
                        setSelection(1);
                    }
                }
                break;
            case MotionEvent.ACTION_DOWN://手指触碰到屏幕
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:  //手指滑动
                applyHeaderPadding(event);
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 调整header的TopPadding 大小
     * @param ev
     */
    private void applyHeaderPadding(MotionEvent ev) {
        //1、 返回手指触摸事件的历史位置的记录数
        int pointerCount = ev.getHistorySize();
        // 2、每个触控事件分析
        for (int p = 0; p < pointerCount; p++) {
            //2.1、“松开刷新”状态,header的topPadding随着触控点的改变而改变
            if (mRefreshState == RELEASE_TO_REFRESH) {
                if (isVerticalFadingEdgeEnabled()) {
                    setVerticalScrollBarEnabled(false);
                }

                int historicalY = (int) ev.getHistoricalY(p);

                // Calculate the padding to apply, we divide by 1.7 to
                // simulate a more resistant effect during pull.
                int topPadding = (int) (((historicalY - mLastMotionY)
                        - mRefreshViewHeight) / 1.7);

                mRefreshView.setPadding(
                        mRefreshView.getPaddingLeft(),
                        topPadding,
                        mRefreshView.getPaddingRight(),
                        mRefreshView.getPaddingBottom());
            }
        }
    }
View Code

关心变量mLastMotionY:记录的是,触碰点的Y轴坐标,在applyHeaderPadding()中会用到,可以根据触碰点的Y轴坐标的改变而改变header的TopPadding,实现在下拉的时候,header 下滑的效果

 出处:http://www.cnblogs.com/sueZheng/articles/4032297.html

 

 

posted @ 2014-10-24 13:01  sue_zheng  Views(1008)  Comments(3Edit  收藏  举报