/** * Copyright 2015 bingoogolapple * * 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.cheng.animationstudy.customview.bagrefresh; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.View; import com.cheng.animationstudy.R; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.ValueAnimator; /** * 作者:王浩 邮件:bingoogolapple@gmail.com * 创建时间:15/5/21 22:34 * 描述:黏性下拉刷新控件 */ public class BGAStickinessRefreshView extends View { private static final String TAG = BGAStickinessRefreshView.class.getSimpleName(); private BGAStickinessRefreshViewHolder mStickinessRefreshViewHolder; private RectF mTopBound; private RectF mBottomBound; private Rect mRotateDrawableBound; private Point mCenterPoint; private Paint mPaint; private Path mPath; private Drawable mRotateDrawable; /** * 旋转图片的大小 */ private int mRotateDrawableSize; private int mMaxBottomHeight; private int mCurrentBottomHeight = 0; /** * 是否正在旋转 */ private boolean mIsRotating = false; private boolean mIsRefreshing = false; /** * 当前旋转角度 */ private int mCurrentDegree = 0; private int mEdge = 0; private int mTopSize = 0; private int mStickinessColor = 0xFF999999; public BGAStickinessRefreshView(Context context) { this(context, null); } public BGAStickinessRefreshView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BGAStickinessRefreshView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initBounds(); initPaint(); initRotateDrawable(); initSize(); } private void initBounds() { mTopBound = new RectF(); mBottomBound = new RectF(); mRotateDrawableBound = new Rect(); mCenterPoint = new Point(); } private void initPaint() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(mStickinessColor); mPath = new Path(); } public void setStickinessColor(int stickinessColor) { mStickinessColor = stickinessColor; if (mPaint == null) { initPaint(); } else { mPaint.setColor(mStickinessColor); } } private void initRotateDrawable() { mRotateDrawable = getContext().getResources().getDrawable(R.mipmap.bga_refresh_stickiness); } public void setRotateDrawable(Drawable rotateDrawable) { mRotateDrawable = rotateDrawable; } private void initSize() { mEdge = BGARefreshLayout.dp2px(getContext(), 5); mRotateDrawableSize = BGARefreshLayout.dp2px(getContext(), 30); mTopSize = mRotateDrawableSize + 2 * mEdge; mMaxBottomHeight = (int) (2.4f * mRotateDrawableSize); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = mTopSize + getPaddingLeft() + getPaddingRight(); int height = mTopSize + getPaddingTop() + getPaddingBottom() + mMaxBottomHeight; setMeasuredDimension(width, height); measureDraw(); } private void measureDraw() { mCenterPoint.x = getMeasuredWidth() / 2; mCenterPoint.y = getMeasuredHeight() / 2; mTopBound.left = mCenterPoint.x - mTopSize / 2; mTopBound.right = mTopBound.left + mTopSize; mTopBound.bottom = getMeasuredHeight() - getPaddingBottom() - mCurrentBottomHeight; mTopBound.top = mTopBound.bottom - mTopSize; float scale = 1.0f - mCurrentBottomHeight * 1.0f / mMaxBottomHeight; scale = Math.min(Math.max(scale, 0.2f), 1.0f); int mBottomSize = (int) (mTopSize * scale); mBottomBound.left = mCenterPoint.x - mBottomSize / 2; mBottomBound.right = mBottomBound.left + mBottomSize; mBottomBound.bottom = mTopBound.bottom + mCurrentBottomHeight; mBottomBound.top = mBottomBound.bottom - mBottomSize; } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); mPath.reset(); mTopBound.round(mRotateDrawableBound); mRotateDrawable.setBounds(mRotateDrawableBound); if (mIsRotating) { mPath.addOval(mTopBound, Path.Direction.CW); canvas.drawPath(mPath, mPaint); canvas.save(); canvas.rotate(mCurrentDegree, mRotateDrawable.getBounds().centerX(), mRotateDrawable.getBounds().centerY()); mRotateDrawable.draw(canvas); canvas.restore(); } else { // 移动到drawable左边缘的中间那个点 mPath.moveTo(mTopBound.left, mTopBound.top + mTopSize / 2); // 从drawable左边缘的中间那个点开始画半圆 mPath.arcTo(mTopBound, 180, 180); // 二阶贝塞尔曲线,第一个是控制点,第二个是终点 // mPath.quadTo(mTopBound.right - mTopSize / 8, mTopBound.bottom, mBottomBound.right, mBottomBound.bottom - mBottomBound.height() / 2); // mCurrentBottomHeight 0 到 mMaxBottomHeight // scale 0.2 到 1 float scale = Math.max(mCurrentBottomHeight * 1.0f / mMaxBottomHeight, 0.2f); float bottomControlXOffset = mTopSize * ((3 + (float) Math.pow(scale, 7) * 16) / 32); float bottomControlY = mTopBound.bottom / 2 + mCenterPoint.y / 2; // 三阶贝塞尔曲线,前两个是控制点,最后一个点是终点 mPath.cubicTo(mTopBound.right - mTopSize / 8, mTopBound.bottom, mTopBound.right - bottomControlXOffset, bottomControlY, mBottomBound.right, mBottomBound.bottom - mBottomBound.height() / 2); mPath.arcTo(mBottomBound, 0, 180); // mPath.quadTo(mTopBound.left + mTopSize / 8, mTopBound.bottom, mTopBound.left, mTopBound.bottom - mTopSize / 2); mPath.cubicTo(mTopBound.left + bottomControlXOffset, bottomControlY, mTopBound.left + mTopSize / 8, mTopBound.bottom, mTopBound.left, mTopBound.bottom - mTopSize / 2); canvas.drawPath(mPath, mPaint); mRotateDrawable.draw(canvas); } } public void setMoveYDistance(int moveYDistance) { int bottomHeight = moveYDistance - mTopSize - getPaddingBottom() - getPaddingTop(); if (bottomHeight > 0) { mCurrentBottomHeight = bottomHeight; } else { mCurrentBottomHeight = 0; } postInvalidate(); } /** * 是否能切换到正在刷新状态 * * @return */ public boolean canChangeToRefreshing() { return mCurrentBottomHeight >= mMaxBottomHeight * 0.98f; } public void startRefreshing() { ValueAnimator animator = ValueAnimator.ofInt(mCurrentBottomHeight, 0); animator.setDuration(mStickinessRefreshViewHolder.getTopAnimDuration()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mCurrentBottomHeight = (int) animation.getAnimatedValue(); postInvalidate(); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { mIsRefreshing = true; if (mCurrentBottomHeight != 0) { mStickinessRefreshViewHolder.startChangeWholeHeaderViewPaddingTop(mCurrentBottomHeight); } else { mStickinessRefreshViewHolder.startChangeWholeHeaderViewPaddingTop(-(mTopSize + getPaddingTop() + getPaddingBottom())); } } @Override public void onAnimationEnd(Animator animation) { mIsRotating = true; startRotating(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); } private void startRotating() { ViewCompat.postOnAnimation(this, new Runnable() { @Override public void run() { mCurrentDegree += 10; if (mCurrentDegree > 360) { mCurrentDegree = 0; } if (mIsRefreshing) { startRotating(); } postInvalidate(); } }); } public void stopRefresh() { mIsRotating = true; mIsRefreshing = false; postInvalidate(); } public void smoothToIdle() { ValueAnimator animator = ValueAnimator.ofInt(mCurrentBottomHeight, 0); animator.setDuration(mStickinessRefreshViewHolder.getTopAnimDuration()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mCurrentBottomHeight = (int) animation.getAnimatedValue(); postInvalidate(); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { mIsRotating = false; } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); } public void setStickinessRefreshViewHolder(BGAStickinessRefreshViewHolder stickinessRefreshViewHolder) { mStickinessRefreshViewHolder = stickinessRefreshViewHolder; } }