/* * Copyright (C) 2015 Brent Marriott * * 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.hookedonplay.decoviewlib.charts; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.hookedonplay.decoviewlib.DecoView; /** * Animates some non-core movements for the series of data, such as fades and swirls. */ public class DecoDrawEffect { /** * Value for fully opaque alpha value */ static private final int MAX_ALPHA = 255; /** * Minimum percentage of dimension to allow explode lines */ static private final float EXPLODE_LINE_MIN = 0.01f; /** * Maximum percentage of dimension to allow explode lines */ static private final float EXPLODE_LINE_MAX = 0.1f; /** * Minimum radius of circle in explode mode */ static private final float EXPLODE_CIRCLE_MIN = 0.01f; /** * Maximum radius of circle in explode mode */ static private final float EXPLODE_CIRCLE_MAX = 0.1f; /** * Number of lines created during explode animation */ static private final int EXPLODE_LINE_COUNT = 9; static private final float MIN_LINE_WIDTH = 10f; static private final float MAX_LINE_WIDTH = 100f; /** * Effect type to draw * {@link EffectType} */ private final EffectType mEffectType; /** * Paint to use for drawing arc item in effect */ private Paint mPaint; /** * Paint to use for drawing explode effect */ private Paint mPaintExplode; /** * Paint to use for drawing text in effect */ private Paint mPaintText; /** * String to display during EFFECT_EXPLODE and EFFECT_SPIRAL_EXPLODE */ private String mText; /** * Bounds used to allow contraction (or expansion) of spiral animations */ private final RectF mSpinBounds = new RectF(); private int mCircuits = 6; /** * Construct the delegate for painting the special effects for the arc * * @param effectType Type of animation * @param paint Paint to use to perform the effect * @param text Optional text to display during some effects * <p/> * No Access Modifier for the constructor is specified. This is deliberate so we use the * default package access. This class should not be constructed outside of the package scope. * Clients of this library need only to pass an EffectType to the {@link DecoView} */ DecoDrawEffect(@NonNull EffectType effectType, @NonNull Paint paint, @Nullable String text) { mEffectType = effectType; setPaint(paint); setText(text, paint.getColor()); } @SuppressWarnings("unused") DecoDrawEffect(@NonNull EffectType effectType, @NonNull Paint paint) { mEffectType = effectType; setPaint(paint); } /** * Determine the visibility of the arc on completion of this animation effect. * * @return should remain visible */ public boolean postExecuteVisibility() { return (mEffectType == EffectType.EFFECT_SPIRAL_OUT) || (mEffectType == EffectType.EFFECT_SPIRAL_OUT_FILL); } private void setPaint(@NonNull Paint paint) { mPaint = new Paint(paint); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); mPaint.setStrokeWidth(determineLineWidth(paint, 1f)); /** * Create the explode line based on the same paint attributes to keep consistent * The line width is made smaller as they will be X lines created during this explode * effect */ mPaintExplode = new Paint(paint); mPaintExplode.setStrokeCap(Paint.Cap.ROUND); mPaintExplode.setStyle(Paint.Style.FILL); mPaintExplode.setStrokeWidth(determineLineWidth(paint, 0.66f)); } private float determineLineWidth(@NonNull Paint paint, float factor) { float width = paint.getStrokeWidth(); width = Math.min(width, MAX_LINE_WIDTH); width = Math.max(width, MIN_LINE_WIDTH); return width * factor; } /** * Set the text and text color for the explode text animation * * @param text String to display during animation * @param color Color of the text */ public void setText(@Nullable String text, int color) { mText = text; mPaintText = new Paint(); mPaintText.setColor(color); mPaintText.setTextAlign(Paint.Align.CENTER); mPaintText.setAntiAlias(true); } @SuppressWarnings("unused") public void setRotationCount(int circuits) { mCircuits = circuits; } /** * Draw effect at current percentage * * @param canvas Canvas to draw animation onto * @param bounds bounds to use for drawing animation * @param percentComplete percentage that the animation is complete * @param startAngle The initial angle the the arc starts from * @param sweepAngle The total amount of angle for the View (360 for circle) */ public void draw(@NonNull Canvas canvas, @NonNull RectF bounds, float percentComplete, float startAngle, float sweepAngle) { switch (mEffectType) { case EFFECT_SPIRAL_EXPLODE: final float step = 0.6f; if (percentComplete <= step) { drawMoveToCenter(canvas, bounds, percentComplete * (1f / step), startAngle, sweepAngle); } else { final float remain = 1.0f - step; drawExplode(canvas, bounds, (percentComplete - step) / remain); drawText(canvas, bounds, (percentComplete - step) / remain); } break; case EFFECT_EXPLODE: drawExplode(canvas, bounds, percentComplete); drawText(canvas, bounds, percentComplete); break; case EFFECT_SPIRAL_IN: case EFFECT_SPIRAL_OUT: case EFFECT_SPIRAL_OUT_FILL: drawMoveToCenter(canvas, bounds, percentComplete, startAngle, sweepAngle); break; } } /** * Animate the series in a spiral motion moving to of from the center of the bounds. * <p/> * If the EffectType is EffectType.EFFECT_SPIRAL_OUT_FILL the animation will continue after * reaching the start position and continue to fill out the complete track which is defined * as the startAngle + the sweep angle. This feature would generally be used to animate the * background track on its initial display. * * @param canvas Canvas to draw animation onto * @param bounds bounds to use for drawing animation * @param percentComplete percentage that the animation is complete * @param startAngle The initial angle the the arc starts from * @param sweepAngle The total amount of angle for the View. If this is a complete circle * this will be 360, or if it is an arc it will be < 360 */ public void drawMoveToCenter(@NonNull Canvas canvas, RectF bounds, float percentComplete, float startAngle, float sweepAngle) { // Animation moves outward from center to outside final boolean moveOutward = mEffectType == EffectType.EFFECT_SPIRAL_OUT || mEffectType == EffectType.EFFECT_SPIRAL_OUT_FILL; // Animation spins in a clockwise direction final boolean spinClockwise = mEffectType != EffectType.EFFECT_SPIRAL_IN && mEffectType != EffectType.EFFECT_SPIRAL_EXPLODE; final float buffer = 10f; final float halfWidth = (bounds.width() / 2) - buffer; final float halfHeight = (bounds.height() / 2) - buffer; final float baseRotateAngle = mCircuits * 360f; float rotateAmount = (mEffectType == EffectType.EFFECT_SPIRAL_OUT_FILL) ? baseRotateAngle + 360f : baseRotateAngle; float rotateOffset = rotateAmount * percentComplete; float newAngle = (startAngle + (spinClockwise ? rotateOffset : -rotateOffset)) % 360; float sweep = getSweepAngle(percentComplete); mSpinBounds.set(bounds); float percent = percentComplete; if (moveOutward) { // Make the animation move outward by inverting the percentage complete percent = 1.0f - percentComplete; } if (mEffectType == EffectType.EFFECT_SPIRAL_OUT_FILL) { if ((rotateAmount * percentComplete) > (rotateAmount - 360f)) { mPaint.setStyle(Paint.Style.STROKE); sweep = (rotateAmount * percentComplete) % 360; if (sweep <= 0) { sweep = 360; } // Cap the fill effect at the sweepAngle for non circular arcs if (sweep > sweepAngle) { sweep = sweepAngle; } newAngle = startAngle; } else { float min = 1.0f - (baseRotateAngle / rotateAmount); if (percent > min) { float adjustedPercentage = (percent - min) / (1.0f - min); mSpinBounds.inset(halfWidth * adjustedPercentage, halfHeight * adjustedPercentage); } } } else { // Restrict the bounds to move drawing closer to center of area mSpinBounds.inset(halfWidth * percent, halfHeight * percent); } canvas.drawArc(mSpinBounds, newAngle, sweep, false, mPaint); } private float getSweepAngle(float percentComplete) { final float sweepMax = 30f; final float sweepMin = 0.1f; if (percentComplete < 0.5) { return sweepMin + (sweepMax - sweepMin) * (percentComplete * 2); } return sweepMax - (sweepMax - sweepMin) * ((percentComplete - 0.5f) * 2); } /** * Animates the drawing of a text animation of size and alpha * * @param canvas Canvas to draw text * @param percentComplete percent of the animation complete (0..1) */ public void drawText(@NonNull Canvas canvas, RectF bounds, float percentComplete) { if (mText != null && mText.length() > 0) { mPaintText.setTextSize(100 * percentComplete); mPaintText.setAlpha(MAX_ALPHA); final float startFadePercent = 0.7f; if (percentComplete > startFadePercent) { int alphaText = (int) (MAX_ALPHA - (MAX_ALPHA * ((percentComplete - startFadePercent) / (1.0f - startFadePercent)))); mPaintText.setAlpha(alphaText); } // Calculate a centered position for the text final float xPos = bounds.left + (bounds.width() / 2); final float yPos = (bounds.top + (bounds.height() / 2)) - ((mPaintText.descent() + mPaintText.ascent()) / 2); canvas.drawText(mText, xPos, yPos, mPaintText); } } /** * Creates an animation where X lines are created and move from the center of the bounds to * the outside of the bounds. As the near the edge an alpha fade is applied * * @param canvas Canvas to draw effect onto * @param bounds Area to perform the effect * @param percentComplete percentage of the animation that has been completed (0..1) */ public void drawExplode(@NonNull Canvas canvas, RectF bounds, float percentComplete) { boolean drawCircles = Build.VERSION.SDK_INT <= 17; final float maxLength = bounds.width() * EXPLODE_LINE_MAX; final float minLength = bounds.width() * EXPLODE_LINE_MIN; final float startPosition = bounds.width() * EXPLODE_LINE_MAX; int alpha = MAX_ALPHA; float length; if (percentComplete > 0.5f) { float completed = (percentComplete - 0.5f) * 2; length = maxLength - (completed * (maxLength - minLength)); alpha = MAX_ALPHA - (int) (MAX_ALPHA * completed); } else { length = minLength + ((percentComplete * 2) * (maxLength - minLength)); } final int initialAlpha = mPaint.getAlpha(); if (alpha < MAX_ALPHA) { mPaintExplode.setAlpha((int) (initialAlpha * (alpha / (float) MAX_ALPHA))); } float radiusEnd = startPosition + (int) (((bounds.width() / 2) - startPosition) * percentComplete); float radiusStart = radiusEnd - length; float angleInDegrees = 0; for (int i = 0; i < EXPLODE_LINE_COUNT; i++) { drawExplodeLine(canvas, bounds, radiusStart, radiusEnd, angleInDegrees, percentComplete, drawCircles); angleInDegrees += (360f / EXPLODE_LINE_COUNT); } if (alpha < MAX_ALPHA) { mPaint.setAlpha(initialAlpha); } } private void drawExplodeLine(@NonNull Canvas canvas, RectF bounds, float radiusStart, float radiusEnd, float angleInDegrees, float percentComplete, boolean compatMode) { float startX = (radiusStart * (float) Math.cos(angleInDegrees * Math.PI / 180F)) + bounds.centerX(); float startY = (radiusStart * (float) Math.sin(angleInDegrees * Math.PI / 180F)) + bounds.centerY(); float endX = (radiusEnd * (float) Math.cos(angleInDegrees * Math.PI / 180F)) + bounds.centerX(); float endY = (radiusEnd * (float) Math.sin(angleInDegrees * Math.PI / 180F)) + bounds.centerY(); if (!compatMode) { canvas.drawLine(startX, startY, endX, endY, mPaintExplode); } else { // Bug on older Android versions where drawLine does not apply round cap when // using drawLine when hardware acceleration is enabled. In this case // we just draw a circle instead float radius = (bounds.width() * EXPLODE_CIRCLE_MIN) + ((bounds.width() * EXPLODE_CIRCLE_MAX - bounds.width() * EXPLODE_CIRCLE_MIN) * percentComplete); canvas.drawCircle(endX, endY, radius, mPaintExplode); } } /** * Type of effect to display */ public enum EffectType { EFFECT_SPIRAL_OUT_FILL, /* Fill track after outward spiral animation */ EFFECT_SPIRAL_OUT, /* Animation from center to outside in spiral motion */ EFFECT_SPIRAL_IN, /* Animation from outside to center in spiral motion */ EFFECT_EXPLODE, /* Explode animation where several lines are produced from center */ EFFECT_SPIRAL_EXPLODE /* Combines EFFECT_SPIRAL_IN and EFFECT_EXPLODE */ } }