Android开发开源项目之-PullToRefresh源码分析

学习Android已经好几年了,但从没有认真写点什么,最近在一位同事的鼓动下有了写博客的想法。这是我第一篇博客,从第三方开源项目开始吧。

首先我们先建立一个有PullToRefresh源码的工程方便我们跟着代码去理解这个开源项目。先下载完整的开源项目PulltoRefresh下载
文中的代码都是开源项目中的samples以及框架,下面就让我们一步一步跟着代码去理解这个框架。工程目录

 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.handmark.pulltorefresh.library"
    android:versionCode="2110"
    android:versionName="2.1.1" >

    <uses-sdk android:minSdkVersion="4" />

    <application
        android:hardwareAccelerated="true"
        android:icon="@drawable/icon"
        android:label="@string/app_name" >
        <activity
            android:name="com.handmark.pulltorefresh.samples.PullToRefreshListActivity"
            android:label="PtR ListView" >
             <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

1.上图为工程目录和Androidmanifest.xml文件。先来说说我们是怎么使用的。首先看布局文件activity_ptr_list.xml。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

<!--     The PullToRefreshListView replaces a standard ListView widget. -->

    <com.handmark.pulltorefresh.library.PullToRefreshListView
        android:id="@+id/pull_refresh_list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:cacheColorHint="#00000000"
        android:divider="#19000000"
        android:dividerHeight="4dp"
        android:fadingEdge="none"
        android:fastScrollEnabled="false"
        android:footerDividersEnabled="false"
        android:headerDividersEnabled="false"
        android:smoothScrollbar="true" />

</LinearLayout

布局文件很简单,使用方式基本和ListView没区别(其实PullToRefreshListView还是有很多自己的属性的,之后代码中我们会遇到,但这里我们都没有设置任何属性,因为在代码中PullToRefreshListView会有一套默认的属性值)。

  1. 继续来看主Activity
    package com.handmark.pulltorefresh.samples;

    import java.util.Arrays;
    import java.util.LinkedList;

    import android.app.ListActivity;
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.text.format.DateUtils;

    import android.view.View;
    import android.widget.AdapterView;
    import android.widget.AdapterView.OnItemClickListener;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    import android.widget.Toast;

    import com.handmark.pulltorefresh.library.PullToRefreshBase;
    import com.handmark.pulltorefresh.library.PullToRefreshBase.OnLastItemVisibleListener;
    import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;
    import com.handmark.pulltorefresh.library.PullToRefreshListView;
    import com.handmark.pulltorefresh.library.R;
    import com.handmark.pulltorefresh.library.extras.SoundPullEventListener;

    public final class PullToRefreshListActivity extends ListActivity {

        static final int MENU_MANUAL_REFRESH = 0;
        static final int MENU_DISABLE_SCROLL = 1;
        static final int MENU_SET_MODE = 2;
        static final int MENU_DEMO = 3;

        private LinkedList<String> mListItems;
        private PullToRefreshListView mPullRefreshListView;
        private ArrayAdapter<String> mAdapter;

        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_ptr_list);

            mPullRefreshListView = (PullToRefreshListView) findViewById(R.id.pull_refresh_list);

            // 设置刷新监听器
            mPullRefreshListView
                    .setOnRefreshListener(new OnRefreshListener<ListView>() {
                        @Override
                        public void onRefresh(
                                PullToRefreshBase<ListView> refreshView) {
                            String label = DateUtils.formatDateTime(
                                    getApplicationContext(),
                                    System.currentTimeMillis(),
                                    DateUtils.FORMAT_SHOW_TIME
                                            | DateUtils.FORMAT_SHOW_DATE
                                            | DateUtils.FORMAT_ABBREV_ALL);

                            // Update the LastUpdatedLabel
                            refreshView.getLoadingLayoutProxy()
                                    .setLastUpdatedLabel(label);

                            // Do work to refresh the list here.
                            new GetDataTask().execute();
                        }
                    });

            // 设置当滚动到最后一行可见时的监听器
            mPullRefreshListView
                    .setOnLastItemVisibleListener(new OnLastItemVisibleListener() {

                        @Override
                        public void onLastItemVisible() {
                            Toast.makeText(PullToRefreshListActivity.this,
                                    "End of List!", Toast.LENGTH_SHORT).show();
                        }
                    });

            ListView actualListView = mPullRefreshListView.getRefreshableView();

            // Need to use the Actual ListView when registering for Context Menu
            registerForContextMenu(actualListView);

            mListItems = new LinkedList<String>();
            mListItems.addAll(Arrays.asList(mStrings));

            mAdapter = new ArrayAdapter<String>(this,
                    android.R.layout.simple_list_item_1, mListItems);

            // 设置适配器 使用方法基本与ListView相同
            mPullRefreshListView.setAdapter(mAdapter);
            // actualListView.setAdapter(mAdapter);
            mPullRefreshListView.setOnItemClickListener(new OnItemClickListener() {

                @Override
                public void onItemClick(AdapterView<?> parent, View view,
                        int position, long id) {
                    // TODO Auto-generated method stub
                    DebugLog.i("tags", "=============position===" + position);
                }

            });
        }

        private class GetDataTask extends AsyncTask<Void, Void, String[]> {

            @Override
            protected String[] doInBackground(Void... params) {
                // Simulates a background job.
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                }
                return mStrings;
            }

            @Override
            protected void onPostExecute(String[] result) {
                mListItems.addFirst("Added after refresh...");
                mAdapter.notifyDataSetChanged();

                // Call onRefreshComplete when the list has been refreshed.
                mPullRefreshListView.onRefreshComplete();

                super.onPostExecute(result);
            }
        }

        private String[] mStrings = { "Abbaye de Belloc",
                "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
                "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu",
                "Airag", "Airedale", "Aisy Cendre", "Allgauer Emmentaler",
                "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam",
                "Abondance", "Ackawi", "Acorn", "Adelost", "Affidelice au Chablis",
                "Afuega'l Pitu", "Airag", "Airedale", "Aisy Cendre",
                "Allgauer Emmentaler" };
    }

3.接下来就进入框架源码的世界了。
当我们执行

mPullRefreshListView = (PullToRefreshListView) findViewById(R.id.pull_refresh_list);
这步时,首先会执行PullToRefreshListView的构造函数。我们先看看继承关系
(1)PullToRefreshListView extends PullToRefreshAdapterViewBase
(2)PullToRefreshAdapterViewBase extends PullToRefreshBase
(3)PullToRefreshBase extends LinearLayout

所以最后执行的其实是PullToRefreshBase的构造函数

我们看到其实还有另外三个构造函数,但是自定义控件通过xml布局文件映射成控件一般都是调用这个构造函数。

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

    @SuppressWarnings("deprecation")
    private void init(Context context, AttributeSet attrs) {
        /**
         *  getPullToRefreshScrollDirection()为抽象函数,具体的实现在子类中,
         *  我们这里子类为 PullToRefreshListView,ListView显然是垂直方向的,看代码
         *  也确实返回的是 VERTICAL,因为当前View是继承的LinearLayout,所以设置方向
         *  为垂直
         */
        switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            setOrientation(LinearLayout.HORIZONTAL);
            break;
        case VERTICAL:
        default:
            setOrientation(LinearLayout.VERTICAL);
            break;
        }

        setGravity(Gravity.CENTER);

        /**
         * 这里获取一个值 用来判断是否被认为是滑动,当滑动距离>mTouchSlop
         * 才被认为是滑动
         */
        ViewConfiguration config = ViewConfiguration.get(context);
        mTouchSlop = config.getScaledTouchSlop();

        /**
         * 根据R.styleable.PullToRefresh 即res/values/attrs.xml中的PullToRefresh
         * 的自定义的属性获取布局文件中定义的PullToRefreshListView的属性,事实上我们布局文件中
         * 没有使用任何PullToRefreshListView自身的属性,都是ListView的
         */

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.PullToRefresh);

        /**
         * 一般使用时不会设置该属性,会使用默认值 PULL_FROM_START
         */
        if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) {
            mMode = Mode.mapIntToValue(a.getInteger(
                    R.styleable.PullToRefresh_ptrMode, 0));
        }
        /**
         * 一般使用时不会设置该属性,会使用默认值 ROTATE
         */
        if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) {
            mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger(
                    R.styleable.PullToRefresh_ptrAnimationStyle, 0));
        }

        // Refreshable View
        // By passing the attrs, we can add ListView/GridView params via XML
        /**
         * 子类实现createRefreshableView
         * PullToRefreshListView中的createRefreshableView() 返回一个ListView的子类
         */
        mRefreshableView = createRefreshableView(context, attrs);

        /**
         * 将ListView添加到mRefreshableViewWrapper
         * 再将mRefreshableViewWrapper添加到当前View
         * mRefreshableViewWrapper为一个FrameLayout
         */
        addRefreshableView(context, mRefreshableView);

        // We need to create now layouts now
        /**
         * 创建两个布局
         */
        mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
        mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

        /**
         * Styleables from XML 本demo中并未设置该属性 所以没有设置背景
         */
        if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) {
            Drawable background = a
                    .getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground);
            if (null != background) {
                mRefreshableView.setBackgroundDrawable(background);
            }
        } else if (a
                .hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) {
            Utils.warnDeprecation("ptrAdapterViewBackground",
                    "ptrRefreshableViewBackground");
            Drawable background = a
                    .getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
            if (null != background) {
                mRefreshableView.setBackgroundDrawable(background);
            }
        }

        /**
         * mOverScrollEnabled默认为true
         * 
         * mScrollingWhileRefreshingEnabled默认为false
         */
        if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
            mOverScrollEnabled = a.getBoolean(
                    R.styleable.PullToRefresh_ptrOverScroll, true);
        }

        if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
            mScrollingWhileRefreshingEnabled = a
                    .getBoolean(
                            R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled,
                            false);
        }

        // Let the derivative classes have a go at handling attributes, then
        // recycle them...
        handleStyledAttributes(a);
        a.recycle();

        // Finally update the UI for the modes
        updateUIForMode();
    }

init中主要是获取xml中定义的一些PullToRefresh自身的属性(我们这里没定义,所以使用的都是默认值)。mMode主要区分是下拉刷新、上拉加载或者是二者都可行等。mLoadingAnimationStyle下拉时候左边的图片及动画效果有两种,一种是转圈的图标,一种是一个大箭头,拉倒一定程度会反向。它有个默认值,我们可以通过修改这个默认值来看看效果,修改getDefault()的返回值ROTATE或者FLIP。

private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();

然后调用PullToRefreshListView中实现的createRefreshableView函数返回一个ListView的子类,我们暂时不追究具体实现,只需要知道此处返回的是个ListView(所以如果是下拉刷新的WebView那子类实现的一定是返回WebView了)

 mRefreshableView = createRefreshableView(context, attrs);
addRefreshableView(context, mRefreshableView);

来看看它是怎么添加的

  private void addRefreshableView(Context context, T refreshableView) {
        mRefreshableViewWrapper = new FrameLayout(context);
        mRefreshableViewWrapper.addView(refreshableView,
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT);

        addViewInternal(mRefreshableViewWrapper, new LinearLayout.LayoutParams(
        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        }

构造一个FrameLayout的mRefreshableViewWrapper,将这个ListView添加到FrameLayout中,再将mRefreshableViewWrapper添加到当前自定义的这个PullToRefreshListView中。
我们继续分析init函数,接下去构造两个LoadingLayout,一个是下拉刷新时顶部显示的View,另一个是上拉时底部加载显示的View,还记得上面说的mLoadingAnimationStyle吗,它有两种模式,createLoadingLayout内部会根据模式创建不同的LoadingLayout,分别是RotateLoadingLayout和FlipLoadingLayout。这两个LoadingLayout中会定义一些不同的动画效果,比如下拉时的动画,比如刷新时的动画等,这个后面会详细说。

handleStyledAttributes(a);

PullToRefreshListView.java中

   @Override
    protected void handleStyledAttributes(TypedArray a) {
        super.handleStyledAttributes(a);

        //默认为true
        mListViewExtrasEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrListViewExtrasEnabled, false);

        if (mListViewExtrasEnabled) {
            final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);

            // Create Loading Views ready for use later
            /**
             * 创建头部\尾部的布局  添加到FrameLayout  
             * 再将头部整个FrameLayout添加到ListView
             * 中
             */
            FrameLayout frame = new FrameLayout(getContext());
            mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, a);
            mHeaderLoadingView.setVisibility(View.GONE);
            frame.addView(mHeaderLoadingView, lp);
            mRefreshableView.addHeaderView(frame, null, false);

            mLvFooterLoadingFrame = new FrameLayout(getContext());
            mFooterLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_END, a);
            mFooterLoadingView.setVisibility(View.GONE);
            mLvFooterLoadingFrame.addView(mFooterLoadingView, lp);

            /**
             * If the value for Scrolling While Refreshing hasn't been
             * explicitly set via XML, enable Scrolling While Refreshing.
             */
            if (!a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
                setScrollingWhileRefreshingEnabled(true);
            }
        }
    }

PullToRefreshAdapterViewBase.java中

@Override
    protected void handleStyledAttributes(TypedArray a) {
        // Set Show Indicator to the XML value, or default value
        //默认为false
        mShowIndicator = a.getBoolean(R.styleable.PullToRefresh_ptrShowIndicator, !isPullToRefreshOverScrollEnabled());
        DebugLog.i("tags", "==========mShowIndicator===="+mShowIndicator);
    }

见下图右下角有个小箭头,mShowIndicator就是指示这个箭头需不需要显示的,默认不显示。

![运行截图](https://img-blog.csdn.net/20150524144900001)

mListViewExtrasEnabled默认是true,为了不影响主线,我们先将这里改为false,即handleStyledAttributes函数中不会执行,所以我们可以暂时不关心,后面再来说这段的作用。

最后来看init中最后执行的一个函数 updateUIForMode();

PullToRefreshAdapterViewBase.java中

@Override
protected void updateUIForMode() {
    super.updateUIForMode();
    // Check Indicator Views consistent with new Mode
    //从mRefreshableViewWrapper中添加或者移除右下角的箭头
    if (getShowIndicatorInternal()) {
        addIndicatorViews();
    } else {
        removeIndicatorViews();
    }
}

PullToRefreshBase.java中

protected void updateUIForMode() {
        // We need to use the correct LayoutParam values, based on scroll
        // direction
        final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams();

        // Remove Header, and then add Header Loading View again if needed
        if (this == mHeaderLayout.getParent()) {
            removeView(mHeaderLayout);
        }
        if (mMode.showHeaderLoadingLayout()) {
            addViewInternal(mHeaderLayout, 0, lp);
        }

        // Remove Footer, and then add Footer Loading View again if needed
        if (this == mFooterLayout.getParent()) {
            removeView(mFooterLayout);
        }
        if (mMode.showFooterLoadingLayout()) {
            addViewInternal(mFooterLayout, lp);
        }

        // Hide Loading Views
        refreshLoadingViewsSize();

        // If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
        // set it to pull down
        mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
    }

updateUIForMode()中将之前创建的mHeaderLayout和mFooterLayout添加到当前空间中的顶部或者尾部。调整
mHeaderLayout和mFooterLayout的大小,对于我们得PullToRefreshListView来说,需要设置MHeaderLayout
的高度为最大可下拉的高度 这里是控件自身高度的0.6倍。最后为了在不下拉刷新时,顶部的控件不可见且
藏在LitView数据的上方,就设置PullToRefreshListView的padding来隐藏mHeaderLayout。最后如果当前模式
为Mode.BOTH,即支持下拉刷新和上拉加载,就将模式直接改为Mode.PULL_FROM_START。

好了,PullToRefreshListView的初始化就这样结束了,总结一下控件的初始化中主要做了啥。
1.定义了一个ListView 并添加到PullToRefreshListView
2.定义了一个顶部刷新用的LoadingLayout 并添加到头部
3.定义了一个尾部加载用的loadingLayout 并添加到尾部
4.根据传入的属性或默认属性定义了头部Loadinglayout和尾部Loadinglayout的动画效果

关键属性:mMode 支持模式 下拉刷新、上拉加载、二者都支持、
mLoadingAnimationStyle 加载动画效果样式
mScrollingWhileRefreshingEnabled 刷新时是否支持滚动
mShowIndicator 是否显示右下角的小箭头

这些属性都可以在xml布局文件中设置,也可以直接代码中修改默认值
`4.我们回到我们的主Activity中,mPullRefreshListView.setAdapter(mAdapter);
内部其实没有做任何事,只是把adapter设置到我们之前在初始化时定义的ListView中去。

5.接下去就是拖动,这里涉及到android触屏事件的分发机制,不了解的同学建议先去了解下(最好不只是了解,而是跟随代码一步步看下去,别人讲的终究是别人的,只有亲自看看才会印象深刻)。有些知识是连贯的,不然很可能看的时候觉得懂了,看完了发现什么都没留下。
当你手指触屏时,事件会一层层传下来,直到传到我们的PullToRefreshListView(事实上当然不会是我说的这么简单,但是为了不影响对事件机制不了解的同学阅读)。
首先会执行PullToRefreshListView的dispatchTouchEvent函数,PullToRefreshListView并没有定义这个函数,最后调用的还是它的祖宗ViewGroup的dispatchTouchEvent。




    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (action == MotionEvent.ACTION_DOWN) {
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);
        }
        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }
        if (isUpOrCancel) {
            mMotionTarget = null;
        }
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }
        return target.dispatchTouchEvent(ev);
    } 

如果是Down事件,先获取disallowIntercept的值,默认是false的,如果是true表示,该事件不允许父控件拦截掉,必须由子空间来处理。因为是false,所以会执行onInterceptTouchEvent(ev),这个函数如果返回false,表示事件还需要传递给子空间。如果返回true,那么会直接不进入这个分支,也就不会交给子控件来处理,而是自己本身来处理看下面这个分支

 if (target == null) {
        // We don't have a target, this means we're handling the
        // event as a regular view.
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }
        return super.dispatchTouchEvent(ev);
    }

这里的super为ViewGrou的父控件View(注意,这里说的父控件不是包含关系,是继承关系),内部调用的是控件本身的onTouchEvent函数,即会调用PullToRefreshListView的onTouchEvent函数。

ok,有了这些知识我们就能继续看我们的PullToRefresh代码了,查看PullToRefreshListView、PullToRefreshAdapterViewBase、PullToRefreshBase这三个类,发现并没有重写ViewGroup的dispatchTouchEvent函数,然后在PullToRefreshBase中我们找到了onInterceptTouchEvent函数,所以对于框架代码来说,这里就是事件的入口了。

@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {

    if (!isPullToRefreshEnabled()) {
        return false;
    }

    final int action = event.getAction();

    if (action == MotionEvent.ACTION_CANCEL
            || action == MotionEvent.ACTION_UP) {
        mIsBeingDragged = false;
        return false;
    }

    /**
     * 如果不是Down事件,且mIsBengDragged为true 那么拦截事件 交给onTouch处理
     */
    if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
        return true;
    }

    switch (action) {
    case MotionEvent.ACTION_MOVE: {
        // If we're refreshing, and the flag is set. Eat all MOVE events
        /**
         * 默认mScrollingWhileRefreshingEnabled为false 即刷新时不能滚动
         */
        DebugLog.i("tags", "=========mScrollingWhileRefreshingEna"
                + "bled=" + mScrollingWhileRefreshingEnabled
                + "=== isRefreshing()=" + isRefreshing());
        if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
            return true;
        }

        /**
         * 位置已经在顶部 向下拉 或者位置在底部 向上拉 记录下当前坐标、mIsBeingDragged = true; 拉动模式
         */
        if (isReadyForPull()) {
            final float y = event.getY(), x = event.getX();
            final float diff, oppositeDiff, absDiff;

            // We need to use the correct values, based on scroll
            // direction
            switch (getPullToRefreshScrollDirection()) {
            case HORIZONTAL:
                diff = x - mLastMotionX;
                oppositeDiff = y - mLastMotionY;
                break;
            case VERTICAL:
            default:
                diff = y - mLastMotionY;
                oppositeDiff = x - mLastMotionX;
                break;
            }
            absDiff = Math.abs(diff);

            if (absDiff > mTouchSlop
                    && (!mFilterTouchEvents || absDiff > Math
                            .abs(oppositeDiff))) {
                if (mMode.showHeaderLoadingLayout() && diff >= 1f
                        && isReadyForPullStart()) {
                    mLastMotionY = y;
                    mLastMotionX = x;
                    mIsBeingDragged = true;
                    if (mMode == Mode.BOTH) {
                        mCurrentMode = Mode.PULL_FROM_START;
                    }
                } else if (mMode.showFooterLoadingLayout() && diff <= -1f
                        && isReadyForPullEnd()) {
                    mLastMotionY = y;
                    mLastMotionX = x;
                    mIsBeingDragged = true;
                    if (mMode == Mode.BOTH) {
                        mCurrentMode = Mode.PULL_FROM_END;
                    }
                }
            }
        }
        break;
    }
    /**
     * 如果是down事件 且view已经处于可下拉刷新 或上拉加载状态
     *  即已经在最顶部 或最底部了 那么记录down事件的初始位置,
     * 
     */
    case MotionEvent.ACTION_DOWN: {
        if (isReadyForPull()) {
            mLastMotionY = mInitialMotionY = event.getY();
            mLastMotionX = mInitialMotionX = event.getX();
            mIsBeingDragged = false;
        }
        break;
    }
    }

    return mIsBeingDragged;
}

并不是说onInterceptTouchEvent返回true就会把事件拦截在父控件,返回false就会往下传,注意前面dispatchTouchEvent代码,还区分Down事件还是Cancel、Up事件,是否消费掉事件,targetView是否找到等,所以还是要深入代码啊。

这里分两种情况来分析
(1)一次ACTION_DOWN后 立即ACTION_UP
这种情况,这里的onInterceptTouchEvent函数的down事件返回的是false,并没有把down时间拦截,up事件时由于targetView并不是空 所以还是会进入
onInterceptTouchEvent,这里同样返回了false,所以最后还是靠子控件去处理up事件

(2)一次ACTION_DOWN后 多次ACTION_MOVE 最后ACTION_UP
这种情况,这里的onInterceptTouchEvent函数的down事件返回的是false,并没有把down时间拦截,但是紧跟着的move事件就会在你拉动列表到刷新或者加载状态时返回true(并不是永远返回true,只有当顶部的刷新loading布局或者底部的加载布局出现时才会返回true)。一旦返回true,这次事件交给子控件处理,但接下来的却将down事件中记录下来的targetView清空,即不再有处理事件的子控件了,所以接下来的move及up等事件都不会在进入onInterceptTouchEvent,而是直接调用PullToRefreshBase的onTouchEvent函数

在onInterceptTouchEvent的move事件中,主要做了两件事,一件记录当前触屏的坐标,另一件事是判断这次move事件的方向是否处于以下两种状态(1.列表已经在顶部,但还在向下move。2.列表已经在底部,但还在向上move ),如果确实处于这两种状态就返回true,把事件交给onTouchEvent去处理。

@Override
    public final boolean onTouchEvent(MotionEvent event) {
    //如果模式为不可拉动刷新 直接返回false
        if (!isPullToRefreshEnabled()) {
            return false;
        }
        // If we're refreshing, and the flag is set. Eat the event
        //如果 刷新中可滚动标志为false  且当前正处于刷新数据时,那么直接返回true
        if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
            return true;
        }
        //判断边缘事件
        if (event.getAction() == MotionEvent.ACTION_DOWN
                && event.getEdgeFlags() != 0) {
            return false;
        }
        switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            if (mIsBeingDragged) {
                mLastMotionY = event.getY();
                mLastMotionX = event.getX();
                pullEvent();
                return true;
            }
            break;
        }
        // down事件 只是记录位置
        case MotionEvent.ACTION_DOWN: {
            if (isReadyForPull()) {
                mLastMotionY = mInitialMotionY = event.getY();
                mLastMotionX = mInitialMotionX = event.getX();
                return true;
            }
            break;
        }
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            if (mIsBeingDragged) {
                mIsBeingDragged = false;
                if (mState == State.RELEASE_TO_REFRESH
                        && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
                    setState(State.REFRESHING, true);
                    return true;
                }
                // If we're already refreshing, just scroll back to the top
                if (isRefreshing()) {
                    smoothScrollTo(0);
                    return true;
                }
                // If we haven't returned by here, then we're not in a state
                // to pull, so just reset
                setState(State.RESET);
                return true;
            }
            break;
        }
        }
        return false;
    }

重点全在onTouchEvent中的MOVE事件和UP事件中了

move事件,记录当前坐标,执行pullEvent()

private void pullEvent() {
    final int newScrollValue;
    final int itemDimension;
    final float initialMotionValue, lastMotionValue;

    switch (getPullToRefreshScrollDirection()) {
    case HORIZONTAL:
        initialMotionValue = mInitialMotionX;
        lastMotionValue = mLastMotionX;
        break;
    case VERTICAL:
    default:
        initialMotionValue = mInitialMotionY;
        lastMotionValue = mLastMotionY;
        break;
    }

    switch (mCurrentMode) {
    case PULL_FROM_END:
        newScrollValue = Math.round(Math.max(initialMotionValue
                - lastMotionValue, 0)
                / FRICTION);
        itemDimension = getFooterSize();
        break;
    case PULL_FROM_START:
    default:
        newScrollValue = Math.round(Math.min(initialMotionValue
                - lastMotionValue, 0)
                / FRICTION);
        itemDimension = getHeaderSize();
        break;
    }

    setHeaderScroll(newScrollValue);

    if (newScrollValue != 0 && !isRefreshing()) {
        float scale = Math.abs(newScrollValue) / (float) itemDimension;
        switch (mCurrentMode) {
        case PULL_FROM_END:
            mFooterLayout.onPull(scale);
            break;
        case PULL_FROM_START:
        default:
            mHeaderLayout.onPull(scale);
            break;
        }

        if (mState != State.PULL_TO_REFRESH
                && itemDimension >= Math.abs(newScrollValue)) {
            setState(State.PULL_TO_REFRESH);
        } else if (mState == State.PULL_TO_REFRESH
                && itemDimension < Math.abs(newScrollValue)) {
            setState(State.RELEASE_TO_REFRESH);
        }
    }
}

我们考虑向下拉动刷新的情况,计算手指向下拉动的距离,newScrollValue=这个距离的一半,然后获取mHeaderLayout的高度,记录在itemDimension 。setHeaderScroll(newScrollValue)设置mHeaderLayout向下滚动,内部是通过PullRefreshListView的scrollTo来实现这种滚动的。

if (newScrollValue != 0 && !isRefreshing()) 当滑动距离不为0,且不是正在刷新状态,计算当前滑动的距离和mHeadLayout本身的高度的比,我们在运行demo,向下拉动过程中会看到随着下拉距离的变化,左边的图片是有动画效果的。mHeaderLayout.onPull(scale);中会根据这个比值确定图片的旋转角度等。

最后判断滑动的距离有没有超过mHeadLayout本身的高度,未超过时和超过时也是会有一些不同的,比如顶部mHeadLayout上的文字显示,未超过时是“下拉刷新”,超过后显示“放开以刷新”。

UP事件
首先将mIsBeingDragged改为false;如果这时mState == State.RELEASE_TO_REFRESH 即拉动超过mHeadLayout高度后松开的,并且有刷新监听器,那么setState(State.REFRESHING, true);
如果已经正在刷新,那么就smoothScrollTo(0);回到原点。
其余情况重置View.

final void setState(State state, final boolean... params) {
    mState = state;
    if (DEBUG) {
        Log.d(LOG_TAG, "State: " + mState.name());
    }

    switch (mState) {
    case RESET:
        onReset();
        break;
    case PULL_TO_REFRESH:
        onPullToRefresh();
        break;
    case RELEASE_TO_REFRESH:
        onReleaseToRefresh();
        break;
    case REFRESHING:
    case MANUAL_REFRESHING:
        onRefreshing(params[0]);
        break;
    case OVERSCROLLING:
        // NO-OP
        break;
    }

    // Call OnPullEventListener
    if (null != mOnPullEventListener) {
        mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
    }
}

最后来看REFRESHING,

onRefreshing(函数在子类PullToRefreshListView和PullToRefreshAdapterViewBase、PullToRefreshBase三个类中都有。

PullToRefreshListView.java中
@Override
    protected void onRefreshing(final boolean doScroll) {
        /**
         * If we're not showing the Refreshing view, or the list is empty, the
         * the header/footer views won't show so we use the normal method.
         */
        ListAdapter adapter = mRefreshableView.getAdapter();
        if (!mListViewExtrasEnabled || !getShowViewWhileRefreshing() || null == adapter || adapter.isEmpty()) {
            super.onRefreshing(doScroll);
            return;
        }

        super.onRefreshing(false);

        final LoadingLayout origLoadingView, listViewLoadingView, oppositeListViewLoadingView;
        final int selection, scrollToY;

        switch (getCurrentMode()) {
            case MANUAL_REFRESH_ONLY:
            case PULL_FROM_END:
                origLoadingView = getFooterLayout();
                listViewLoadingView = mFooterLoadingView;
                oppositeListViewLoadingView = mHeaderLoadingView;
                selection = mRefreshableView.getCount() - 1;
                scrollToY = getScrollY() - getFooterSize();
                break;
            case PULL_FROM_START:
            default:
                origLoadingView = getHeaderLayout();
                listViewLoadingView = mHeaderLoadingView;
                oppositeListViewLoadingView = mFooterLoadingView;
                selection = 0;
                scrollToY = getScrollY() + getHeaderSize();
                break;
        }
        DebugLog.i("tags", "====doScroll======"+doScroll+"==========");
        // Hide our original Loading View
        origLoadingView.reset();
        origLoadingView.hideAllViews();

        // Make sure the opposite end is hidden too
        oppositeListViewLoadingView.setVisibility(View.GONE);

        // Show the ListView Loading View and set it to refresh.
        listViewLoadingView.setVisibility(View.VISIBLE);
        listViewLoadingView.refreshing();

        if (doScroll) {
            // We need to disable the automatic visibility changes for now
            disableLoadingLayoutVisibilityChanges();

            // We scroll slightly so that the ListView's header/footer is at the
            // same Y position as our normal header/footer
            setHeaderScroll(scrollToY);

            // Make sure the ListView is scrolled to show the loading
            // header/footer
            mRefreshableView.setSelection(selection);

            // Smooth scroll as normal
            smoothScrollTo(0);
        }
    }


 还记得我们开始时把mListViewExtrasEnabled默认设置成false了吗,
 所以这里直接调用父类的onRefreshing



    protected void onRefreshing(boolean doScroll) {
            super.onRefreshing(doScroll);
            if (getShowIndicatorInternal()) {
                updateIndicatorViewsVisibility();
            }
        }


继续调用父类的onRefreshing ,然后如果右下角的箭头需要显示的话就显示箭头。


        protected void onRefreshing(final boolean doScroll) {
        if (mMode.showHeaderLoadingLayout()) {
            mHeaderLayout.refreshing();
        }
        if (mMode.showFooterLoadingLayout()) {
            mFooterLayout.refreshing();
        }

        if (doScroll) {
            DebugLog.i("tags", "=======mShowViewWhileRefreshing======"
                    + mShowViewWhileRefreshing);
            if (mShowViewWhileRefreshing) {

                // Call Refresh Listener when the Scroll has finished
                OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() {
                    @Override
                    public void onSmoothScrollFinished() {
                        callRefreshListener();
                    }
                };

                switch (mCurrentMode) {
                case MANUAL_REFRESH_ONLY:
                case PULL_FROM_END:
                    smoothScrollTo(getFooterSize(), listener);
                    break;
                default:
                case PULL_FROM_START:
                    smoothScrollTo(-getHeaderSize(), listener);
                    break;
                }
            } else {
                smoothScrollTo(0);
            }
        } else {
            // We're not scrolling, so just call Refresh Listener now
            callRefreshListener();
        }
    }

刷新时,整个View会回到刚刚好把mHeadLayout显示出来的位置,smoothScrollTo(-getHeaderSize(), listener),并且传入了一个监听器,

protected final void smoothScrollTo(int scrollValue,
            OnSmoothScrollFinishedListener listener) {
            //传入了方向和完成滚动的时间
        smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0,
                listener);
    }


private final void smoothScrollTo(int newScrollValue, long duration,
        long delayMillis, OnSmoothScrollFinishedListener listener) {
    if (null != mCurrentSmoothScrollRunnable) {
        mCurrentSmoothScrollRunnable.stop();
    }

    final int oldScrollValue;
    switch (getPullToRefreshScrollDirection()) {
    case HORIZONTAL:
        oldScrollValue = getScrollX();
        break;
    case VERTICAL:
    default:
        oldScrollValue = getScrollY();
        break;
    }
    DebugLog.i("tags", "==oldScrollValue=="+oldScrollValue+"  newScrollValue="+newScrollValue);
    if (oldScrollValue != newScrollValue) {
        if (null == mScrollAnimationInterpolator) {
            // Default interpolator is a Decelerate Interpolator
            mScrollAnimationInterpolator = new DecelerateInterpolator();
        }
        mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(
                oldScrollValue, newScrollValue, duration, listener);

        if (delayMillis > 0) {
            postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
        } else {
            post(mCurrentSmoothScrollRunnable);
        }
    }
}

如果滚动距离有变化,就执行SmoothScrollRunnable线程

    @Override
    public void run() {

        /**
         * Only set mStartTime if this is the first time we're starting,
         * else actually calculate the Y delta
         */
        if (mStartTime == -1) {
            mStartTime = System.currentTimeMillis();
        } else {

            /**
             * We do do all calculations in long to reduce software float
             * calculations. We use 1000 as it gives us good accuracy and
             * small rounding errors
             */
            long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime))
                    / mDuration;
            normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);

            final int deltaY = Math.round((mScrollFromY - mScrollToY)
                    * mInterpolator
                            .getInterpolation(normalizedTime / 1000f));
            mCurrentY = mScrollFromY - deltaY;
            setHeaderScroll(mCurrentY);
        }

        // If we're not at the target Y, keep going...
        if (mContinueRunning && mScrollToY != mCurrentY) {
            ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
        } else {
            if (null != mListener) {
                mListener.onSmoothScrollFinished();
            }
        }
    }

这里首次执行时会记录时间,如果没有滚动到目的地,会继续执行线程 if (mContinueRunning && mScrollToY != mCurrentY) {…}

normalizedTime值是和时间成正比的,而滚动距离
final int deltaY = Math.round((mScrollFromY - mScrollToY)
* mInterpolator
.getInterpolation(normalizedTime / 1000f));

我们来看看传进来的

public class DecelerateInterpolator implements Interpolator {
    public DecelerateInterpolator() {
    }

    /**
     * Constructor
     * 
     * @param factor Degree to which the animation should be eased. Setting factor to 1.0f produces
     *        an upside-down y=x^2 parabola. Increasing factor above 1.0f makes exaggerates the
     *        ease-out effect (i.e., it starts even faster and ends evens slower)
     */
    public DecelerateInterpolator(float factor) {
        mFactor = factor;
    }

    public DecelerateInterpolator(Context context, AttributeSet attrs) {
        TypedArray a =
            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.DecelerateInterpolator);

        mFactor = a.getFloat(com.android.internal.R.styleable.DecelerateInterpolator_factor, 1.0f);

        a.recycle();
    }

    /**
    * 注意传入的是一个和时间成正比的小数  大小在0~1
    * 
    */
    public float getInterpolation(float input) {
        float result;
        if (mFactor == 1.0f) {
            result = (float)(1.0f - (1.0f - input) * (1.0f - input));
        } else {
            result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
        }
        return result;
    }

    private float mFactor = 1.0f;
}

假设调用5次getInterpolation传入的input为0.1, 0.2, 0.3, 0.4, 0.5
输出分别为0.19,0.36,0.51,0.64,0.75 我们可以发现输出的值变化越来越小,
开始增加了0.17,第二次增加了0.15,第三次增加了0.13,最后增加了0.11 所以相应的滑动距离的变化也越来越慢,所以说当我们下拉然后松开手时,控件会减速的滑到适当的位置执行刷新。回到SmoothScrollRunnable线程,
if (null != mListener) {
mListener.onSmoothScrollFinished();
}
执行传入的监听器,最后调用的是我们传入的刷新监听器。

6.最后我们再来看看mListViewExtrasEnabled这个变量,如果它为true的话,会出现什么情况。

首先是开始初始化时init中调用了PullToRefreshListView的handleStyledAttributes

@Override
    protected void handleStyledAttributes(TypedArray a) {
        super.handleStyledAttributes(a);
        //默认为true
        mListViewExtrasEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrListViewExtrasEnabled, false);
        if (mListViewExtrasEnabled) {
            final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL);
            // Create Loading Views ready for use later
            /**
             * 创建头部\尾部的布局  添加到FrameLayout  
             * 再将头部整个FrameLayout添加到ListView
             * 中
             */
            FrameLayout frame = new FrameLayout(getContext());
            mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, a);
            mHeaderLoadingView.setVisibility(View.GONE);
            frame.addView(mHeaderLoadingView, lp);
            mRefreshableView.addHeaderView(frame, null, false);
            mLvFooterLoadingFrame = new FrameLayout(getContext());
            mFooterLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_END, a);
            mFooterLoadingView.setVisibility(View.GONE);
            mLvFooterLoadingFrame.addView(mFooterLoadingView, lp);
            /**
             * If the value for Scrolling While Refreshing hasn't been
             * explicitly set via XML, enable Scrolling While Refreshing.
             */
            if (!a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
                setScrollingWhileRefreshingEnabled(true);
            }
        }
    }

“`

创建LoadingLayout加在init中创建的ListView的headView中。
再回到PullToRefreshListView的onRefreshing中,当刷新时,会将mHeadLayout隐藏,将ListView中添加的headView显示,并设置滚动距离。
所以mListViewExtrasEnabled这个变量是设置刷新时头部的LoadingLayout是不是ListView的headView的,如果你不希望刷新时的LoadingLayout是ListView的headView那就设置为false吧。添加了headView就会对onitemclick等函数的位置有所影响需要注意。
pulltoRefresh的框架就说到这了,还有很多细节没讲到,大家可以自己在分析分析。第一次写博客,也许有很多错误,也可能写的不好,希望大家多指正。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值