@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)); mCircleView.measure( MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); if (!mUsingCustomStart && !mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); } mCircleViewIndex = -1; // Get the index of the circleview. for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == mCircleView) { mCircleViewIndex = index; break; } } }
@Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } } else { mProgress.stop(); mCircleView.setVisibility(View.GONE); setColorViewAlpha(MAX_ALPHA); // Return the circle to its start position if (mScale) { setAnimationProgress(0 /* animation complete and view is hidden */); } else { setTargetOffsetTopAndBottom( mOriginalOffsetTop - mCurrentTargetOffsetTop, true /* requires update */); } } mCurrentTargetOffsetTop = mCircleView.getTop(); }
@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; } 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); int circleWidth = mCircleView.getMeasuredWidth(); int circleHeight = mCircleView.getMeasuredHeight(); mCircleView.layout( (width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); }
private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { mCircleView.bringToFront(); mCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTop = mCircleView.getTop(); if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { invalidate(); } }
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); }
/** * 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. * * @param scale Set to true if there is no view at a higher z-order than where the progress * spinner is set to appear. * @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) { mScale = scale; mCircleView.setVisibility(View.GONE); mOriginalOffsetTop = mCurrentTargetOffsetTop = start; mSpinnerFinalOffset = end; mUsingCustomStart = true; mCircleView.invalidate(); }
private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { mFrom = from; mAnimateToCorrectPosition.reset(); mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mAnimateToCorrectPosition); }
private void startScaleDownAnimation(Animation.AnimationListener listener) { mScaleDownAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(1 - interpolatedTime); } }; mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); mCircleView.setAnimationListener(listener); mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleDownAnimation); }
@Override public boolean onSingleTapUp(MotionEvent e) { mTappedViewsPostition = pointToPosition(e.getX(), e.getY()); if (mTappedViewsPostition >= 0) { mTappedView = getChildAt(mTappedViewsPostition); mTappedView.setPressed(true); } else { float centerX = circleWidth / 2; float centerY = circleHeight / 2; if (e.getX() < centerX + (childWidth / 2) && e.getX() > centerX - childWidth / 2 && e.getY() < centerY + (childHeight / 2) && e.getY() > centerY - (childHeight / 2)) { if (mOnCenterClickListener != null) { mOnCenterClickListener.onCenterClick(); return true; } } } if (mTappedView != null) { CircleImageView view = (CircleImageView) (mTappedView); if (selected != mTappedViewsPostition) { rotateViewToCenter(view, false); if (!rotateToCenter) { if (mOnItemSelectedListener != null) { mOnItemSelectedListener.onItemSelected( mTappedView, mTappedViewsPostition, mTappedView.getId(), view.getName()); } if (mOnItemClickListener != null) { mOnItemClickListener.onItemClick( mTappedView, mTappedViewsPostition, mTappedView.getId(), view.getName()); } } } else { rotateViewToCenter(view, false); if (mOnItemClickListener != null) { mOnItemClickListener.onItemClick( mTappedView, mTappedViewsPostition, mTappedView.getId(), view.getName()); } } return true; } return super.onSingleTapUp(e); }
private void animateOffsetToStartPosition(int from, AnimationListener listener) { if (mScale) { // Scale the item back down startScaleDownReturnToStartAnimation(from, listener); } else { mFrom = from; mAnimateToStartPosition.reset(); mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mAnimateToStartPosition); } }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int layoutWidth = r - l; int layoutHeight = b - t; // Laying out the child views final int childCount = getChildCount(); int left, top; radius = (layoutWidth <= layoutHeight) ? layoutWidth / 3 : layoutHeight / 3; childWidth = (int) (radius / 1.5); childHeight = (int) (radius / 1.5); float angleDelay = 360 / getChildCount(); for (int i = 0; i < childCount; i++) { final CircleImageView child = (CircleImageView) getChildAt(i); if (child.getVisibility() == GONE) { continue; } if (angle > 360) { angle -= 360; } else { if (angle < 0) { angle += 360; } } child.setAngle(angle); child.setPosition(i); left = Math.round( (float) (((layoutWidth / 2) - childWidth / 2) + radius * Math.cos(Math.toRadians(angle)))); top = Math.round( (float) (((layoutHeight / 2) - childHeight / 2) + radius * Math.sin(Math.toRadians(angle)))); child.layout(left, top, left + childWidth, top + childHeight); angle += angleDelay; } }
/** 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) { mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); } else { mCircleHeight = mCircleWidth = (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 mCircleView.setImageDrawable(null); mProgress.updateSizes(size); mCircleView.setImageDrawable(mProgress); }
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { // Fail fast if we're not in a state where a swipe is possible 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; }
/** * Rotate the buttons. * * @param degrees The degrees, the menu items should get rotated. */ private void rotateButtons(float degrees) { int left, top, childCount = getChildCount(); float angleDelay = 360 / childCount; angle += degrees; if (angle > 360) { angle -= 360; } else { if (angle < 0) { angle += 360; } } for (int i = 0; i < childCount; i++) { if (angle > 360) { angle -= 360; } else { if (angle < 0) { angle += 360; } } final CircleImageView child = (CircleImageView) getChildAt(i); if (child.getVisibility() == GONE) { continue; } left = Math.round( (float) (((circleWidth / 2) - childWidth / 2) + radius * Math.cos(Math.toRadians(angle)))); top = Math.round( (float) (((circleHeight / 2) - childHeight / 2) + radius * Math.sin(Math.toRadians(angle)))); child.setAngle(angle); if (Math.abs(angle - firstChildPos) < (angleDelay / 2) && selected != child.getPosition()) { selected = child.getPosition(); if (mOnItemSelectedListener != null && rotateToCenter) { mOnItemSelectedListener.onItemSelected(child, selected, child.getId(), child.getName()); } } child.layout(left, top, left + childWidth, top + childHeight); angle += angleDelay; } }
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 (mScale && isAlphaUsedForScale()) { return null; } Animation alpha = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { mProgress.setAlpha( (int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime))); } }; alpha.setDuration(ALPHA_ANIMATION_DURATION); // Clear out the previous animation listeners. mCircleView.setAnimationListener(null); mCircleView.clearAnimation(); mCircleView.startAnimation(alpha); return alpha; }
private void startScaleUpAnimation(AnimationListener listener) { mCircleView.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. mProgress.setAlpha(MAX_ALPHA); } mScaleAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { setAnimationProgress(interpolatedTime); } }; mScaleAnimation.setDuration(mMediumAnimationDuration); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleAnimation); }
@Override public void applyTransformation(float interpolatedTime, Transformation t) { int targetTop = 0; int endTarget = 0; if (!mUsingCustomStart) { endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); } else { endTarget = (int) mSpinnerFinalOffset; } targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); int offset = targetTop - mCircleView.getTop(); setTargetOffsetTopAndBottom(offset, false /* requires update */); mProgress.setArrowScale(1 - interpolatedTime); }
private void startScaleDownReturnToStartAnimation( int from, Animation.AnimationListener listener) { mFrom = from; if (isAlphaUsedForScale()) { mStartingScale = mProgress.getAlpha(); } else { mStartingScale = ViewCompat.getScaleX(mCircleView); } mScaleDownToStartAnimation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); setAnimationProgress(targetScale); moveToStart(interpolatedTime); } }; mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); if (listener != null) { mCircleView.setAnimationListener(listener); } mCircleView.clearAnimation(); mCircleView.startAnimation(mScaleDownToStartAnimation); }
/** * Rotates the given view to the center of the menu. * * @param view the view to be rotated to the center * @param fromRunnable if the method is called from the runnable which animates the rotation then * it should be true, otherwise false */ private void rotateViewToCenter(CircleImageView view, boolean fromRunnable) { if (rotateToCenter) { float velocityTemp = 1; float destAngle = (float) (firstChildPos - view.getAngle()); float startAngle = 0; int reverser = 1; if (destAngle < 0) { destAngle += 360; } if (destAngle > 180) { reverser = -1; destAngle = 360 - destAngle; } while (startAngle < destAngle) { startAngle += velocityTemp / 75; velocityTemp *= 1.0666F; } CircleLayout.this.post(new FlingRunnable(reverser * velocityTemp, !fromRunnable)); } }
/** * Get the diameter of the progress circle that is displayed as part of the swipe to refresh * layout. This is not valid until a measure pass has completed. * * @return Diameter in pixels of the progress circle view. */ public int getProgressCircleDiameter() { return mCircleView != null ? mCircleView.getMeasuredHeight() : 0; }
/** * 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. * @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) { mSpinnerFinalOffset = end; mScale = scale; mCircleView.invalidate(); }
/** * Set the background color of the progress spinner disc. * * @param color */ public void setProgressBackgroundColorSchemeColor(int color) { mCircleView.setBackgroundColor(color); mProgress.setBackgroundColor(color); }
private void setColorViewAlpha(int targetAlpha) { mCircleView.getBackground().setAlpha(targetAlpha); mProgress.setAlpha(targetAlpha); }
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } if (!isEnabled() || mReturningToStart || canChildScrollUp()) { // Fail fast if we're not in a state where a swipe is possible return false; } 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) { Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; if (mIsBeingDragged) { mProgress.showArrow(true); float originalDragPercent = overscrollTop / mTotalDragDistance; if (originalDragPercent < 0) { return false; } float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop : 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); // where 1.0f is a full circle if (mCircleView.getVisibility() != View.VISIBLE) { mCircleView.setVisibility(View.VISIBLE); } if (!mScale) { ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } if (overscrollTop < mTotalDragDistance) { if (mScale) { setAnimationProgress(overscrollTop / mTotalDragDistance); } if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } float strokeStart = adjustedPercent * .8f; mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mProgress.setArrowScale(Math.min(1f, adjustedPercent)); } else { if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } } float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mProgress.setProgressRotation(rotation); setTargetOffsetTopAndBottom( targetY - mCurrentTargetOffsetTop, true /* requires update */); } 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) { if (action == MotionEvent.ACTION_UP) { Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); } 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 { // cancel refresh mRefreshing = false; mProgress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!mScale) { listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {} @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } @Override public void onAnimationRepeat(Animation animation) {} }; } animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); mProgress.showArrow(false); } mActivePointerId = INVALID_POINTER; return false; } } return true; }
private void moveToStart(float interpolatedTime) { int targetTop = 0; targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); int offset = targetTop - mCircleView.getTop(); setTargetOffsetTopAndBottom(offset, false /* requires update */); }