Java tutorial
/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.alex.swiperefreshlayout; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.NestedScrollingParent; import android.support.v4.view.NestedScrollingParentHelper; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.DecelerateInterpolator; import android.view.animation.Transformation; import android.widget.AbsListView; import org.alex.callback.OnRefreshListener; import org.alex.simplerefreshlayout.R; import org.alex.swiperefreshlayout.callback.IFootView; import org.alex.swiperefreshlayout.callback.OnFootViewChangerListener; import org.alex.swiperefreshlayout.model.FootType; import org.alex.swiperefreshlayout.view.CircleImageView; import org.alex.swiperefreshlayout.view.MaterialProgressDrawable; import org.alex.util.LogUtil; import static android.R.attr.enabled; @SuppressWarnings("all") public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild { public static final int LARGE = MaterialProgressDrawable.LARGE; public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; @VisibleForTesting /** * */ protected static final int CIRCLE_DIAMETER = 40; @VisibleForTesting /** * */ protected static final int CIRCLE_DIAMETER_LARGE = 56; protected static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); /** * ?? ?? */ protected static final int MAX_ALPHA = 255; protected static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); protected static final float DECELERATE_INTERPOLATION_FACTOR = 2f; protected static final int INVALID_POINTER = -1; protected static final float DRAG_RATE = .5f; // Max amount of circle that can be filled by progress during swipe gesture, // where 1.0 is a full circle protected static final float MAX_PROGRESS_ANGLE = .8f; protected static final int SCALE_DOWN_DURATION = 150; protected static final int ALPHA_ANIMATION_DURATION = 300; protected static final int ANIMATE_TO_TRIGGER_DURATION = 200; protected static final int ANIMATE_TO_START_DURATION = 200; // Default background for the progress spinner protected static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; // Default offset in dips from the top of the view to where the progress spinner should stop protected static final int DEFAULT_CIRCLE_TARGET = 64; /** * xml SwipeRefreshLayout ? */ protected View targetView; protected OnRefreshListener onRefreshListener; /** * */ protected boolean refreshing = false; /** * ? false???loadMore */ protected boolean loadMoreEnabled; /** * */ protected boolean isLoadingMore; /** * ? ? */ protected boolean isLoadingMoreCancel; protected int touchSlop; /** * ? ?? */ protected float refreshMaxDragDistance = -1; /** * ?? */ protected int loadMoreMinDistance; /** * ? ? */ protected float swipeDownUnconsumed; /** * ? ? */ protected float swipeUpUnconsumed; protected final NestedScrollingParentHelper nestedScrollingParentHelper; protected final NestedScrollingChildHelper nestedScrollingChildHelper; protected final int[] mParentScrollConsumed = new int[2]; protected final int[] parentOffsetInWindow = new int[2]; protected boolean nestedScrollInProgress; protected int mMediumAnimationDuration; /** * CircleView ?CircleView? */ protected int currentTargetOffsetTop; protected float mInitialMotionY; protected float mInitialDownY; protected boolean isBeingDragged; protected int mActivePointerId = INVALID_POINTER; // Whether this item is scaled up rather than clipped protected boolean scale; // Target is returning to its start offset because it was cancelled or a // refresh was triggered. protected boolean mReturningToStart; protected final DecelerateInterpolator decelerateInterpolator; protected static final int[] LAYOUT_ATTRS = new int[] { enabled }; protected CircleImageView circleView; protected int mCircleViewIndex = -1; protected int headFrom; protected float startingScale; protected int originalOffsetTop; protected int spinnerOffsetEnd; protected MaterialProgressDrawable progress; /** * */ protected Animation scaleAnimation; protected Animation scaleDownAnimation; protected Animation alphaStartAnimation; protected Animation alphaMaxAnimation; protected Animation scaleDownToStartAnimation; protected boolean notify; /** * ? */ protected int circleDiameter; // Whether the client has set a custom starting position; protected boolean usingCustomStart; protected OnChildScrollUpCallback childScrollUpCallback; protected OnChildScrollDownCallback childScrollDownCallback; protected IFootView footView; protected int footType; protected OnFootViewChangerListener onFootViewChangerListener; /** * ? */ private Animation.AnimationListener refreshAnimatorListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (isRefreshing()) { // Make sure the progress view is fully visible progress.setAlpha(MAX_ALPHA); progress.start(); if (notify) { callRefreshCallback(); } /*circleView top ?CircleView */ currentTargetOffsetTop = circleView.getTop(); } else { reset(); } } }; private int width; private int height; /** * ? loading? offset */ private int footOffsetMax; /** * ? loading? offset */ private int headOffsetMax; protected void reset() { //LogUtil.e("?"); circleView.clearAnimation(); progress.stop(); circleView.setVisibility(View.GONE); setColorViewAlpha(MAX_ALPHA); // Return the circle to its start position if (scale) { setAnimationProgress(0 /* animation complete and view is hidden */); } else { /* requires update */ setTargetOffsetTopAndBottom(originalOffsetTop - currentTargetOffsetTop, true); } currentTargetOffsetTop = circleView.getTop(); } /** * ? * * @param enabled */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (!enabled) { reset(); } } /** * ? * * @param enabled */ public void setRefreshEnable(boolean enabled) { setEnabled(enabled); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); reset(); } private void setColorViewAlpha(int targetAlpha) { circleView.getBackground().setAlpha(targetAlpha); progress.setAlpha(targetAlpha); } /** * The refresh indicator starting and resting position is always positioned * near the top of the refreshing content. This position is a consistent * location, but can be adjusted in either direction based on whether or not * there is a toolbar or actionbar present. * <p> * <strong>Note:</strong> Calling this will reset the position of the refresh indicator to * <code>start</code>. * </p> * * @param scale Set to true if there is no view at a higher z-order than where the progress * spinner is set to appear. Setting it to true will cause indicator to be scaled * up rather than clipped. * @param start The offset in pixels from the top of this view at which the * progress spinner should appear. * @param end The offset in pixels from the top of this view at which the * progress spinner should come to rest after a successful swipe * gesture. */ public void setProgressViewOffset(boolean scale, int start, int end) { this.scale = scale; originalOffsetTop = start; spinnerOffsetEnd = end; usingCustomStart = true; reset(); refreshing = false; } /** * @return The offset in pixels from the top of this view at which the progress spinner should * appear. */ public int getProgressViewStartOffset() { return originalOffsetTop; } /** * @return The offset in pixels from the top of this view at which the progress spinner should * come to rest after a successful swipe gesture. */ public int getProgressViewEndOffset() { return spinnerOffsetEnd; } /** * The refresh indicator resting position is always positioned near the top * of the refreshing content. This position is a consistent location, but * can be adjusted in either direction based on whether or not there is a * toolbar or actionbar present. * * @param scale Set to true if there is no view at a higher z-order than where the progress * spinner is set to appear. Setting it to true will cause indicator to be scaled * up rather than clipped. * @param end The offset in pixels from the top of this view at which the * progress spinner should come to rest after a successful swipe * gesture. */ public void setProgressViewEndTarget(boolean scale, int end) { spinnerOffsetEnd = end; this.scale = scale; circleView.invalidate(); } /** * One of DEFAULT, or LARGE. */ public void setSize(int size) { if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { return; } final DisplayMetrics metrics = getResources().getDisplayMetrics(); if (size == MaterialProgressDrawable.LARGE) { circleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); } else { circleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); } // force the bounds of the progress circle inside the circle view to // update by setting it to null before updating its size and then // re-setting it circleView.setImageDrawable(null); progress.updateSizes(size); circleView.setImageDrawable(progress); } /** * Simple constructor to use when creating a SwipeRefreshLayout from code. * * @param context */ public SwipeRefreshLayout(Context context) { this(context, null); } /** * Constructor that is called when inflating SwipeRefreshLayout from XML. * * @param context * @param attrs */ public SwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); isLoadingMore = false; loadMoreEnabled = true; loadMoreMinDistance = (int) dp2px(72); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMediumAnimationDuration = getResources().getInteger(android.R.integer.config_mediumAnimTime); setWillNotDraw(false); decelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); final DisplayMetrics metrics = getResources().getDisplayMetrics(); circleDiameter = (int) (CIRCLE_DIAMETER * metrics.density); createProgressView(); ViewCompat.setChildrenDrawingOrderEnabled(this, true); // the absolute offset has to take into account that the circle starts at an offset spinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density); refreshMaxDragDistance = spinnerOffsetEnd; nestedScrollingParentHelper = new NestedScrollingParentHelper(this); nestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); originalOffsetTop = currentTargetOffsetTop = -circleDiameter; LogUtil.e("originalOffsetTop = " + originalOffsetTop); moveToStart(1.0f); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); setEnabled(a.getBoolean(0, true)); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwipeRefreshLayout); footType = typedArray.getInteger(R.styleable.SwipeRefreshLayout_srl_footType, 100); typedArray.recycle(); a.recycle(); setColorSchemeColors(0xff99cc00, 0xffffbb33, 0xffff4444); } @Override protected int getChildDrawingOrder(int childCount, int i) { if (mCircleViewIndex < 0) { return i; } else if (i == childCount - 1) { // Draw the selected child last return mCircleViewIndex; } else if (i >= mCircleViewIndex) { // Move the children after the selected child earlier one return i + 1; } else { // Keep the children before the selected child the same return i; } } private void createProgressView() { circleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT); progress = new MaterialProgressDrawable(getContext(), this); progress.setBackgroundColor(CIRCLE_BG_LIGHT); circleView.setImageDrawable(progress); circleView.setVisibility(View.GONE); //circleView.setVisibility(View.VISIBLE); addView(circleView); } /** * Set the listener to be notified when a refresh is triggered via the swipe * gesture. */ public void setOnRefreshListener(OnRefreshListener listener) { onRefreshListener = listener; } /** * Pre API 11, alpha is used to make the progress circle appear instead of scale. */ private boolean isAlphaUsedForScale() { return android.os.Build.VERSION.SDK_INT < 11; } /** * Notify the widget that refresh state has changed. Do not call this when * refresh is triggered by a swipe gesture. * * @param refreshing Whether or not the view should show refresh progress. */ public void setRefreshing(boolean refreshing) { if (refreshing && this.refreshing != refreshing) { this.refreshing = refreshing; } else { setRefreshing(refreshing, false); } } private void startScaleUpAnimation(AnimationListener listener) { circleView.setVisibility(View.VISIBLE); if (android.os.Build.VERSION.SDK_INT >= 11) { // Pre API 11, alpha is used in place of scale up to show the // progress circle appearing. // Don't adjust the alpha during appearance otherwise. progress.setAlpha(MAX_ALPHA); } scaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(interpolatedTime); } }; scaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { circleView.setAnimationListener(listener); } circleView.clearAnimation(); circleView.startAnimation(scaleAnimation); } /** * Pre API 11, this does an alpha animation. * * @param progress */ void setAnimationProgress(float progress) { if (isAlphaUsedForScale()) { setColorViewAlpha((int) (progress * MAX_ALPHA)); } else { ViewCompat.setScaleX(circleView, progress); ViewCompat.setScaleY(circleView, progress); } } public View getCircleView() { return circleView; } /** * * * @param refreshing true * @param notify */ public void setRefreshing(boolean refreshing, final boolean notify) { if (this.refreshing != refreshing) { this.notify = notify; ensureTarget(); this.refreshing = refreshing; if (this.refreshing) { LogUtil.e("currentTargetOffsetTop = " + currentTargetOffsetTop); headAnimateOffsetToCorrectPosition(currentTargetOffsetTop, refreshAnimatorListener); } else { startScaleDownAnimation(refreshAnimatorListener); } } } /** * CircleView ?? ??UI? * * @param visible */ public void setVisibility4CircleViewAtTop(boolean visible) { if (!visible) { circleView.setVisibility(View.GONE); circleView.getBackground().setAlpha(0); progress.setAlpha(0); progress.stop(); reset(); return; } int endTarget = 0; if (!usingCustomStart) { endTarget = spinnerOffsetEnd + originalOffsetTop; } else { endTarget = spinnerOffsetEnd; } setTargetOffsetTopAndBottom(endTarget - currentTargetOffsetTop, true); startScaleUpAnimation(refreshAnimatorListener); } /** * CircleView ?? ??UI? * * @param visible */ public void setVisibility4CircleViewAtBottom(boolean visible) { if (!visible) { circleView.setVisibility(View.GONE); circleView.getBackground().setAlpha(0); progress.setAlpha(0); progress.stop(); reset(); return; } progress.setAlpha(MAX_ALPHA); progress.start(); circleView.setVisibility(View.VISIBLE); circleView.getBackground().setAlpha(MAX_ALPHA); circleView.bringToFront(); circleView.setScaleX(1F); circleView.setScaleY(1F); } protected void startScaleDownAnimation(Animation.AnimationListener listener) { scaleDownAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(1 - interpolatedTime); } }; scaleDownAnimation.setDuration(SCALE_DOWN_DURATION); circleView.setAnimationListener(listener); circleView.clearAnimation(); circleView.startAnimation(scaleDownAnimation); } private void startProgressAlphaStartAnimation() { alphaStartAnimation = startAlphaAnimation(progress.getAlpha(), STARTING_PROGRESS_ALPHA); } private void startProgressAlphaMaxAnimation() { alphaMaxAnimation = startAlphaAnimation(progress.getAlpha(), MAX_ALPHA); } private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { // Pre API 11, alpha is used in place of scale. Don't also use it to // show the trigger point. if (scale && isAlphaUsedForScale()) { return null; } Animation alpha = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { progress.setAlpha((int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime))); } }; alpha.setDuration(ALPHA_ANIMATION_DURATION); // Clear out the previous animation listeners. circleView.setAnimationListener(null); circleView.clearAnimation(); circleView.startAnimation(alpha); return alpha; } /** * Use {@link #setProgressBackgroundColorSchemeResource(int)} */ public void setProgressBackgroundColor(int colorRes) { setProgressBackgroundColorSchemeResource(colorRes); } /** * Set the background color of the progress spinner disc. * * @param colorRes Resource id of the color. */ public void setProgressBackgroundColorSchemeResource(@ColorRes int colorRes) { setProgressBackgroundColorSchemeColor(ContextCompat.getColor(getContext(), colorRes)); } /** * Set the background color of the progress spinner disc. * * @param color */ public void setProgressBackgroundColorSchemeColor(@ColorInt int color) { circleView.setBackgroundColor(color); progress.setBackgroundColor(color); } /** * Use {@link #setColorSchemeResources(int...)} */ public void setColorScheme(int... colors) { setColorSchemeResources(colors); } /** * Set the color resources used in the progress animation from color resources. * The first color will also be the color of the bar that grows in response * to a user swipe gesture. * * @param colorResIds */ public void setColorSchemeResources(int... colorResIds) { final Context context = getContext(); int[] colorRes = new int[colorResIds.length]; for (int i = 0; i < colorResIds.length; i++) { colorRes[i] = ContextCompat.getColor(context, colorResIds[i]); } setColorSchemeColors(colorRes); } /** * Set the colors used in the progress animation. The first * color will also be the color of the bar that grows in response to a user * swipe gesture. * * @param colors */ public void setColorSchemeColors(@ColorInt int... colors) { ensureTarget(); progress.setColorSchemeColors(colors); } /** * @return Whether the SwipeRefreshWidget is actively showing refresh * progress. */ public boolean isRefreshing() { return refreshing; } /** * xml SwipeRefreshLayout ? */ private void ensureTarget() { if (targetView == null) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (!child.equals(circleView)) { targetView = child; } else if (targetView instanceof RecyclerView) { RecyclerView recyclerView = (RecyclerView) targetView; if (FootType.useFootViewInAdapter == footType) { footView = (IFootView) recyclerView.getAdapter(); } break; } } } } /** * ? ? ?? dp * * @param distance */ public void setRefreshDistanceToTriggerSync(int distance) { refreshMaxDragDistance = getResources().getDisplayMetrics().density * distance; } /** * ? ? ?? px * * @param distance */ public int getRefreshDistanceToTriggerSync() { return (int) refreshMaxDragDistance; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { width = getMeasuredWidth(); height = getMeasuredHeight(); if (getChildCount() == 0) { return; } if (targetView == null) { ensureTarget(); } if (targetView == null) { return; } final View child = targetView; 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); circleViewLayoutAtTop(); } private void circleViewLayoutAtTop() { int circleWidth = circleView.getMeasuredWidth(); int circleHeight = circleView.getMeasuredHeight(); circleView.layout((width / 2 - circleWidth / 2), currentTargetOffsetTop, (width / 2 + circleWidth / 2), currentTargetOffsetTop + circleHeight); } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (targetView == null) { ensureTarget(); } if (targetView == null) { return; } targetView.measure( MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); circleView.measure(MeasureSpec.makeMeasureSpec(circleDiameter, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(circleDiameter, MeasureSpec.EXACTLY)); mCircleViewIndex = -1; // Get the index of the circleview. for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == circleView) { mCircleViewIndex = index; break; } } } /** * Get the diameter of the progress circle that is displayed as part of the * swipe to refresh layout. * * @return Diameter in pixels of the progress circle view. */ public int getProgressCircleDiameter() { return circleDiameter; } /** * SwipeRefreshLayout ???? * RecyclerView.canScrollVertically(-1); //true??false? * * @return true ? ? */ public boolean canChildScrollUp() { if (childScrollUpCallback != null) { return childScrollUpCallback.canChildScrollUp(this, targetView); } if (android.os.Build.VERSION.SDK_INT < 14) { if (targetView instanceof AbsListView) { final AbsListView absListView = (AbsListView) targetView; return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); } else { return ViewCompat.canScrollVertically(targetView, -1) || targetView.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(targetView, -1); } } /** * SwipeRefreshLayout ?? ? * RecyclerView.canScrollVertically(1); //true??false? */ public boolean canChildScrollDown() { if (childScrollDownCallback != null) { return childScrollDownCallback.canChildScrollDown(this, targetView); } if (android.os.Build.VERSION.SDK_INT < 14) { if (targetView instanceof AbsListView) { final AbsListView absListView = (AbsListView) targetView; if (absListView.getChildCount() > 0) { int lastChildBottom = absListView.getChildAt(absListView.getChildCount() - 1).getBottom(); return absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1 && lastChildBottom <= absListView.getMeasuredHeight(); } else { return false; } } else { return ViewCompat.canScrollVertically(targetView, 1) || targetView.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(targetView, 1); } } /** * Set a callback to override {@link SwipeRefreshLayout#canChildScrollUp()} method. Non-null * callback will return the value provided by the callback and ignore all internal logic. * * @param callback Callback that should be called when canChildScrollUp() is called. */ public void setOnChildScrollUpCallback(@Nullable OnChildScrollUpCallback callback) { childScrollUpCallback = callback; } public void setOnChildScrollDownCallback(@Nullable OnChildScrollDownCallback callback) { childScrollDownCallback = callback; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || isRefreshing() || nestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: if (!isLoadingMore()) { setTargetOffsetTopAndBottom(originalOffsetTop - circleView.getTop(), true); } mActivePointerId = ev.getPointerId(0); isBeingDragged = false; pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } mInitialDownY = ev.getY(pointerIndex); 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; } pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { return false; } final float y = ev.getY(pointerIndex); startDragging(y); break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: isBeingDragged = false; mActivePointerId = INVALID_POINTER; break; } return isBeingDragged; } @Override public void requestDisallowInterceptTouchEvent(boolean b) { // if this is a List < L or another view that doesn't support nested // scrolling, ignore this request so that the vertical scroll event // isn't stolen if ((android.os.Build.VERSION.SDK_INT < 21 && targetView instanceof AbsListView) || (targetView != null && !ViewCompat.isNestedScrollingEnabled(targetView))) { // Nope. } else { super.requestDisallowInterceptTouchEvent(b); } } // NestedScrollingParent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return isEnabled() && !mReturningToStart && !isRefreshing() && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int axes) { // Reset the counter of how much leftover scroll needs to be consumed. nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); // Dispatch up to the nested parent startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); swipeDownUnconsumed = 0; swipeUpUnconsumed = 0; nestedScrollInProgress = true; } /** * ??? ? * ??? ? * * @param target * @param dx * @param dy * @param consumed */ @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { // If we are in the middle of consuming, a scroll, then we want to move the spinner back up // before allowing the list to scroll if (dy > 0 && swipeDownUnconsumed > 0) { if (dy > swipeDownUnconsumed) { consumed[1] = dy - (int) swipeDownUnconsumed; swipeDownUnconsumed = 0; } else { swipeDownUnconsumed -= dy; consumed[1] = dy; } if (!isLoadingMore()) { //LogUtil.e("onNestedPreScroll... "); moveHead(swipeDownUnconsumed); } } else if (dy < 0 && swipeUpUnconsumed > 0) { if (dy > swipeUpUnconsumed) { consumed[1] = -dy + (int) swipeUpUnconsumed; swipeUpUnconsumed = 0; } else { swipeUpUnconsumed += dy; consumed[1] = dy; } if (!isRefreshing()) { //TODO ? LogUtil.e("? "); isLoadingMoreCancel = true; onLoadingMoreNone(); moveFoot(swipeUpUnconsumed); } } // If a client layout is using a custom start position for the circle // view, they mean to hide it again before scrolling the child view // If we get back to swipeDownUnconsumed == 0 and there is more to go, hide // the circle so it isn't exposed if its blocking content is moved if (usingCustomStart && dy > 0 && swipeDownUnconsumed == 0 && Math.abs(dy - consumed[1]) > 0) { circleView.setVisibility(View.GONE); } // Now let our nested parent consume the leftovers final int[] parentConsumed = mParentScrollConsumed; if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { consumed[0] += parentConsumed[0]; consumed[1] += parentConsumed[1]; } } @Override public int getNestedScrollAxes() { return nestedScrollingParentHelper.getNestedScrollAxes(); } /** * ? ?? | * * @param target */ @Override public void onStopNestedScroll(View target) { nestedScrollingParentHelper.onStopNestedScroll(target); nestedScrollInProgress = false; //LogUtil.e("swipeDownUnconsumed = " + swipeDownUnconsumed + " swipeUpUnconsumed = " + swipeUpUnconsumed); /** * ? ? */ if (swipeDownUnconsumed > 0 && swipeUpUnconsumed <= 0) { //LogUtil.e("onStopNestedScroll ... "); LogUtil.e("currentTargetOffsetTop = " + currentTargetOffsetTop); finishHead(swipeDownUnconsumed); swipeDownUnconsumed = 0; } /** * ?? */ if (swipeUpUnconsumed > 0 && swipeDownUnconsumed <= 0) { //LogUtil.e(" currentTargetOffsetTop = " + currentTargetOffsetTop); finishFoot(swipeUpUnconsumed); swipeUpUnconsumed = 0; } // Dispatch up our nested parent stopNestedScroll(); } /** * ??? * ??? * * @param target * @param dxConsumed * @param dyConsumed * @param dxUnconsumed * @param dyUnconsumed */ @Override public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, final int dxUnconsumed, final int dyUnconsumed) { // Dispatch up to the nested parent first dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, parentOffsetInWindow); /** * * ??? ?? ?????? ???? */ /** * ? ? */ final int dy = dyUnconsumed + parentOffsetInWindow[1]; /** * ? ? */ if (dy < 0 && !canChildScrollUp() && !isLoadingMore()) { swipeDownUnconsumed += Math.abs(dy); //LogUtil.e("onNestedScroll ... "); moveHead(swipeDownUnconsumed); } else if (dy > 0 && !canChildScrollDown() && !isLoadingMore() && !isRefreshing()) { swipeUpUnconsumed += Math.abs(dy); isLoadingMoreCancel = false; LogUtil.e("swipeUpUnconsumed = " + swipeUpUnconsumed); moveFoot(swipeUpUnconsumed); } } // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { nestedScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return nestedScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return nestedScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { nestedScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return nestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return nestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return nestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } private boolean isAnimationRunning(Animation animation) { return animation != null && animation.hasStarted() && !animation.hasEnded(); } /** * ? * * @param overScrollTop */ private void moveFoot(float overScrollTop) { /** * overScrollTop?? */ if (overScrollTop > footOffsetMax * 0.9F) { onLoadingMoreCritical(); } else { onLoadingMoreNone(); } if (FootType.useHeadViewAtBottom != footType) { /* ?? CircleView */ return; } //LogUtil.e("moveFoot ... overScrollTop = " + overScrollTop); progress.showArrow(true); float originalDragPercent = overScrollTop / refreshMaxDragDistance; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overScrollTop) - refreshMaxDragDistance; float slingshotDist = usingCustomStart ? spinnerOffsetEnd - originalOffsetTop : spinnerOffsetEnd; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (slingshotDist) * tensionPercent * 2; int targetY = originalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); if (circleView.getVisibility() != View.VISIBLE) { circleView.setVisibility(View.VISIBLE); } if (!scale) { ViewCompat.setScaleX(circleView, 1f); ViewCompat.setScaleY(circleView, 1f); } if (scale) { setAnimationProgress(Math.min(1f, overScrollTop / refreshMaxDragDistance)); } if (overScrollTop < refreshMaxDragDistance) { if (progress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(alphaStartAnimation)) { startProgressAlphaStartAnimation(); } } else { if (progress.getAlpha() < MAX_ALPHA && !isAnimationRunning(alphaMaxAnimation)) { startProgressAlphaMaxAnimation(); } } float strokeStart = adjustedPercent * .8f; progress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); progress.setArrowScale(Math.min(1f, adjustedPercent)); float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; progress.setProgressRotation(rotation); int offset = targetY - originalOffsetTop; offset = (offset > 0) ? offset : 0; LogUtil.e("offset = " + offset + " targetY = " + targetY + " originalOffsetTop = " + originalOffsetTop); footOffsetMax = (footOffsetMax > offset) ? footOffsetMax : offset; circleViewLayoutAtBottom(offset); } /** * ? loading? offset * * @param offset */ private void circleViewLayoutAtBottom(int offset) { int circleWidth = circleView.getMeasuredWidth(); int circleHeight = circleView.getMeasuredHeight(); circleView.layout((width / 2 - circleWidth / 2), height - currentTargetOffsetTop - offset - circleHeight, (width / 2 + circleWidth / 2), height - currentTargetOffsetTop - offset); } private void finishFoot(float overScrollTop) { //currentTargetOffsetTop = -120 LogUtil.e("overScrollTop = " + overScrollTop + " footOffsetMax = " + footOffsetMax); /* * ?? * */ if (!isLoadingMoreCancel && (overScrollTop > footOffsetMax * 0.9F)) { setLoadingMore(true); //setTargetOffsetTopAndBottom(offset, false); } else { setLoadingMore(false); startScaleDownAnimation(refreshAnimatorListener); } } /** * ? CircleView * * @param overScrollTop */ private void moveHead(float overScrollTop) { //LogUtil.e("moveHead ... overScrollTop = " + overScrollTop); progress.showArrow(true); float originalDragPercent = overScrollTop / refreshMaxDragDistance; float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overScrollTop) - refreshMaxDragDistance; float slingshotDist = usingCustomStart ? spinnerOffsetEnd - originalOffsetTop : spinnerOffsetEnd; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (slingshotDist) * tensionPercent * 2; int targetY = originalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); // where 1.0f is a full circle if (circleView.getVisibility() != View.VISIBLE) { circleView.setVisibility(View.VISIBLE); } if (!scale) { ViewCompat.setScaleX(circleView, 1f); ViewCompat.setScaleY(circleView, 1f); } if (scale) { setAnimationProgress(Math.min(1f, overScrollTop / refreshMaxDragDistance)); } if (overScrollTop < refreshMaxDragDistance) { if (progress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(alphaStartAnimation)) { startProgressAlphaStartAnimation(); } } else { if (progress.getAlpha() < MAX_ALPHA && !isAnimationRunning(alphaMaxAnimation)) { startProgressAlphaMaxAnimation(); } } float strokeStart = adjustedPercent * .8f; progress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); progress.setArrowScale(Math.min(1f, adjustedPercent)); float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; progress.setProgressRotation(rotation); LogUtil.e("targetY = " + targetY); headOffsetMax = (headOffsetMax < targetY) ? targetY : headOffsetMax; setTargetOffsetTopAndBottom(targetY - currentTargetOffsetTop, true); } /** * CircleView??? * * @param overScrollTop */ private void finishHead(float overScrollTop) { //overScrollTop = 997.0 currentTargetOffsetTop = 264 refreshMaxDragDistance = 192.0 LogUtil.e("overScrollTop = " + overScrollTop + " currentTargetOffsetTop = " + currentTargetOffsetTop + " refreshMaxDragDistance = " + refreshMaxDragDistance + " getTop = " + circleView.getTop()); if (overScrollTop > refreshMaxDragDistance) { setRefreshing(true, true); } else { /** * ? */ refreshing = false; progress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!scale) { listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!scale) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) { } }; } animateOffsetToStartPosition(currentTargetOffsetTop, listener); progress.showArrow(false); } } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex = -1; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || isRefreshing() || nestedScrollInProgress) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); isBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = ev.getY(pointerIndex); startDragging(y); if (isBeingDragged) { final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE; if (overScrollTop > 0 && !isLoadingMore()) { //LogUtil.e("onTouchEvent ... "); moveHead(overScrollTop); } else { return false; } } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { pointerIndex = MotionEventCompat.getActionIndex(ev); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index."); return false; } mActivePointerId = ev.getPointerId(pointerIndex); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: { pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex < 0) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); return false; } if (isBeingDragged) { final float y = ev.getY(pointerIndex); final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE; isBeingDragged = false; LogUtil.e("currentTargetOffsetTop = " + currentTargetOffsetTop); finishHead(overScrollTop); } mActivePointerId = INVALID_POINTER; return false; } case MotionEvent.ACTION_CANCEL: return false; } return true; } private void startDragging(float y) { final float yDiff = y - mInitialDownY; if (yDiff > touchSlop && !isBeingDragged) { mInitialMotionY = mInitialDownY + touchSlop; isBeingDragged = true; progress.setAlpha(STARTING_PROGRESS_ALPHA); } } private void headAnimateOffsetToCorrectPosition(int from, AnimationListener listener) { headFrom = from; LogUtil.e("from = " + from); headAnimateToCorrectPosition.reset(); headAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); headAnimateToCorrectPosition.setInterpolator(decelerateInterpolator); if (listener != null) { circleView.setAnimationListener(listener); } circleView.clearAnimation(); circleView.startAnimation(headAnimateToCorrectPosition); } private void animateOffsetToStartPosition(int from, AnimationListener listener) { if (scale) { startScaleDownReturnToStartAnimation(from, listener); } else { headFrom = from; animateToStartPosition.reset(); animateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); animateToStartPosition.setInterpolator(decelerateInterpolator); if (listener != null) { circleView.setAnimationListener(listener); } circleView.clearAnimation(); circleView.startAnimation(animateToStartPosition); } } /** * ? CircleView */ private final Animation headAnimateToCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = 0; int endTarget = 0; if (!usingCustomStart) { endTarget = spinnerOffsetEnd - Math.abs(originalOffsetTop); } else { endTarget = spinnerOffsetEnd; } targetTop = (headFrom + (int) ((endTarget - headFrom) * interpolatedTime)); int offset = targetTop - circleView.getTop(); setTargetOffsetTopAndBottom(offset, false); progress.setArrowScale(1 - interpolatedTime); } }; protected void moveToStart(float interpolatedTime) { int targetTop = 0; targetTop = (headFrom + (int) ((originalOffsetTop - headFrom) * interpolatedTime)); int offset = targetTop - circleView.getTop(); //LogUtil.e("? ... moveToStart"); setTargetOffsetTopAndBottom(offset, false); } private final Animation animateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(interpolatedTime); } }; private void startScaleDownReturnToStartAnimation(int from, Animation.AnimationListener listener) { headFrom = from; if (isAlphaUsedForScale()) { startingScale = progress.getAlpha(); } else { startingScale = ViewCompat.getScaleX(circleView); } scaleDownToStartAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float targetScale = (startingScale + (-startingScale * interpolatedTime)); setAnimationProgress(targetScale); moveToStart(interpolatedTime); } }; scaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { circleView.setAnimationListener(listener); } circleView.clearAnimation(); circleView.startAnimation(scaleDownToStartAnimation); } /** * circleView ? * * @param offset * @param requiresUpdate */ protected void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { circleView.bringToFront(); ViewCompat.offsetTopAndBottom(circleView, offset); currentTargetOffsetTop = circleView.getTop(); //currentTargetOffsetTop = 1248 offset = 1056 //LogUtil.e("currentTargetOffsetTop = " + currentTargetOffsetTop + " offset = " + offset); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); } } /** * Classes that wish to override {@link SwipeRefreshLayout#canChildScrollUp()} method * behavior should implement this interface. */ public interface OnChildScrollUpCallback { /** * Callback that will be called when {@link SwipeRefreshLayout#canChildScrollUp()} method * is called to allow the implementer to override its behavior. * * @param parent SwipeRefreshLayout that this callback is overriding. * @param child The child view of SwipeRefreshLayout. * @return Whether it is possible for the child view of parent layout to scroll up. */ boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child); } public interface OnChildScrollDownCallback { /** * Callback that will be called when {@link SwipeRefreshLayout#canChildScrollUp()} method * is called to allow the implementer to override its behavior. * * @param parent SwipeRefreshLayout that this callback is overriding. * @param child The child view of SwipeRefreshLayout. * @return Whether it is possible for the child view of parent layout to scroll up. */ boolean canChildScrollDown(SwipeRefreshLayout parent, @Nullable View child); } /** * ? */ protected void callRefreshCallback() { if (onRefreshListener != null && isRefreshing()) { onRefreshListener.onRefresh(); } } /** * ? */ protected void respLoadMoreCallback() { if (onRefreshListener != null) { onRefreshListener.onLoadMore(); } } public void setLoadMoreEnabled(boolean enabled) { this.loadMoreEnabled = enabled; } public boolean getLoadMoreEnabled() { return loadMoreEnabled; } /** * */ public boolean isLoadingMore() { return isLoadingMore; } /** * */ public void setLoadingMore(boolean isLoadingMore) { this.isLoadingMore = isLoadingMore; if (isLoadingMore) { if (FootType.useHeadViewAtBottom == getFootType()) { circleViewLayoutAtBottom(footOffsetMax); progress.setAlpha(MAX_ALPHA); progress.start(); } respLoadMoreCallback(); onLoadingMoreStart(); } else { circleViewLayoutAtTop(); setVisibility4CircleViewAtBottom(false); onLoadingMoreFinish(); } if (FootType.useHeadViewAtTop == getFootType()) { setVisibility4CircleViewAtTop(isLoadingMore); } } /** * ? */ public void stopRefreshLayout() { setRefreshing(false); setLoadingMore(false); } public void setRefreshEnabled(boolean enabled) { setEnabled(enabled); } /** * UI ? */ public void autoRefresh() { if (!isRefreshing() && !isLoadingMore()) { setRefreshing(true); callRefreshCallback(); setVisibility4CircleViewAtTop(true); } } /** * ?? * * @param distance ?? dp 12dp */ public SwipeRefreshLayout setLoadMoreMinDistance(int distance) { this.loadMoreMinDistance = (int) dp2px(distance); return this; } /** * ? ?? * * @return */ public int getLoadMoreMinDistance() { return loadMoreMinDistance; } public SwipeRefreshLayout setOnFootViewChangerListener(OnFootViewChangerListener onFootViewChangerListener) { this.onFootViewChangerListener = onFootViewChangerListener; return this; } /** * ? */ public void onLoadingMoreNone() { if (footView != null) { footView.getFootView().setVisibility(View.VISIBLE); footView.onLoadingMoreNone(); } if (onFootViewChangerListener != null) { onFootViewChangerListener.onLoadingMoreNone(); } } /** * ? */ public void onLoadingMoreCritical() { if (footView != null) { footView.getFootView().setVisibility(View.VISIBLE); footView.onLoadingMoreCritical(); } if (onFootViewChangerListener != null) { onFootViewChangerListener.onLoadingMoreCritical(); } } /** * */ public void onLoadingMoreStart() { if (footView != null) { footView.getFootView().setVisibility(View.VISIBLE); footView.onLoadingMoreStart(); } if (onFootViewChangerListener != null) { onFootViewChangerListener.onLoadingMoreStart(); } } /** * ? */ public void onLoadingMoreFinish() { if (footView != null) { footView.getFootView().setVisibility(View.VISIBLE); footView.onLoadingMoreFinish(); } if (onFootViewChangerListener != null) { onFootViewChangerListener.onLoadingMoreFinish(); } } /** * footView * * @return */ public int getFootType() { return footType; } /** * ??: dp---->px */ public float dp2px(float dp) { return dp * getContext().getResources().getDisplayMetrics().density; } }