SwipeRefreshLayout Source Analysis

Posted by mattal999 on Wed, 12 Jun 2019 18:33:56 +0200

1. Overview

In the previous blog, we found that using SwipeRefreshLayout for drop-down refresh is extremely convenient and fast, and the pull-refresh control is separated from the control for displaying content, making it very easy to extend and maintain code.But its refresh UI is really simple. It's hard to get in the eyes of company designers, so we need to modify the refresh UI.

This blog analyzes the implementation principle of SwipeRefreshLayout from a source code perspective, that is, to learn the implementation idea of SwipeRefreshLayout, you can also achieve the refresh UI you want.

SwipeRefreshLayout is a view class that inherits ViewGroup.The core methods are:
Construction method;
onMeasure();
onLayout();
onIntercepterTouchEvent(); (Key)
onTouchEvent(); (Key)
The source code is analyzed step by step in the order described above.

2. Construction methods

The core code for the construction method is as follows:

    public SwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

      /**The minimum distance at which mobile events are triggered, and sometimes when a custom View handles touch events, it is necessary to determine whether a user really has a movie, which is how the system works.Indicates that when sliding, the movement of the hand is greater than the distance returned before the control starts to move.*/
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        //Get the Differential of Moving Animation
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);//Get the width of the refreshed View
        mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density);//Get the height of the refreshed View

        createProgressView();//Create Refresh View
    }

The source code for the createProgressView method is as follows:

    private void createProgressView() {
        mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2);
        mProgress = new MaterialProgressDrawable(getContext(), this);
        mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
        mCircleView.setImageDrawable(mProgress);
        mCircleView.setVisibility(View.GONE);
        addView(mCircleView);//Add mCircleView to parent View.
    }

Note: The construction method mainly does two things:
1, use addView to add a refreshed view
2, Initialization parameters, refresh view size, maximum sliding distance, etc.

Three, onMeasure method

The core code of the method is:

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) {
            ensureTarget();//Find mTarget, which is both a view that hosts the data and may be ListView, ScrollView, RecyclerView.
        }
        if (mTarget == null) {
            return;
        }
        //Measure mTarget
        mTarget.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
         //Measure Refresh View    
        mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
    }

Note: There is no special note in the onMeasure method to find two sub view s and measure.

Fourth, onLayout method

The core code of the method is:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();
        final View child = mTarget;
        final int childLeft = getPaddingLeft();
        final int childTop = getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();

        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);//Layout mTarget
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);//Layout Refresh View
    }

Note: The location of the refreshed view is related to the mCurrentTarget OffsetTop.

Fifth, onIntercepterTouchEvent method

This method is the focus of the refresh function implementation.If the onMeasure method and onLayout method are just knowledge of customizing the View, then the onIntercepterTouchEvent method is at the core of the refresh functionality implementation.Be careful with this method.
The core code for the method is as follows:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();//mTarget object found
        final int action = MotionEventCompat.getActionMasked(ev);//Get gesture action

        /**The following situations return false directly, indicating no interception.canChildScrollUp is very important and will be explained in more detail later.*/
        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                final float initialDownY = getMotionEventY(ev, mActivePointerId);
                if (initialDownY == -1) {
                    return false;
                }
                mInitialDownY = initialDownY;
                break;

            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                    return false;
                }

                final float y = getMotionEventY(ev, mActivePointerId);
                if (y == -1) {
                    return false;
                }
                final float yDiff = y - mInitialDownY;
                if (yDiff > mTouchSlop && !mIsBeingDragged) {
                    mInitialMotionY = mInitialDownY + mTouchSlop;
                    mIsBeingDragged = true;
                    mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
                }
                break;

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }

Topics: Mobile