Android源代码学习笔记:android-Ultra-Pull-To-Refresh-master

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android源代码学习笔记:android-Ultra-Pull-To-Refresh-master相关的知识,希望对你有一定的参考价值。

技术分享

技术分享

 

学习要点:下拉刷新

这个小应用包含了在使用到GridView,FrameLayout,TextView,ListView等等控件时的所有下拉刷新效果下拉刷新具有极为强大的使用空间,几乎所有的应用都会用到。

源码解析文档:http://a.codekk.com/detail/android/Grumoon/android-Ultra-Pull-To-Refresh%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90

public interface PtrHandler {

   // 刷新回调函数,用户在这里写自己的刷新功能实现,处理业务数据的刷新。 
    public boolean checkCanDoRefresh(final PtrFrameLayout frame, final View content, final View header);

  /*判断是否可以下拉刷新。 UltraPTR 的 Content 可以包含任何内容,用户在这里判断决定是否可以下拉。
例如,如果 Content 是 TextView,则可以直接返回 true,表示可以下拉刷新。
如果 Content 是 ListView,当第一条在顶部时返回 true,表示可以下拉刷新。
如果 Content 是 ScrollView,当滑动到顶部时返回 true,表示可以刷新*/
    public void onRefreshBegin(final PtrFrameLayout frame);
}

 在PtrDefaultHandler中会实现这两个接口。

@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
    return checkContentCanBePulledDown(frame, content, header);
}
public static boolean checkContentCanBePulledDown(PtrFrameLayout frame, View content, View header) {
    /**
     * 如果 Content 不是 ViewGroup,返回 true,表示可以下拉</br>
     * 例如:TextView,ImageView
     */
    if (!(content instanceof ViewGroup)) {
        return true;
    }

    ViewGroup viewGroup = (ViewGroup) content;

    /**
     * 如果 Content 没有子 View(内容为空)时候,返回 true,表示可以下拉
     */
    if (viewGroup.getChildCount() == 0) {
        return true;
    }

    /**
     * 如果 Content 是 AbsListView(ListView,GridView),当第一个 item 不可见是,返回 false,不可以下拉。
     */
    if (viewGroup instanceof AbsListView) {
        AbsListView listView = (AbsListView) viewGroup;
        if (listView.getFirstVisiblePosition() > 0) {
            return false;
        }
    }

    /**
     * 如果 SDK 版本为 14 以上,可以用 canScrollVertically 判断是否能在竖直方向上,向上滑动</br>
     * 不能向上,表示已经滑动到在顶部或者 Content 不能滑动,返回 true,可以下拉</br>
     * 可以向上,返回 false,不能下拉
     */
    if (Build.VERSION.SDK_INT >= 14) {
        return !content.canScrollVertically(-1);
    } else {
        /**
         * SDK 版本小于 14,如果 Content 是 ScrollView 或者 AbsListView,通过 getScrollY 判断滑动位置 </br>
         * 如果位置为 0,表示在最顶部,返回 true,可以下拉
         */
        if (viewGroup instanceof ScrollView || viewGroup instanceof AbsListView) {
            return viewGroup.getScrollY() == 0;
        }
    }

    /**
     * 最终判断,判断第一个子 View 的 top 值</br>
     * 如果第一个子 View 有 margin,则当 top==子 view 的 marginTop+content 的 paddingTop 时,表示在最顶部,返回 true,可以下拉</br>
     * 如果没有 margin,则当 top==content 的 paddinTop 时,表示在最顶部,返回 true,可以下拉
     */
    View child = viewGroup.getChildAt(0);
    ViewGroup.LayoutParams glp = child.getLayoutParams();
    int top = child.getTop();
    if (glp instanceof ViewGroup.MarginLayoutParams) {
        ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) glp;
        return top == mlp.topMargin + viewGroup.getPaddingTop();
    } else {
        return top == viewGroup.getPaddingTop();
    }
}
public static boolean canChildScrollUp(View view) {
    if (android.os.Build.VERSION.SDK_INT < 14) {
        if (view instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) view;
            return absListView.getChildCount() > 0
                    && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                    .getTop() < absListView.getPaddingTop());
        } else {
            return view.getScrollY() > 0;
        }
    } else {
        return view.canScrollVertically(-1);
    }
}
public static boolean checkContentCanBePulledDown(PtrFrameLayout frame, View content, View header) {
    return !canChildScrollUp(content);
}

以上代码是用来判断是否可以滑动的,给出了AbsListView和非AbsListView的判断方法。
/*
   用于实现UI界面的更新
*/
public interface PtrUIHandler {

    /**
     * When the content view has reached top and refresh has been completed, view will be reset.
     *
     * @param frame
     */
    public void onUIReset(PtrFrameLayout frame);

    /**
     * prepare for loading
     *
     * @param frame
     */
    public void onUIRefreshPrepare(PtrFrameLayout frame);

    /**
     * perform refreshing UI
     */
    public void onUIRefreshBegin(PtrFrameLayout frame);

    /**
     * perform UI after refresh
     */
    public void onUIRefreshComplete(PtrFrameLayout frame);

    public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator);
}

 


PtrFrameLayout

在自定义空间PtrFrameLayout中,定义了mHeaderView和  mContent,表示头部信息和内容信息。

在PtrFrameLayout中实现了view的onFinishInflate 类,onFinishInflate 是在当View中所有的子控件均被映射成xml后触发,例如MyView mv = (MyView)View.inflate (context,R.layout.my_view,null);当加载完成xml后,就会执行那个方法。在这个函数中,通过viewgroup函数getChild函数,获得了两个view,从而赋值给了
header与content两个函数。

 

/*在这个函数中,通过viewgroup函数getChild函数,获得了两个view,从而赋值给了
header与content两个函数。
*/

protected void onFinishInflate() {
        final int childCount = getChildCount();
        if (childCount > 2) {
            throw new IllegalStateException("PtrFrameLayout only can host 2 elements");
        } else if (childCount == 2) {
            if (mHeaderId != 0 && mHeaderView == null) {
                mHeaderView = findViewById(mHeaderId);
            }
            if (mContainerId != 0 && mContent == null) {
                mContent = findViewById(mContainerId);
            }

            // not specify header or content
            if (mContent == null || mHeaderView == null) {

                View child1 = getChildAt(0);
                View child2 = getChildAt(1);
                if (child1 instanceof PtrUIHandler) {
                    mHeaderView = child1;
                    mContent = child2;
                } else if (child2 instanceof PtrUIHandler) {
                    mHeaderView = child2;
                    mContent = child1;
                } else {
                    // both are not specified
                    if (mContent == null && mHeaderView == null) {
                        mHeaderView = child1;
                        mContent = child2;
                    }
                    // only one is specified
                    else {
                        if (mHeaderView == null) {
                            mHeaderView = mContent == child1 ? child2 : child1;
                        } else {
                            mContent = mHeaderView == child1 ? child2 : child1;
                        }
                    }
                }
            }
        } else if (childCount == 1) {
            mContent = getChildAt(0);
        } else {
            TextView errorView = new TextView(getContext());
            errorView.setClickable(true);
            errorView.setTextColor(0xffff6600);
            errorView.setGravity(Gravity.CENTER);
            errorView.setTextSize(20);
            errorView.setText("The content view in PtrFrameLayout is empty. Do you forget to specify its id in xml layout file?");
            mContent = errorView;
            addView(mContent);
        }
        if (mHeaderView != null) {
            mHeaderView.bringToFront();
        }
        super.onFinishInflate();
    }

 


view的描绘流程:

onMeasure:计算空间的长宽 ——》 onLayout:计算子控件的位置 ——》 onDraw:进行绘制

重写 onMeasure,测量 Header 和 Content,将 Header 的高度赋值给 mHeaderHeight 变量,将计算出的下拉刷新偏移量赋值给 mOffsetToRefresh变量。

重写 onLayout 方法来确定子 View 的位置。

对于 Headerf:inal int top = paddingTop + lp.topMargin + offsetX - mHeaderHeight;

对于 Content:final int top = paddingTop + lp.topMargin + offsetX;

这就回答了,如何制造出下拉时显示header的效果。在 LonLayout中调用了layoutChildren():

private void layoutChildren() {
        int offsetX = mPtrIndicator.getCurrentPosY();
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();

        if (mHeaderView != null) {
            MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
            final int left = paddingLeft + lp.leftMargin;
            final int top = paddingTop + lp.topMargin + offsetX - mHeaderHeight;
            final int right = left + mHeaderView.getMeasuredWidth();
            final int bottom = top + mHeaderView.getMeasuredHeight();
            mHeaderView.layout(left, top, right, bottom);
            if (DEBUG && DEBUG_LAYOUT) {
                PtrCLog.d(LOG_TAG, "onLayout header: %s %s %s %s", left, top, right, bottom);
            }
        }
        if (mContent != null) {
            if (isPinContent()) {
                offsetX = 0;
            }
            MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams();
            final int left = paddingLeft + lp.leftMargin;
            final int top = paddingTop + lp.topMargin + offsetX;
            final int right = left + mContent.getMeasuredWidth();
            final int bottom = top + mContent.getMeasuredHeight();
            if (DEBUG && DEBUG_LAYOUT) {
                PtrCLog.d(LOG_TAG, "onLayout content: %s %s %s %s", left, top, right, bottom);
            }
            mContent.layout(left, top, right, bottom);
        }
    }

通过  mHeaderView.layout(left, top, right, bottom);设置了mHeaderView在viewgrop中的位置。

 

事件传输

1、基础知识

(1) 所有 Touch 事件都被封装成了 MotionEvent 对象,包括 Touch 的位置、时间、历史记录以及第几个手指(多指触摸)等。 

(2) 事件类型分为 ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL,每个事件都是以 ACTION_DOWN 开始 ACTION_UP 结束。 

(3) 对事件的处理包括三类,分别为传递——dispatchTouchEvent()函数、拦截——onInterceptTouchEvent()函数、消费——onTouchEvent()函数和 OnTouchListener 

2、传递流程

(1) 事件从 Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的 View(ViewGroup)开始一直往下(子 View)传递。子 View 可以通过 onTouchEvent()对事件进行处理。 

(2) 事件由父 View(ViewGroup)传递给子 View,ViewGroup 可以通过 onInterceptTouchEvent()对事件做拦截,停止其往下传递。 

(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子 View 没有消费事件,事件会反向往上传递,这时父 View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到 Activity 的 onTouchEvent()函数。 

(4) 如果 View 没有对 ACTION_DOWN 进行消费,之后的其他事件不会传递过来。 

(5) OnTouchListener 优先于 onTouchEvent()对事件进行消费。
上面的消费即表示相应函数返回值为 true
 public boolean dispatchTouchEvent(MotionEvent e) {
        if (!isEnabled() || mContent == null || mHeaderView == null) {
            return dispatchTouchEventSupper(e);
        }
        int action = e.getAction();
        switch (action) {
/*功能上,通过执行 tryToPerformRefresh 方法,如果向下拉动的位移已经超过了触发下拉刷新的偏移量 mOffsetToRefresh,并且当前状态是 PTR_STATUS_PREPARE,执行刷新功能回调。
行为上,如果没有达到触发刷新的偏移量,或者当前状态为 PTR_STATUS_COMPLETE,或者刷新过程中不保持头部位置,则执行向上的位置回复动作。*/
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mPtrIndicator.onRelease();
                if (mPtrIndicator.hasLeftStartPosition()) {
                    if (DEBUG) {
                        PtrCLog.d(LOG_TAG, "call onRelease when user release");
                    }
                    onRelease(false);
                    if (mPtrIndicator.hasMovedAfterPressedDown()) {
                        sendCancelEvent();
                        return true;
                    }
                    return dispatchTouchEventSupper(e);
                } else {
                    return dispatchTouchEventSupper(e);
                }

            case MotionEvent.ACTION_DOWN:
                mHasSendCancelEvent = false;
                mPtrIndicator.onPressDown(e.getX(), e.getY());

                mScrollChecker.abortIfWorking();

                mPreventForHorizontal = false;
                // The cancel event will be sent once the position is moved.
                // So let the event pass to children.
                // fix #93, #102
                dispatchTouchEventSupper(e);
                return true;
/*2.ACTION_MOVE 中判断是否可以纵向 move。
ACTION_MOVE 的方向向下,如果 mPtrHandler 不为空,并且 mPtrHandler.checkCanDoRefresh 返回值为 true,则可以移动, Header 和 Content 向下移动,否则,事件交由父类处理。
ACTION_MOVE 的方向向上,如果当前位置大于起始位置,则可以移动,Header 和 Content 向上移动,否则,事件交由父类处理。*/
            case MotionEvent.ACTION_MOVE:
                mLastMoveEvent = e;
                mPtrIndicator.onMove(e.getX(), e.getY());
                float offsetX = mPtrIndicator.getOffsetX();
                float offsetY = mPtrIndicator.getOffsetY();

                if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
                    if (mPtrIndicator.isInStartPosition()) {
                        mPreventForHorizontal = true;
                    }
                }
                if (mPreventForHorizontal) {
                    return dispatchTouchEventSupper(e);
                }

                boolean moveDown = offsetY > 0;
                boolean moveUp = !moveDown;
                boolean canMoveUp = mPtrIndicator.hasLeftStartPosition();

                if (DEBUG) {
                    boolean canMoveDown = mPtrHandler != null && mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView);
                    PtrCLog.v(LOG_TAG, "ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s", offsetY, mPtrIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown);
                }

                // disable move when header not reach top
                if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) {
                    return dispatchTouchEventSupper(e);
                }

                if ((moveUp && canMoveUp) || moveDown) {
                    movePos(offsetY);
                    return true;
                }
        }
        return dispatchTouchEventSupper(e);
    }

 

 

 

private void onRelease(boolean stayForLoading) {

        tryToPerformRefresh();

        if (mStatus == PTR_STATUS_LOADING) {
            // keep header for fresh
            if (mKeepHeaderWhenRefresh) {
                // scroll header back
                if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) {
                    mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose);
                } else {
                    // do nothing
                }
            } else {
                tryScrollBackToTopWhileLoading();
            }
        } else {
            if (mStatus == PTR_STATUS_COMPLETE) {
                notifyUIRefreshComplete(false);
            } else {
                tryScrollBackToTopAbortRefresh();
            }
        }
    }

onRealse()中,如果功能上,通过执行 tryToPerformRefresh 方法,如果向下拉动的位移已经超过了触发下拉刷新的偏移量 mOffsetToRefresh,并且当前状态是 PTR_STATUS_PREPARE,执行刷新功能回调。行为上,如果没有达到触发刷新的偏移量,或者当前状态为 PTR_STATUS_COMPLETE,或者刷新过程中不保持头部位置,则执行向上的位置回复动作。

PtrClassicDefaultHeader的具体实现:

@Override
public void onUIReset(PtrFrameLayout frame) {
//重置 View
    resetView();
    mShouldShowLastUpdate = true;
    tryUpdateLastUpdateTime();
}
private void resetView() {
//隐藏忙碌进度条
    hideRotateView();
    mProgressBar.setVisibility(INVISIBLE);
}
private void hideRotateView() {
//隐藏箭头 View
    mRotateView.clearAnimation();
    mRotateView.setVisibility(INVISIBLE);
}

PtrClassicFrameLayout.java:经典下拉刷新实现类

 

 

 

 

 

 

 

         

技术分享

以上是关于Android源代码学习笔记:android-Ultra-Pull-To-Refresh-master的主要内容,如果未能解决你的问题,请参考以下文章

Android源代码学习笔记:android-Ultra-Pull-To-Refresh-master

Android第一行代码--学习笔记(更新中ing)

ANDROID_MARS学习笔记_S02_007_Animation第一种使用方式:代码

Android:学习笔记

android学习笔记

Android第一行代码学习笔记---手动创建活动