/* * Created by LuaView. * Copyright (c) 2017, Alibaba Group. All rights reserved. * * This source code is licensed under the MIT. * For the full copyright and license information,please view the LICENSE file in the root directory of this source tree. */ package com.taobao.luaview.view.widget; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.os.Handler; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowManager; 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 android.widget.RelativeLayout; /** * Created by tuoli on 12/5/16. */ public class SuperSwipeRefreshLayout extends ViewGroup { private static final int HEADER_VIEW_HEIGHT = 50;// HeaderView height (dp) private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; private static final int INVALID_POINTER = -1; private static final float DRAG_RATE = .5f; private static final int ANIMATE_TO_TRIGGER_DURATION = 200; private static final int ANIMATE_TO_START_DURATION = 200; private static final int DEFAULT_CIRCLE_TARGET = 64; // SuperSwipeRefreshLayout内的目标View,因为包装了LVViewGroup,所以((ViewGroup)mTarget).getChildAt(0)拿到的才是真正的目标view, 比如RecyclerView,ListView,ScrollView,GridView // etc. private View mTarget; private OnPullRefreshListener mListener;// 下拉刷新listener private boolean mRefreshing = false; private int mTouchSlop; private float mTotalDragDistance = -1; private int mMediumAnimationDuration; private int mCurrentTargetOffsetTop; private boolean mOriginalOffsetCalculated = false; private float mInitialMotionY; private boolean mIsBeingDragged; private int mActivePointerId = INVALID_POINTER; private boolean mReturningToStart; private final DecelerateInterpolator mDecelerateInterpolator; private static final int[] LAYOUT_ATTRS = new int[]{android.R.attr.enabled}; private HeadViewContainer mHeadViewContainer; protected int mFrom; protected int mOriginalOffsetTop; private Animation mScaleAnimation; // 最后停顿时的偏移量px,与DEFAULT_CIRCLE_TARGET正比 private float mSpinnerFinalOffset; private boolean mNotify; private int mHeaderViewWidth;// headerView的宽度 private int mHeaderViewHeight; private boolean targetScrollWithLayout = true; private int pushDistance = 0; private boolean usingDefaultHeader = true; private float density = 1.0f; /** * 下拉时,超过距离之后,弹回来的动画监听器 */ private AnimationListener mRefreshListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { if (mNotify) { if (mListener != null) { Log.i("tuolidd", "onRefresh"); mListener.onRefresh(); } } } mCurrentTargetOffsetTop = mHeadViewContainer.getTop(); updateListenerCallBack(); } }; /** * 更新回调 */ private void updateListenerCallBack() { int distance = mCurrentTargetOffsetTop + mHeadViewContainer.getHeight(); if (mListener != null) { mListener.onPullDistance(mNotify, distance); } } public SuperSwipeRefreshLayout(Context context) { this(context, null); } @SuppressWarnings("deprecation") public SuperSwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); /** * getScaledTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。如果小于这个距离就不触发移动控件 */ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMediumAnimationDuration = getResources().getInteger( android.R.integer.config_mediumAnimTime); setWillNotDraw(false); mDecelerateInterpolator = new DecelerateInterpolator( DECELERATE_INTERPOLATION_FACTOR); final TypedArray a = context .obtainStyledAttributes(attrs, LAYOUT_ATTRS); setEnabled(a.getBoolean(0, true)); a.recycle(); WindowManager wm = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); final DisplayMetrics metrics = getResources().getDisplayMetrics(); mHeaderViewWidth = (int) display.getWidth(); mHeaderViewHeight = (int) (HEADER_VIEW_HEIGHT * metrics.density); createHeaderViewContainer(); ViewCompat.setChildrenDrawingOrderEnabled(this, true); mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; density = metrics.density; mTotalDragDistance = mSpinnerFinalOffset; } public void setRefreshingOffset(float offset) { mSpinnerFinalOffset = offset * density * 2; } /** * 创建头布局的容器 */ private void createHeaderViewContainer() { mHeadViewContainer = new HeadViewContainer(getContext()); addView(mHeadViewContainer); } /** * 设置 * * @param listener */ public void setOnRefreshListener(OnPullRefreshListener listener) { mListener = listener; } /** * 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 && mRefreshing != refreshing) { // scale and show mRefreshing = refreshing; int endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, true /* requires update */); mNotify = false; startScaleUpAnimation(mRefreshListener); } else { setRefreshing(refreshing, false /* notify */); } } private void startScaleUpAnimation(AnimationListener listener) { mScaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(interpolatedTime); } }; mScaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { mHeadViewContainer.setAnimationListener(listener); } mHeadViewContainer.clearAnimation(); mHeadViewContainer.startAnimation(mScaleAnimation); } private void setAnimationProgress(float progress) { if (!usingDefaultHeader) { progress = 1; } ViewCompat.setScaleX(mHeadViewContainer, progress); ViewCompat.setScaleY(mHeadViewContainer, progress); } private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); } else { animateOffsetToStartPosition(mCurrentTargetOffsetTop, mRefreshListener); } } } /** * 确保mTarget不为空<br> * mTarget一般是可滑动的ScrollView,ListView,RecyclerView等 */ private void ensureTarget() { if (mTarget == null) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (!child.equals(mHeadViewContainer)) { mTarget = child; break; } } } } /** * Set the distance to trigger a sync in dips * * @param distance */ public void setDistanceToTriggerSync(int distance) { mTotalDragDistance = distance; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } int distance = mCurrentTargetOffsetTop + mHeadViewContainer.getMeasuredHeight(); if (!targetScrollWithLayout) { // 判断标志位,如果目标View不跟随手指的滑动而滑动,将下拉偏移量设置为0 distance = 0; } final View child = mTarget; final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop() + distance - pushDistance;// 根据偏移量distance更新 final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);// 更新目标View的位置 int headViewWidth = mHeadViewContainer.getMeasuredWidth(); int headViewHeight = mHeadViewContainer.getMeasuredHeight(); mHeadViewContainer.layout((width / 2 - headViewWidth / 2), mCurrentTargetOffsetTop, (width / 2 + headViewWidth / 2), mCurrentTargetOffsetTop + headViewHeight);// 更新头布局的位置 } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); mHeadViewContainer.measure(MeasureSpec.makeMeasureSpec( mHeaderViewWidth, MeasureSpec.EXACTLY), MeasureSpec .makeMeasureSpec(3 * mHeaderViewHeight, MeasureSpec.EXACTLY)); if (!mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; mCurrentTargetOffsetTop = mOriginalOffsetTop = -mHeadViewContainer .getMeasuredHeight(); updateListenerCallBack(); } } /** * 判断目标View是否滑动到顶部-还能否继续滑动 * * @return */ public boolean isChildScrollToTop() { if (Build.VERSION.SDK_INT < 14) { if (((ViewGroup)mTarget).getChildAt(0) instanceof AbsListView) { final AbsListView absListView = (AbsListView) ((ViewGroup)mTarget).getChildAt(0); return !(absListView.getChildCount() > 0 && (absListView .getFirstVisiblePosition() > 0 || absListView .getChildAt(0).getTop() < absListView.getPaddingTop())); } else { return !(((ViewGroup)mTarget).getChildAt(0).getScrollY() > 0); } } else { return !ViewCompat.canScrollVertically(((ViewGroup)mTarget).getChildAt(0), -1); } } /** * 主要判断是否应该拦截子View的事件<br> * 如果拦截,则交给自己的OnTouchEvent处理<br> * 否者,交给子View处理<br> */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || mRefreshing || !isChildScrollToTop()) { // 如果子View可以滑动,不拦截事件,交给子View处理-下拉刷新 // 或者子View没有滑动到底部不拦截事件-上拉加载更多 return false; } // 下拉刷新判断 switch (action) { case MotionEvent.ACTION_DOWN: setTargetOffsetTopAndBottom( mOriginalOffsetTop - mHeadViewContainer.getTop(), true);// 恢复HeaderView的初始位置 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; final float initialMotionY = getMotionEventY(ev, mActivePointerId); if (initialMotionY == -1) { return false; } mInitialMotionY = initialMotionY;// 记录按下的位置 case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { return false; } final float y = getMotionEventY(ev, mActivePointerId); if (y == -1) { return false; } float yDiff = y - mInitialMotionY;// 计算下拉距离 if (yDiff > mTouchSlop && !mIsBeingDragged) {// 判断是否下拉的距离足够 mIsBeingDragged = true;// 正在下拉 } 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;// 如果正在拖动,则拦截子View的事件 } private float getMotionEventY(MotionEvent ev, int activePointerId) { final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); if (index < 0) { return -1; } return MotionEventCompat.getY(ev, index); } @Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || !isChildScrollToTop()) { // 如果子View可以滑动,不拦截事件,交给子View处理 return false; } return handlerPullTouchEvent(ev, action); } private boolean handlerPullTouchEvent(MotionEvent ev, int action) { switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: { final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (pointerIndex < 0) { return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (mIsBeingDragged) { float originalDragPercent = overscrollTop / mTotalDragDistance; if (originalDragPercent < 0) { return false; } float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; float slingshotDist = mSpinnerFinalOffset; 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 = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); if (overscrollTop < mTotalDragDistance) { if (mListener != null) { mListener.onPullEnable(false); } } else { if (mListener != null) { mListener.onPullEnable(true); } } setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true); } break; } case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); mActivePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { if (mActivePointerId == INVALID_POINTER) { return false; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; mIsBeingDragged = false; if (overscrollTop > mTotalDragDistance) { setRefreshing(true, true /* notify */); } else { mRefreshing = false; AnimationListener listener = null; animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); } mActivePointerId = INVALID_POINTER; return false; } } return true; } private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { mFrom = from; mAnimateToCorrectPosition.reset(); mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mHeadViewContainer.setAnimationListener(listener); } mHeadViewContainer.clearAnimation(); mHeadViewContainer.startAnimation(mAnimateToCorrectPosition); } private void animateOffsetToStartPosition(int from, AnimationListener listener) { mFrom = from; mAnimateToStartPosition.reset(); mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mHeadViewContainer.setAnimationListener(listener); } mHeadViewContainer.clearAnimation(); mHeadViewContainer.startAnimation(mAnimateToStartPosition); resetTargetLayoutDelay(ANIMATE_TO_START_DURATION); } /** * 重置Target位置 * * @param delay */ public void resetTargetLayoutDelay(int delay) { new Handler().postDelayed(new Runnable() { @Override public void run() { resetTargetLayout(); } }, delay); } /** * 重置Target的位置 */ public void resetTargetLayout() { final int width = getMeasuredWidth(); final View child = mTarget; final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = child.getWidth() - getPaddingLeft() - getPaddingRight(); final int childHeight = child.getHeight() - getPaddingTop() - getPaddingBottom(); child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); int headViewWidth = mHeadViewContainer.getMeasuredWidth(); int headViewHeight = mHeadViewContainer.getMeasuredHeight(); mHeadViewContainer.layout((width / 2 - headViewWidth / 2), -headViewHeight, (width / 2 + headViewWidth / 2), 0);// 更新头布局的位置 } private final Animation mAnimateToCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { int endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); int targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); int offset = targetTop - mHeadViewContainer.getTop() - (int)mSpinnerFinalOffset/2; setTargetOffsetTopAndBottom(offset, false /* requires update */); } @Override public void setAnimationListener(AnimationListener listener) { super.setAnimationListener(listener); } }; private void moveToStart(float interpolatedTime) { int targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); int offset = targetTop - mHeadViewContainer.getTop(); setTargetOffsetTopAndBottom(offset, false /* requires update */); } private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { moveToStart(interpolatedTime); } }; private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { mHeadViewContainer.bringToFront(); mHeadViewContainer.offsetTopAndBottom(offset); mCurrentTargetOffsetTop = mHeadViewContainer.getTop(); if (requiresUpdate && Build.VERSION.SDK_INT < 11) { invalidate(); } updateListenerCallBack(); } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } } private class HeadViewContainer extends RelativeLayout { private AnimationListener mAnimationListener; public HeadViewContainer(Context context) { super(context); } public void setAnimationListener(AnimationListener listener) { mAnimationListener = listener; } @Override public void onAnimationStart() { super.onAnimationStart(); if (mListener != null && mAnimationListener != null) { mAnimationListener.onAnimationStart(getAnimation()); } } @Override public void onAnimationEnd() { super.onAnimationEnd(); if (mListener != null && mAnimationListener != null) { mAnimationListener.onAnimationEnd(getAnimation()); } } } public interface OnPullRefreshListener { void onRefresh(); void onPullDistance(boolean notify, int distance); void onPullEnable(boolean enable); } }