/* * Copyright 2014 Diogo Bernardino * * 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.db.chart.view.animation; import java.lang.reflect.Array; import java.util.ArrayList; import android.graphics.Path; import android.graphics.PathMeasure; import com.db.chart.model.ChartSet; import com.db.chart.view.ChartView; import com.db.chart.view.animation.easing.BaseEasingMethod; import com.db.chart.view.animation.easing.quint.QuintEaseOut; /** * Controls the whole animation process. */ public class Animation{ /** The delay between data updates to be drawn in the screen */ private final static long DELAY_BETWEEN_UPDATES = 20; /** Default animation duration */ private final static int DEFAULT_DURATION = 1000; /** Default animation overlap */ private final static float DEFAULT_OVERLAP_FACTOR = 1f; /** Default animation alpha */ private final static int DEFAULT_ALPHA_OFF = -1; /** Task that handles with animation updates */ private Runnable mRunnable; /** Maintains path measures to get position updates **/ private PathMeasure[][] mPathMeasures; /** * Animation global duration * Likely to be removed in future. */ private long mGlobalDuration; /** * Keeps the current global duration * Likely to be removed in future. */ private long mCurrentGlobalDuration; /** * Keeps the global initial time of the animation * Likely to be removed in future. */ private long mGlobalInitTime; /** Controls interpolation of the animation */ private BaseEasingMethod mEasing; /** Control animation updates */ private Runnable mAnimator = new Runnable() { @Override public void run() { if(mChartView.canIPleaseAskYouToDraw()){ mChartView.addData(getUpdate(mChartView.getData())); mChartView.postInvalidate(); } } }; /** {@link ChartView} element to request draw updates */ private ChartView mChartView; /** Keeps information if animation is ongoing or not */ private boolean mPlaying; /** Flags the cancellation of the on-going animation */ private boolean mCancelled; /** Keeps the initial time of the animation for each of the points */ private long[] mInitTime; /** Keeps the current duration of the animation in each of the points */ private long[] mCurrentDuration; /** Animation duration for each of the points */ private int mDuration; /** * Keeps the information regarding whether points will be animated * in parallel or in sequence. */ private float mOverlapingFactor; /** * Factor from 0 to 1 to specify where does the animation starts * according innerchart area. */ private float mStartXFactor; private float mStartYFactor; /** Alpha speed to include in animation */ private int mAlphaSpeed; /** Animation order */ private int[] mOrder; /** Whether the animation refers to entering or exiting */ private boolean mIsExiting; public Animation(){ init(DEFAULT_DURATION); } public Animation(int duration){ init(duration); } private void init(int duration){ mGlobalDuration = duration; mOverlapingFactor = DEFAULT_OVERLAP_FACTOR; mAlphaSpeed = DEFAULT_ALPHA_OFF; mEasing = new QuintEaseOut(); mStartXFactor = -1f; mStartYFactor = -1f; mPlaying = false; mCurrentGlobalDuration = 0; mGlobalInitTime = 0; } /** * Method that prepares the animation. Defines starting points, targets, * distance, yadda, as well as the first set of points to be drawn. * * @param chartView {@link ChartView} to be invalidated each time the * animation wants to update values * @param start X and Y start coordinates * @param end X and Y end coordinates * @return Array of {@link ChartSet} containing the first values to be drawn. */ public ArrayList<ChartSet> prepareAnimation(ChartView chartView, ArrayList<float[][]> start, ArrayList<float[][]> end){ final int nSets = start.size(); final int nEntries = start.get(0).length; mChartView = chartView; mCurrentDuration = new long[nEntries]; // Set the animation order if not defined already if(mOrder == null){ mOrder = new int[nEntries]; for(int i = 0; i < mOrder.length; i++) mOrder[i] = i; } // Calculates the expected duration as there was with no overlap (factor = 0) float noOverlapDuration = mGlobalDuration / nEntries; // Adjust the duration to the overlap mDuration = (int) (noOverlapDuration + (mGlobalDuration - noOverlapDuration) * mOverlapingFactor); // Define animation paths for each entry Path path; mPathMeasures = new PathMeasure[nSets][nEntries]; for(int i = 0; i < nSets; i++){ for(int j = 0; j < nEntries; j++){ path = new Path(); path.moveTo(start.get(i)[j][0], start.get(i)[j][1]); path.lineTo(end.get(i)[j][0], end.get(i)[j][1]); mPathMeasures[i][j] = new PathMeasure(path, false); } } // Define initial time for each entry mInitTime = new long[nEntries]; mGlobalInitTime = System.currentTimeMillis(); long noOverlapInitTime; for(int i = 0; i < nEntries; i++){ // Calculates the expected init time as there was with no overlap (factor = 0) noOverlapInitTime = mGlobalInitTime + (i * (mGlobalDuration / nEntries)); // Adjust the init time to overlap mInitTime[mOrder[i]] = (noOverlapInitTime - ((long) (mOverlapingFactor * (noOverlapInitTime - mGlobalInitTime)))); } mPlaying = true; return getUpdate(mChartView.getData()); } /** * Method that prepares the animation. Defines starting points, targets, * distance, yadda, as well as the first set of points to be drawn. * * @param chartView {@link ChartView} to be invalidate each time the * animation wants to update values and to get the {@link ChartSet} * containing the target values */ private ArrayList<ChartSet> prepareAnimation(ChartView chartView){ final ArrayList<ChartSet> sets = chartView.getData(); float x = 0; if(mStartXFactor != -1) x = chartView.getInnerChartLeft() + (chartView.getInnerChartRight() - chartView.getInnerChartLeft()) * mStartXFactor; else x = chartView.getZeroPosition(); float y = 0; if(mStartYFactor != -1) y = chartView.getInnerChartBottom() - (chartView.getInnerChartBottom() - chartView.getInnerChartTop()) * mStartYFactor; else y = chartView.getZeroPosition(); final int nSets = sets.size(); final int nEntries = sets.get(0).size(); ArrayList<float[][]> startValues = new ArrayList<float[][]>(nSets); ArrayList<float[][]> endValues = new ArrayList<float[][]>(nSets); float[][] startSet; float[][] endSet; for(int i = 0; i < nSets; i++){ startSet = new float[nEntries][2]; endSet = new float[nEntries][2]; for(int j = 0; j < nEntries; j++){ if(mStartXFactor == -1 && chartView.getOrientation() == ChartView.Orientation.VERTICAL) startSet[j][0] = sets.get(i).getEntry(j).getX(); else startSet[j][0] = x; if(mStartYFactor == -1 && chartView.getOrientation() == ChartView.Orientation.HORIZONTAL) startSet[j][1] = sets.get(i).getEntry(j).getY(); else startSet[j][1] = y; endSet[j][0] = sets.get(i).getEntry(j).getX(); endSet[j][1] = sets.get(i).getEntry(j).getY();; } startValues.add(startSet); endValues.add(endSet); } return prepareAnimation(chartView, startValues, endValues); } /** * Method that prepares the enter animation. Defines starting points, targets, * distance, yadda, as well as the first set of points to be drawn. * * @param chartView {@link ChartView} to be invalidate each time the animation wants to update * values and to get the {@link ChartSet} containing the target values */ public ArrayList<ChartSet> prepareEnterAnimation(ChartView chartView){ mIsExiting = false; return prepareAnimation(chartView); } /** * Method that prepares the enter animation. Defines starting points, targets, * distance, yadda, as well as the first set of points to be drawn. * * @param chartView {@link ChartView} to be invalidate each time the animation wants to * update values and to get the {@link ChartSet} containing the target values */ public ArrayList<ChartSet> prepareExitAnimation(ChartView chartView){ mIsExiting = true; return prepareAnimation(chartView); } /** * Updates values, with the next interpolation, to be drawn next. * * @return return the next interpolated values. */ private ArrayList<ChartSet> getUpdate(ArrayList<ChartSet> data){ final int nSets = data.size(); final int nEntries = data.get(0).size(); // Process current animation duration long diff; long currentTime = System.currentTimeMillis(); mCurrentGlobalDuration = currentTime - mGlobalInitTime; for(int i = 0; i < nEntries; i++) { try { diff = currentTime - mInitTime[i]; if(diff < 0) mCurrentDuration[i] = 0; else mCurrentDuration[i] = diff; } catch (ArrayIndexOutOfBoundsException e) { mCurrentDuration[mCurrentDuration.length - 1] = 0; } } // In case current duration slightly goes over the animation duration, // force it to the duration value if(mCurrentGlobalDuration > mGlobalDuration) mCurrentGlobalDuration = mGlobalDuration; // Update next values to be drawn float[] posUpdate = new float[2]; float timeNormalized = 1; for(int i = 0; i < nSets; i++) for(int j = 0; j < nEntries; j++){ try { timeNormalized = normalizeTime(j); } catch (ArrayIndexOutOfBoundsException e) {} if(mAlphaSpeed != -1) data.get(i).setAlpha(timeNormalized * mAlphaSpeed); if(!getEntryUpdate(i, j, timeNormalized, posUpdate)){ posUpdate[0] = data.get(i).getEntry(j).getX(); posUpdate[1] = data.get(i).getEntry(j).getY(); } data.get(i).getEntry(j).setCoordinates(posUpdate[0], posUpdate[1]); } // Sets the next update or finishes the animation if(mCurrentGlobalDuration < mGlobalDuration && !mCancelled){ mChartView.postDelayed(mAnimator, DELAY_BETWEEN_UPDATES); mCurrentGlobalDuration+= DELAY_BETWEEN_UPDATES; }else{ mCurrentGlobalDuration = 0; mGlobalInitTime = 0; if(mRunnable != null) mRunnable.run(); mPlaying = false; mAlphaSpeed = -1; } return data; } /** * Normalize time to a 0-1 relation. * * @param index * @return value from 0 to 1 telling the next step. */ private float normalizeTime(int index) { if(!mIsExiting) return (float) mCurrentDuration[index] / mDuration; else return 1f - (float) mCurrentDuration[index] / mDuration; } /** * Cancel the on-going animation. */ public void cancel(){ mCancelled = true; } /** * Gets the next position coordinate of a point. * * @param i set index * @param j point index * @param normalizedTime normalized time from 0 to 1 * @return x display value where point will be drawn */ private boolean getEntryUpdate(int i, int j, float normalizedTime, float[] pos){ try { return mPathMeasures[i][j].getPosTan(mPathMeasures[i][j].getLength() * mEasing.next(normalizedTime), pos, null); } catch (ArrayIndexOutOfBoundsException e) { return true; } } public boolean isPlaying(){ return mPlaying; } public Runnable getEndAction(){ return mRunnable; } /* * -------- * Setters * -------- */ public Animation setEasing(BaseEasingMethod easing){ mEasing = easing; return this; } public Animation setDuration(int duration){ mGlobalDuration = duration; return this; } /** * Sets whether entries should be animate in sequence or parallel. * * @param factor value from 0 to 1 that tells how much will be the overlap of an entry's * animation according to the previous one. * 0 - no overlap * 1 - all entries animate in parallel (default) */ public Animation setOverlap(float factor){ mOverlapingFactor = factor; return this; } /** * Sets whether entries should be animate in sequence or parallel. * * @param factor value from 0 to 1 that tells how much will be the overlap of an entry's * animation according to the previous one * 0 - no overlap * 1 - all entries animate in parallel (default) * @param order order from which the entries will be animated * { 0, 1, 2, 3, ...} - default order */ public Animation setOverlap(float factor, int[] order){ mOverlapingFactor = factor; mOrder = order; return this; } /** * Sets an action to be executed once the animation finishes. * * @param runnable to be executed once the animation finishes */ public Animation setEndAction(Runnable runnable){ mRunnable = runnable; return this; } /** * Sets the starting point for the animation. * * @param xFactor horizontal factor between 0 and 1 * @param yFactor vertical factor between 0 and 1 * Eg. xFactor=0; yFactor=0; starts the animation on the bottom left * corner of the inner chart area. */ public Animation setStartPoint(float xFactor, float yFactor){ mStartXFactor = xFactor; mStartYFactor = yFactor; return this; } /** * Sets an alpha speed to animation. * * @param speed speed of alpha animation values according with translation. * To disable alpha set it to -1. * Eg. If speed 2 alpha goes twice faster than translation. */ public Animation setAlpha(int speed){ mAlphaSpeed = speed; return this; } }