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;
}