Java tutorial
/* * Copyright (C) 2015 Kuloud * * 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 com.kuloud.android.unity.ui.widget.steprefresh; import android.content.Context; import android.support.annotation.NonNull; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; 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.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.Transformation; import android.widget.ImageView; import com.kuloud.android.unity.util.UIUtils; /** * Created by Kuloud on 3/13/15. */ public abstract class PullToRefreshView extends ViewGroup { public abstract BaseRefreshDrawable onRefreshLayoutAttach(); private static final int DRAG_MAX_DISTANCE = 80; private static final float DRAG_RATE = .5f; private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; public static final int MAX_OFFSET_ANIMATION_DURATION = 700; public static final int MAX_OFFSET_END_ANIMATION_DURATION = 400; public static final int MAX_JET_RESTORE_ANIMATION_DURATION = 2350; private static final int INVALID_POINTER = -1; private View mTarget; private ImageView mRefreshView; private Interpolator mDecelerateInterpolator; private int mTouchSlop; private int mTotalDragDistance; private BaseRefreshDrawable mBaseRefreshView; private float mCurrentDragPercent; private int mCurrentOffsetTop; private boolean mRefreshing; private int mActivePointerId; private boolean mIsBeingDragged; private float mInitialMotionY; private int mFrom; private float mFromDragPercent; private boolean mNotify; private OnRefreshListener mListener; public PullToRefreshView(Context context) { this(context, null); } public PullToRefreshView(Context context, AttributeSet attrs) { super(context, attrs); mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mTotalDragDistance = UIUtils.dpToPxInt(context, DRAG_MAX_DISTANCE); mRefreshView = new ImageView(context); setRefreshStyle(); addView(mRefreshView); setWillNotDraw(false); ViewCompat.setChildrenDrawingOrderEnabled(this, true); } private void setRefreshStyle() { setRefreshing(false); mBaseRefreshView = onRefreshLayoutAttach(); mRefreshView.setImageDrawable(mBaseRefreshView); } public int getTotalDragDistance() { return mTotalDragDistance; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); ensureTarget(); if (mTarget == null) return; widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingRight() - getPaddingLeft(), MeasureSpec.EXACTLY); heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); mTarget.measure(widthMeasureSpec, heightMeasureSpec); mRefreshView.measure(widthMeasureSpec, heightMeasureSpec); } private void ensureTarget() { if (mTarget != null) return; if (getChildCount() > 0) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child != mRefreshView) mTarget = child; } } } @Override public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) { if (!isEnabled() || canChildScrollUp() || mRefreshing) { return false; } final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_DOWN: setTargetOffsetTop(0); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; final float initialMotionY = getMotionEventY(ev, mActivePointerId); if (initialMotionY == -1) { return false; } mInitialMotionY = initialMotionY; break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { return false; } final float y = getMotionEventY(ev, mActivePointerId); if (y == -1) { return false; } final float yDiff = y - mInitialMotionY; if (yDiff > mTouchSlop && !mIsBeingDragged) { mIsBeingDragged = true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } return mIsBeingDragged; } @Override public boolean onTouchEvent(@NonNull MotionEvent ev) { // if (!mIsBeingDragged) { // return super.onTouchEvent(ev); // } final int action = MotionEventCompat.getActionMasked(ev); switch (action) { 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 yDiff = y - mInitialMotionY; final float scrollTop = yDiff * DRAG_RATE; mCurrentDragPercent = scrollTop / mTotalDragDistance; if (mCurrentDragPercent < 0) { return false; } float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent)); float extraOS = Math.abs(scrollTop) - mTotalDragDistance; float slingshotDist = mTotalDragDistance; 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 = (int) ((slingshotDist * boundedDragPercent) + extraMove); mBaseRefreshView.setPercent(mCurrentDragPercent, true); setTargetOffsetTop(targetY - mCurrentOffsetTop); 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); } else { mRefreshing = false; animateOffsetToPosition(mAnimateToStartPosition, false); } mActivePointerId = INVALID_POINTER; return false; } } return true; } private void animateOffsetToPosition(Animation animation, boolean isEndAnimation) { mFrom = mCurrentOffsetTop; mFromDragPercent = mCurrentDragPercent; long animationDuration = Math.abs((long) (!isEndAnimation ? MAX_OFFSET_ANIMATION_DURATION : MAX_OFFSET_END_ANIMATION_DURATION * mFromDragPercent)); animation.reset(); animation.setDuration(animationDuration); animation.setInterpolator(mDecelerateInterpolator); animation.setAnimationListener(mToStartListener); mRefreshView.clearAnimation(); mRefreshView.startAnimation(animation); } private void animateOffsetToCorrectPosition() { mFrom = mCurrentOffsetTop; mFromDragPercent = mCurrentDragPercent; mAnimateToCorrectPosition.reset(); mAnimateToCorrectPosition.setDuration(getDurationForRestoreAnimation()); mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); mRefreshView.clearAnimation(); mRefreshView.startAnimation(mAnimateToCorrectPosition); if (mRefreshing) { mBaseRefreshView.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } } else { mBaseRefreshView.stop(); animateOffsetToPosition(mAnimateToStartPosition, false); } mCurrentOffsetTop = mTarget.getTop(); } /** * Depends on different styles of refresh we should return restore animation time * * @return - animation duration */ private int getDurationForRestoreAnimation() { return MAX_JET_RESTORE_ANIMATION_DURATION; } private final Animation mAnimateToStartPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, @NonNull Transformation t) { moveToStart(interpolatedTime); } }; private Animation mAnimateToEndPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, @NonNull Transformation t) { moveToEnd(interpolatedTime); } }; private final Animation mAnimateToCorrectPosition = new Animation() { @Override public void applyTransformation(float interpolatedTime, @NonNull Transformation t) { int targetTop; int endTarget = mTotalDragDistance; targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); int offset = targetTop - mTarget.getTop(); mCurrentDragPercent = mFromDragPercent - (mFromDragPercent - 1.0f) * interpolatedTime; mBaseRefreshView.setPercent(mCurrentDragPercent, false); setTargetOffsetTop(offset); } }; private void moveToStart(float interpolatedTime) { int targetTop = mFrom - (int) (mFrom * interpolatedTime); float targetPercent = mFromDragPercent * (1.0f - interpolatedTime); int offset = targetTop - mTarget.getTop(); mCurrentDragPercent = targetPercent; mBaseRefreshView.setPercent(mCurrentDragPercent, true); setTargetOffsetTop(offset); } private void moveToEnd(float interpolatedTime) { int targetTop = mFrom - (int) (mFrom * interpolatedTime); float targetPercent = mFromDragPercent * (1.0f + interpolatedTime); int offset = targetTop - mTarget.getTop(); mCurrentDragPercent = targetPercent; mBaseRefreshView.setPercent(mCurrentDragPercent, true); setTargetOffsetTop(offset); } public void setRefreshing(boolean refreshing) { if (mRefreshing != refreshing) { setRefreshing(refreshing, false /* notify */); } } private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { mBaseRefreshView.setPercent(1.0f, true); animateOffsetToCorrectPosition(); } else { mBaseRefreshView.setEndOfRefreshing(true); animateOffsetToPosition(mAnimateToEndPosition, true); } } } private Animation.AnimationListener mToStartListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mBaseRefreshView.stop(); mCurrentOffsetTop = mTarget.getTop(); } }; 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 float getMotionEventY(MotionEvent ev, int activePointerId) { final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); if (index < 0) { return -1; } return MotionEventCompat.getY(ev, index); } private void setTargetOffsetTop(int offset) { mTarget.offsetTopAndBottom(offset); mBaseRefreshView.offsetTopAndBottom(offset); mCurrentOffsetTop = mTarget.getTop(); } private boolean canChildScrollUp() { return ViewCompat.canScrollVertically(mTarget, -1); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { ensureTarget(); if (mTarget == null) return; int height = getMeasuredHeight(); int width = getMeasuredWidth(); int left = getPaddingLeft(); int top = getPaddingTop(); int right = getPaddingRight(); int bottom = getPaddingBottom(); mTarget.layout(left, top + mCurrentOffsetTop, left + width - right, top + height - bottom + mCurrentOffsetTop); mRefreshView.layout(left, top, left + width - right, top + height - bottom); } public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } public interface OnRefreshListener { void onRefresh(); } }