package com.scwang.smart.refresh.header; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.graphics.ColorUtils; import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateInterpolator; import com.scwang.smart.drawable.PathsDrawable; import com.scwang.smart.refresh.layout.api.RefreshKernel; import com.scwang.smart.refresh.layout.api.RefreshLayout; import com.scwang.smart.refresh.layout.constant.RefreshState; import com.scwang.smart.refresh.layout.simple.SimpleComponent; import com.scwang.smart.refresh.layout.api.RefreshHeader; import com.scwang.smart.refresh.layout.constant.SpinnerStyle; import com.scwang.smart.refresh.layout.util.SmartUtil; import com.scwang.smartrefresh.header.R; /** * DropBoxRefresh * Created by scwang on 2017/6/24. * design https://dribbble.com/shots/3470499-DropBox-Refresh */ public class DropBoxHeader extends SimpleComponent implements RefreshHeader { // protected static String[] drawable1Paths = new String[]{ "M3 2h18v20h-18z", "m4,1c-1.105,0 -2,0.895 -2,2v3,11 3,1c0,1.105 0.895,2 2,2h2,12 2c1.105,0 2,-0.895 2,-2v-1,-3 -11,-3c0,-1.105 -0.895,-2 -2,-2h-2,-12 -2zM3.5,3h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM19.5,3h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM3.5,6h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM19.5,6h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM3.5,9h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM19.5,9h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM3.5,12h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM19.5,12h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM3.5,15h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM19.5,15h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM3.5,18h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5zM19.5,18h1c0.276,0 0.5,0.224 0.5,0.5v1c0,0.276 -0.224,0.5 -0.5,0.5h-1c-0.276,0 -0.5,-0.224 -0.5,-0.5v-1c0,-0.276 0.224,-0.5 0.5,-0.5z" }; protected static int[] drawable1Colors = new int[]{ 0xffecf0f1, 0xfffc4108 }; protected static String[] drawable2Paths = new String[]{ "M49,16.5l-14,-14l-27,0l0,53l41,0z", "m16,23.5h25c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1L16,21.5c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1z", "m16,15.5h10c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1L16,13.5c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1z", "M41,29.5L16,29.5c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h25c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z", "M41,37.5L16,37.5c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h25c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z", "M41,45.5L16,45.5c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h25c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z", "M49,16.5l-14,-14l0,14z" }; protected static int[] drawable2Colors = new int[]{ 0xfffed469, 0xffd5ae57 }; protected static String[] drawable3Paths = new String[]{ "M6.021,2.188L6.021,11.362C5.46,11.327 4.843,11.414 4.229,11.663C2.624,12.312 1.696,13.729 2.155,14.825C2.62,15.924 4.294,16.284 5.898,15.634C7.131,15.134 7.856,14.184 7.965,13.272L7.958,4.387L15.02,3.028L15.02,9.406C14.422,9.343 13.746,9.432 13.076,9.703C11.471,10.353 10.544,11.77 11.004,12.866C11.467,13.964 13.141,14.325 14.746,13.675C15.979,13.174 16.836,12.224 16.947,11.313L16.958,0.002L6.021,2.188L6.021,2.188Z" }; protected static int[] drawable3Colors = new int[]{ 0xff98d761 }; // // protected Path mPath; protected Paint mPaint; protected BoxBody mBoxBody; protected int mHeight; protected int mAccentColor; protected int mHeaderHeight; protected int mBackgroundColor; protected boolean mDropOutOverFlow; protected Drawable mDrawable1; protected Drawable mDrawable2; protected Drawable mDrawable3; protected float mDropOutPercent; protected float mReboundPercent; protected ValueAnimator mReboundAnimator; protected ValueAnimator mDropOutAnimator; protected RefreshState mState; protected RefreshKernel mKernel; // // public DropBoxHeader(Context context) { this(context, null); } public DropBoxHeader(Context context, @Nullable AttributeSet attrs) { super(context, attrs, 0); mPath = new Path(); mPaint = new Paint(); mBoxBody = new BoxBody(); mPaint.setAntiAlias(true); mAccentColor = 0xff6ea9ff; mBackgroundColor = 0xff283645; final View thisView = this; thisView.setMinimumHeight(SmartUtil.dp2px(150)); mSpinnerStyle = SpinnerStyle.FixedBehind; TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DropBoxHeader); if (ta.hasValue(R.styleable.DropBoxHeader_dhDrawable1)) { mDrawable1 = ta.getDrawable(R.styleable.DropBoxHeader_dhDrawable1); } else if (ta.hasValue(R.styleable.DropBoxHeader_srlDrawable1)) { mDrawable1 = ta.getDrawable(R.styleable.DropBoxHeader_srlDrawable1); } else { PathsDrawable drawable1 = new PathsDrawable(); drawable1.parserColors(drawable1Colors); if (!drawable1.parserPaths(drawable1Paths)) { drawable1.declareOriginal(2, 1, 20, 22); } // drawable1.printOriginal("drawable1"); mDrawable1 = drawable1; } if (ta.hasValue(R.styleable.DropBoxHeader_dhDrawable2)) { mDrawable2 = ta.getDrawable(R.styleable.DropBoxHeader_dhDrawable2); } else if (ta.hasValue(R.styleable.DropBoxHeader_srlDrawable2)) { mDrawable2 = ta.getDrawable(R.styleable.DropBoxHeader_srlDrawable2); } else { PathsDrawable drawable2 = new PathsDrawable(); drawable2.parserColors(drawable2Colors); if (!drawable2.parserPaths(drawable2Paths)) { drawable2.declareOriginal(8, 3, 41, 53); } // drawable2.printOriginal("drawable2"); mDrawable2 = drawable2; } if (ta.hasValue(R.styleable.DropBoxHeader_dhDrawable3)) { mDrawable3 = ta.getDrawable(R.styleable.DropBoxHeader_dhDrawable3); } else if (ta.hasValue(R.styleable.DropBoxHeader_srlDrawable3)) { mDrawable3 = ta.getDrawable(R.styleable.DropBoxHeader_srlDrawable3); } else { PathsDrawable drawable3 = new PathsDrawable(); drawable3.parserColors(drawable3Colors); if (!drawable3.parserPaths(drawable3Paths)) { drawable3.declareOriginal(2, 0, 15, 16); } // drawable3.printOriginal("drawable3"); mDrawable3 = drawable3; } ta.recycle(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); AccelerateInterpolator interpolator = new AccelerateInterpolator(); mReboundAnimator = ValueAnimator.ofFloat(0, 1, 0); mReboundAnimator.setInterpolator(interpolator); mReboundAnimator.setDuration(300); mReboundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { final View thisView = DropBoxHeader.this; mReboundPercent = (float) animation.getAnimatedValue(); thisView.invalidate(); } }); mReboundAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mState == RefreshState.Refreshing) { if (mDropOutAnimator != null) { mDropOutAnimator.start(); } } else { mDropOutPercent = 0; } } }); mDropOutAnimator = ValueAnimator.ofFloat(0, 1); mDropOutAnimator.setInterpolator(interpolator); mDropOutAnimator.setDuration(300); mDropOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (mDropOutPercent < 1 || mDropOutPercent >= 3) { mDropOutPercent = (float) animation.getAnimatedValue(); } else if (mDropOutPercent < 2) { mDropOutPercent = 1 + (float) animation.getAnimatedValue(); } else if (mDropOutPercent < 3) { mDropOutPercent = 2 + (float) animation.getAnimatedValue(); if (mDropOutPercent == 3) { mDropOutOverFlow = true; } } final View thisView = DropBoxHeader.this; thisView.invalidate(); } }); mDropOutAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mReboundAnimator != null) { mReboundAnimator.start(); } } }); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mReboundAnimator != null) { mReboundAnimator.removeAllUpdateListeners(); mReboundAnimator.removeAllListeners(); mReboundAnimator = null; } if (mDropOutAnimator != null) { mDropOutAnimator.removeAllUpdateListeners(); mDropOutAnimator.removeAllListeners(); mDropOutAnimator = null; } } @Override protected void dispatchDraw(Canvas canvas) { final View thisView = this; final int width = thisView.getWidth(); final int height = mHeight;//thisView.getHeight(); final int sideLength = generateSideLength(); //noinspection EqualsBetweenInconvertibleTypes final boolean footer = mKernel != null && (this.equals(mKernel.getRefreshLayout().getRefreshFooter())); if (footer) { canvas.save(); canvas.translate(0, thisView.getHeight() - mHeight); } BoxBody body = generateBoxBody(width, height, sideLength); mPaint.setColor(ColorUtils.setAlphaComponent(mAccentColor, 150)); canvas.drawPath(generateBoxBodyPath(body), mPaint); mPaint.setColor(mAccentColor); canvas.drawPath(generateBoxCoverPath(body), mPaint); if (thisView.isInEditMode()) { mDropOutPercent = 2.5f; } if (mDropOutPercent > 0) { canvas.clipPath(generateClipPath(body, width)); final float percent1 = Math.min(mDropOutPercent, 1); Rect bounds1 = mDrawable1.getBounds(); bounds1.offsetTo(width / 2 - bounds1.width() / 2, (int)((body.boxCenterY - bounds1.height() / 2 + bounds1.height()) * percent1) - bounds1.height()); mDrawable1.draw(canvas); final float percent2 = Math.min(Math.max(mDropOutPercent - 1, 0), 1); Rect bounds2 = mDrawable2.getBounds(); bounds2.offsetTo(width / 2 - bounds2.width() / 2, (int)((body.boxCenterY - bounds2.height() / 2 + bounds2.height()) * percent2) - bounds2.height()); mDrawable2.draw(canvas); final float percent3 = Math.min(Math.max(mDropOutPercent - 2, 0), 1); Rect bounds3 = mDrawable3.getBounds(); bounds3.offsetTo(width / 2 - bounds3.width() / 2, (int) ((body.boxCenterY - bounds3.height() / 2 + bounds3.height()) * percent3) - bounds3.height()); mDrawable3.draw(canvas); if (mDropOutOverFlow) { bounds1.offsetTo(width / 2 - bounds1.width() / 2, ((body.boxCenterY - bounds1.height() / 2))); mDrawable1.draw(canvas); bounds2.offsetTo(width / 2 - bounds2.width() / 2, ((body.boxCenterY - bounds2.height() / 2))); mDrawable2.draw(canvas); bounds3.offsetTo(width / 2 - bounds3.width() / 2, ((body.boxCenterY - bounds3.height() / 2))); mDrawable3.draw(canvas); } } if (footer) { canvas.restore(); } super.dispatchDraw(canvas); } // // protected int generateSideLength() { return mHeaderHeight / 5; } @NonNull protected Path generateClipPath(BoxBody body, int width) { mPath.reset(); mPath.lineTo(0, body.boxCenterTop); mPath.lineTo(body.boxLeft, body.boxCenterTop); mPath.lineTo(body.boxCenterX, body.boxCenterY); mPath.lineTo(body.boxRight, body.boxCenterTop); mPath.lineTo(width, body.boxCenterTop); mPath.lineTo(width, 0); mPath.close(); return mPath; } @NonNull protected BoxBody generateBoxBody(int width, int height, int sideLength) { final int margin = sideLength / 2; return mBoxBody.measure(width, height, sideLength, margin); } @NonNull protected Path generateBoxCoverPath(BoxBody body) { mPath.reset(); final int sideLength = (body.boxCenterX - body.boxLeft) * 4 / 5; final double offsetAngle = mReboundPercent * (Math.PI * 2 / 5); /* * 开始画左上的盖子 */ final float offsetLeftTopX = sideLength * (float) Math.sin(Math.PI / 3 - offsetAngle / 2); final float offsetLeftTopY = sideLength * (float) Math.cos(Math.PI / 3 - offsetAngle / 2); mPath.moveTo(body.boxLeft, body.boxCenterTop); mPath.lineTo(body.boxCenterX, body.boxTop); mPath.lineTo(body.boxCenterX - offsetLeftTopX, body.boxTop - offsetLeftTopY); mPath.lineTo(body.boxLeft - offsetLeftTopX, body.boxCenterTop - offsetLeftTopY); mPath.close(); /* * 开始画左下的盖子 */ final float offsetLeftBottomX = sideLength * (float) Math.sin(Math.PI / 3 + offsetAngle); final float offsetLeftBottomY = sideLength * (float) Math.cos(Math.PI / 3 + offsetAngle); mPath.moveTo(body.boxLeft, body.boxCenterTop); mPath.lineTo(body.boxCenterX, (body.boxBottom + body.boxTop) / 2f); mPath.lineTo(body.boxCenterX - offsetLeftBottomX, (body.boxBottom + body.boxTop) / 2f + offsetLeftBottomY); mPath.lineTo(body.boxLeft - offsetLeftBottomX, body.boxCenterTop + offsetLeftBottomY); mPath.close(); /* * 开始画右上的盖子 */ final float offsetRightTopX = sideLength * (float) Math.sin(Math.PI / 3 - offsetAngle / 2); final float offsetRightTopY = sideLength * (float) Math.cos(Math.PI / 3 - offsetAngle / 2); mPath.moveTo(body.boxRight, body.boxCenterTop); mPath.lineTo(body.boxCenterX, body.boxTop); mPath.lineTo(body.boxCenterX + offsetRightTopX, body.boxTop - offsetRightTopY); mPath.lineTo(body.boxRight + offsetRightTopX, body.boxCenterTop - offsetRightTopY); mPath.close(); /* * 开始画右下的盖子 */ final float offsetRightBottomX = sideLength * (float) Math.sin(Math.PI / 3 + offsetAngle); final float offsetRightBottomY = sideLength * (float) Math.cos(Math.PI / 3 + offsetAngle); mPath.moveTo(body.boxRight, body.boxCenterTop); mPath.lineTo(body.boxCenterX, (body.boxBottom + body.boxTop) / 2f); mPath.lineTo(body.boxCenterX + offsetRightBottomX, (body.boxBottom + body.boxTop) / 2f + offsetRightBottomY); mPath.lineTo(body.boxRight + offsetRightBottomX, body.boxCenterTop + offsetRightBottomY); mPath.close(); return mPath; } @NonNull protected Path generateBoxBodyPath(BoxBody body) { mPath.reset(); mPath.moveTo(body.boxLeft, body.boxCenterBottom); mPath.lineTo(body.boxCenterX, body.boxBottom); mPath.lineTo(body.boxRight, body.boxCenterBottom); mPath.quadTo(body.boxRight + body.boxSideLength / 2f * mReboundPercent, body.boxCenterY, body.boxRight, body.boxCenterTop); mPath.lineTo(body.boxCenterX, body.boxTop); mPath.lineTo(body.boxLeft, body.boxCenterTop); mPath.quadTo(body.boxLeft - body.boxSideLength / 2f * mReboundPercent, body.boxCenterY, body.boxLeft, body.boxCenterBottom); mPath.close(); return mPath; } // // @Override public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) { mKernel = kernel; mHeaderHeight = height; kernel.requestDrawBackgroundFor(this, mBackgroundColor); final int sideLength = generateSideLength(); mDrawable1.setBounds(0, 0, sideLength, sideLength); mDrawable2.setBounds(0, 0, sideLength, sideLength); mDrawable3.setBounds(0, 0, sideLength, sideLength); } @Override public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) { mHeight = offset; if (!isDragging || mState != RefreshState.Refreshing) { mReboundPercent = 1f * Math.max(0, offset - height) / maxDragHeight; } this.invalidate(); } @Override public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) { mState = newState; if (newState == RefreshState.None) { mDropOutOverFlow = false; } } /** * @param colors 对应Xml中配置的 srlPrimaryColor srlAccentColor * @deprecated 请使用 {@link RefreshLayout#setPrimaryColorsId(int...)} */ @Override@Deprecated public void setPrimaryColors(@ColorInt int ... colors) { if (colors.length > 0) { mBackgroundColor = colors[0]; if (mKernel != null) { mKernel.requestDrawBackgroundFor(this, mBackgroundColor); } if (colors.length > 1) { mAccentColor = colors[1]; } } } @Override public void onStartAnimator(@NonNull RefreshLayout layout, int height, int maxDragHeight) { if (mDropOutAnimator != null) { mDropOutAnimator.start(); } } @Override public int onFinish(@NonNull RefreshLayout layout, boolean success) { mDropOutPercent = 0; return 0; } // protected static class BoxBody { int boxCenterX; int boxCenterY; int boxBottom; int boxTop; int boxLeft; int boxCenterTop; int boxCenterBottom; int boxRight; int boxSideLength; BoxBody measure(int width, int height, int sideLength, int margin) { boxSideLength = sideLength; boxCenterX = width / 2; boxBottom = height - margin; boxTop = boxBottom - 2 * sideLength; boxLeft = boxCenterX - (int) (sideLength * Math.sin(Math.PI / 3)); boxCenterTop = boxTop + sideLength / 2; boxCenterBottom = boxBottom - sideLength / 2; boxRight = width - boxLeft; boxCenterY = boxBottom - sideLength; return this; } } }