/* * Copyright (C) 2015 Hippo Seven * * 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.hippo.drawable; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import androidx.annotation.ColorInt; import com.hippo.ehviewer.R; import com.hippo.yorozuya.MathUtils; /** * A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them. */ public class DrawerArrowDrawable extends Drawable { private final Paint mPaint = new Paint(); // The angle in degrees that the arrow head is inclined at. private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45); private final float mBarThickness; // The length of top and bottom bars when they merge into an arrow private final float mTopBottomArrowSize; // The length of middle bar private final float mBarSize; // The length of the middle bar when arrow is shaped private final float mMiddleArrowSize; // The space between bars when they are parallel private final float mBarGap; // Whether bars should spin or not during progress private final boolean mSpin; // Use Path instead of canvas operations so that if color has transparency, overlapping sections // wont look different private final Path mPath = new Path(); // The reported intrinsic size of the drawable. private final int mSize; // Whether we should mirror animation when animation is reversed. private boolean mVerticalMirror = false; // The interpolated version of the original progress private float mProgress; // the amount that overlaps w/ bar size when rotation is max private final float mMaxCutForBarSize; /** * @param context used to get the configuration for the drawable from */ public DrawerArrowDrawable(Context context, int color) { Resources resources = context.getResources(); mPaint.setAntiAlias(true); mPaint.setColor(color); mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size); // round this because having this floating may cause bad measurements mBarSize = Math.round(resources.getDimension(R.dimen.dad_bar_size)); // round this because having this floating may cause bad measurements mTopBottomArrowSize = Math.round(resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size)); mBarThickness = resources.getDimension(R.dimen.dad_thickness); // round this because having this floating may cause bad measurements mBarGap = Math.round(resources.getDimension(R.dimen.dad_gap_between_bars)); mSpin = resources.getBoolean(R.bool.dad_spin_bars); mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeJoin(Paint.Join.MITER); mPaint.setStrokeCap(Paint.Cap.BUTT); mPaint.setStrokeWidth(mBarThickness); mMaxCutForBarSize = (float) (mBarThickness / 2 * Math.cos(ARROW_HEAD_ANGLE)); } /** * If set, canvas is flipped when progress reached to end and going back to start. */ protected void setVerticalMirror(boolean verticalMirror) { mVerticalMirror = verticalMirror; } @Override public void draw(Canvas canvas) { Rect bounds = getBounds(); // Interpolated widths of arrow bars final float arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress); final float middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress); // Interpolated size of middle bar final float middleBarCut = Math.round(MathUtils.lerp(0, mMaxCutForBarSize, mProgress)); // The rotation of the top and bottom bars (that make the arrow head) final float rotation = MathUtils.lerp(0, ARROW_HEAD_ANGLE, mProgress); // The whole canvas rotates as the transition happens final float canvasRotate = MathUtils.lerp(-180, 0, mProgress); final float arrowWidth = Math.round(arrowSize * Math.cos(rotation)); final float arrowHeight = Math.round(arrowSize * Math.sin(rotation)); mPath.rewind(); final float topBottomBarOffset = MathUtils.lerp(mBarGap + mBarThickness, -mMaxCutForBarSize, mProgress); final float arrowEdge = -middleBarSize / 2; // draw middle bar mPath.moveTo(arrowEdge + middleBarCut, 0); mPath.rLineTo(middleBarSize - middleBarCut * 2, 0); // bottom bar mPath.moveTo(arrowEdge, topBottomBarOffset); mPath.rLineTo(arrowWidth, arrowHeight); // top bar mPath.moveTo(arrowEdge, -topBottomBarOffset); mPath.rLineTo(arrowWidth, -arrowHeight); mPath.close(); canvas.save(); // Rotate the whole canvas if spinning, if not, rotate it 180 to get // the arrow pointing the other way for RTL. canvas.translate(bounds.centerX(), bounds.centerY()); if (mSpin) { canvas.rotate(canvasRotate * (mVerticalMirror ? -1 : 1)); } canvas.drawPath(mPath, mPaint); canvas.restore(); } public void setColor(@ColorInt int color) { mPaint.setColor(color); invalidateSelf(); } @Override public void setAlpha(int i) { mPaint.setAlpha(i); } @Override public void setColorFilter(ColorFilter colorFilter) { mPaint.setColorFilter(colorFilter); } @Override public int getIntrinsicHeight() { return mSize; } @Override public int getIntrinsicWidth() { return mSize; } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @SuppressWarnings("unused") public float getProgress() { return mProgress; } @SuppressWarnings("unused") public void setProgress(float progress) { if (progress == 1f) { setVerticalMirror(true); } else if (progress == 0f) { setVerticalMirror(false); } mProgress = progress; invalidateSelf(); } public void setMenu(long duration) { setShape(false, duration); } public void setArrow(long duration) { setShape(true, duration); } public void setShape(boolean arrow, long duration) { if (!((!arrow && mProgress == 0f) || (arrow && mProgress == 1f))) { float endProgress = arrow ? 1f : 0f; if (duration <= 0) { setProgress(endProgress); } else { ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress); oa.setDuration(duration); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { oa.setAutoCancel(true); } oa.start(); } } } }