oak.animatedsvgview.AnimatedSvgView.java Source code

Java tutorial

Introduction

Here is the source code for oak.animatedsvgview.AnimatedSvgView.java

Source

/*
 * Copyright 2014 Google Inc.
 *
 * 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 oak.animatedsvgview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PointF;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

import java.text.ParseException;

public class AnimatedSvgView extends View {

    private static final String TAG = "AnimatedSvgView";

    private int mTraceTime = 2000;
    private int mTraceTimePerGlyph = 1000;
    private int mFillStart = 1200;
    private int mFillTime = 1000;
    private static final int MARKER_LENGTH_DIP = 16;
    private int[] mTraceResidueColors;
    private int[] mTraceColors;
    private RatioSizingUtils.RatioSizingInfo mRatioSizingInfo = new RatioSizingUtils.RatioSizingInfo();
    private int mViewportWidth;
    private int mViewportHeight;
    private PointF mViewport = new PointF(mViewportWidth, mViewportHeight);

    private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();

    private Paint mFillPaint;
    private int[] mFillAlphas;
    private int[] mFillReds;
    private int[] mFillGreens;
    private int[] mFillBlues;
    private GlyphData[] mGlyphData;
    private String[] mGlyphStrings;
    private float mMarkerLength;
    private int mWidth;
    private int mHeight;
    private long mStartTime;

    public static final int STATE_NOT_STARTED = 0;
    public static final int STATE_TRACE_STARTED = 1;
    public static final int STATE_FILL_STARTED = 2;
    public static final int STATE_FINISHED = 3;

    private int mState = STATE_NOT_STARTED;
    private OnStateChangeListener mOnStateChangeListener;

    public AnimatedSvgView(Context context) {
        super(context);
        init(context, null);
    }

    public AnimatedSvgView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public AnimatedSvgView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    @SuppressWarnings("NewApi")
    private void init(Context context, AttributeSet attrs) {
        mFillPaint = new Paint();
        mFillPaint.setAntiAlias(true);
        mFillPaint.setStyle(Paint.Style.FILL);

        mMarkerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MARKER_LENGTH_DIP,
                getResources().getDisplayMetrics());

        mTraceColors = new int[1];
        mTraceColors[0] = Color.BLACK;
        mTraceResidueColors = new int[1];
        mTraceResidueColors[0] = Color.argb(50, 0, 0, 0);

        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatedSvgView);

            mViewportWidth = a.getInt(R.styleable.AnimatedSvgView_oakSvgImageSizeX, 433);
            mRatioSizingInfo.aspectRatioWidth = a.getInt(R.styleable.AnimatedSvgView_oakSvgImageSizeX, 433);
            mViewportHeight = a.getInt(R.styleable.AnimatedSvgView_oakSvgImageSizeY, 433);
            mRatioSizingInfo.aspectRatioHeight = a.getInt(R.styleable.AnimatedSvgView_oakSvgImageSizeY, 433);

            mTraceTime = a.getInt(R.styleable.AnimatedSvgView_oakSvgTraceTime, 2000);
            mTraceTimePerGlyph = a.getInt(R.styleable.AnimatedSvgView_oakSvgTraceTimePerGlyph, 1000);
            mFillStart = a.getInt(R.styleable.AnimatedSvgView_oakSvgFillStart, 1200);
            mFillTime = a.getInt(R.styleable.AnimatedSvgView_oakSvgFillTime, 1000);

            a.recycle();

            mViewport = new PointF(mViewportWidth, mViewportHeight);
        }

        // See https://github.com/romainguy/road-trip/blob/master/application/src/main/java/org/curiouscreature/android/roadtrip/IntroView.java
        // Note: using a software layer here is an optimization. This view works with
        // hardware accelerated rendering but every time a path is modified (when the
        // dash path effect is modified), the graphics pipeline will rasterize the path
        // again in a new texture. Since we are dealing with dozens of paths, it is much
        // more efficient to rasterize the entire view into a single re-usable texture
        // instead. Ideally this should be toggled using a heuristic based on the number
        // and or dimensions of paths to render.
        // Note that PathDashPathEffects can lead to clipping issues with hardware rendering.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        }
    }

    public void setViewportSize(int viewportWidth, int viewportHeight) {
        mViewportWidth = viewportWidth;
        mViewportHeight = viewportHeight;

        mRatioSizingInfo.aspectRatioWidth = viewportWidth;
        mRatioSizingInfo.aspectRatioHeight = viewportHeight;

        mViewport = new PointF(mViewportWidth, mViewportHeight);

        requestLayout();
    }

    public void setGlyphStrings(String[] glyphStrings) {
        mGlyphStrings = glyphStrings;
    }

    public void setTraceResidueColors(int[] traceResidueColors) {
        mTraceResidueColors = traceResidueColors;
    }

    public void setTraceColors(int[] traceColors) {
        mTraceColors = traceColors;
    }

    public void setFillPaints(int[] fillAlphas, int[] fillReds, int[] fillGreens, int[] fillBlues) {
        mFillAlphas = fillAlphas;
        mFillReds = fillReds;
        mFillGreens = fillGreens;
        mFillBlues = fillBlues;
    }

    public void start() {
        mStartTime = System.currentTimeMillis();
        changeState(STATE_TRACE_STARTED);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    public void reset() {
        mStartTime = 0;
        changeState(STATE_NOT_STARTED);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    public void setToFinishedFrame() {
        mStartTime = 1;
        changeState(STATE_FINISHED);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        rebuildGlyphData();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        RatioSizingUtils.RatioMeasureInfo rmi = RatioSizingUtils.getMeasureInfo(widthMeasureSpec, heightMeasureSpec,
                mRatioSizingInfo, 0, 0);

        super.onMeasure(MeasureSpec.makeMeasureSpec(rmi.width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(rmi.height, MeasureSpec.EXACTLY));
    }

    private void rebuildGlyphData() {
        SvgPathParser parser = new SvgPathParser() {
            @Override
            protected float transformX(float x) {
                return x * mWidth / mViewport.x;
            }

            @Override
            protected float transformY(float y) {
                return y * mHeight / mViewport.y;
            }
        };

        mGlyphData = new GlyphData[mGlyphStrings.length];
        for (int i = 0; i < mGlyphStrings.length; i++) {
            mGlyphData[i] = new GlyphData();
            try {
                mGlyphData[i].path = parser.parsePath(mGlyphStrings[i]);
            } catch (ParseException e) {
                mGlyphData[i].path = new Path();
                Log.e(TAG, "Couldn't parse path", e);
            }
            PathMeasure pm = new PathMeasure(mGlyphData[i].path, true);
            while (true) {
                mGlyphData[i].length = Math.max(mGlyphData[i].length, pm.getLength());
                if (!pm.nextContour()) {
                    break;
                }
            }
            mGlyphData[i].paint = new Paint();
            mGlyphData[i].paint.setStyle(Paint.Style.STROKE);
            mGlyphData[i].paint.setAntiAlias(true);
            mGlyphData[i].paint.setColor(Color.WHITE);
            mGlyphData[i].paint.setStrokeWidth(
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()));
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mState == STATE_NOT_STARTED || mGlyphData == null) {
            return;
        }

        long t = System.currentTimeMillis() - mStartTime;

        // Draw outlines (starts as traced)
        for (int i = 0; i < mGlyphData.length; i++) {
            float phase = MathUtil.constrain(0, 1,
                    (t - (mTraceTime - mTraceTimePerGlyph) * i * 1f / mGlyphData.length) * 1f / mTraceTimePerGlyph);
            float distance = INTERPOLATOR.getInterpolation(phase) * mGlyphData[i].length;
            mGlyphData[i].paint.setColor(mTraceResidueColors[i]);
            mGlyphData[i].paint
                    .setPathEffect(new DashPathEffect(new float[] { distance, mGlyphData[i].length }, 0));
            canvas.drawPath(mGlyphData[i].path, mGlyphData[i].paint);

            mGlyphData[i].paint.setColor(mTraceColors[i]);
            mGlyphData[i].paint.setPathEffect(new DashPathEffect(
                    new float[] { 0, distance, phase > 0 ? mMarkerLength : 0, mGlyphData[i].length }, 0));
            canvas.drawPath(mGlyphData[i].path, mGlyphData[i].paint);
        }

        if (t > mFillStart) {
            if (mState < STATE_FILL_STARTED) {
                changeState(STATE_FILL_STARTED);
            }

            // If after fill start, draw fill
            float phase = MathUtil.constrain(0, 1, (t - mFillStart) * 1f / mFillTime);
            for (int i = 0; i < mGlyphData.length; i++) {
                GlyphData glyphData = mGlyphData[i];
                mFillPaint.setARGB((int) (phase * ((float) mFillAlphas[i] / (float) 255) * 255), mFillReds[i],
                        mFillGreens[i], mFillBlues[i]);
                canvas.drawPath(glyphData.path, mFillPaint);
            }
        }

        if (t < mFillStart + mFillTime) {
            // draw next frame if animation isn't finished
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            changeState(STATE_FINISHED);
        }
    }

    private void changeState(int state) {
        if (mState == state) {
            return;
        }

        mState = state;
        if (mOnStateChangeListener != null) {
            mOnStateChangeListener.onStateChange(state);
        }
    }

    public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) {
        mOnStateChangeListener = onStateChangeListener;
    }

    public static interface OnStateChangeListener {

        void onStateChange(int state);
    }

    private static class GlyphData {

        Path path;
        Paint paint;
        float length;
    }

    public static class RatioSizingUtils {

        public static class RatioSizingInfo {

            public long aspectRatioWidth = 1;
            public long aspectRatioHeight = 1;
        }

        public static class RatioMeasureInfo {

            public int width;
            public int height;
        }

        public static RatioSizingInfo getRatioSizingInfoFromAttrs(Context context, AttributeSet attrs)
                throws IllegalArgumentException {
            RatioSizingInfo rsi = new RatioSizingInfo();

            //        if (attrs != null) {
            //            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AspectRatioLayout);
            //
            //            String ratioString = a.getString(R.styleable.AspectRatioLayout_oakAspectRatio);
            //            try {
            //                rsi = parseRatioSizingInfo(ratioString);
            //            } catch (IllegalArgumentException e) {
            //                throw e;
            //            }
            //
            //            a.recycle();
            //        }

            return rsi;
        }

        public static RatioSizingInfo parseRatioSizingInfo(String ratioString) throws IllegalArgumentException {
            RatioSizingInfo rsi = new RatioSizingInfo();

            if (TextUtils.isEmpty(ratioString)) {
                rsi.aspectRatioWidth = 1;
                rsi.aspectRatioHeight = 1;
                return rsi;
            }

            String[] parts = ratioString.split("[x:]");
            if (parts.length == 2) {
                rsi.aspectRatioWidth = Integer.parseInt(parts[0]);
                rsi.aspectRatioHeight = Integer.parseInt(parts[1]);
            } else {
                throw new IllegalArgumentException("Invalid ratio: " + ratioString);
            }

            return rsi;
        }

        public static RatioMeasureInfo getMeasureInfo(int widthMeasureSpec, int heightMeasureSpec,
                RatioSizingInfo ratioSizingInfo, int widthPadding, int heightPadding) {
            RatioMeasureInfo rmi = new RatioMeasureInfo();

            rmi.width = View.MeasureSpec.getSize(widthMeasureSpec) - widthPadding;
            rmi.height = View.MeasureSpec.getSize(heightMeasureSpec) - heightPadding;
            int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
            int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);

            if (rmi.height <= 0 && rmi.width <= 0 && heightMode == View.MeasureSpec.UNSPECIFIED
                    && widthMode == View.MeasureSpec.UNSPECIFIED) {
                rmi.width = 0;
                rmi.height = 0;
            } else if (rmi.height <= 0 && heightMode == View.MeasureSpec.UNSPECIFIED) {
                rmi.height = (int) (rmi.width * ratioSizingInfo.aspectRatioHeight
                        / ratioSizingInfo.aspectRatioWidth);
            } else if (rmi.width <= 0 && widthMode == View.MeasureSpec.UNSPECIFIED) {
                rmi.width = (int) (rmi.height * ratioSizingInfo.aspectRatioWidth
                        / ratioSizingInfo.aspectRatioHeight);
            } else if (rmi.width * ratioSizingInfo.aspectRatioHeight > ratioSizingInfo.aspectRatioWidth
                    * rmi.height) {
                rmi.width = (int) (rmi.height * ratioSizingInfo.aspectRatioWidth
                        / ratioSizingInfo.aspectRatioHeight);
            } else {
                rmi.height = (int) (rmi.width * ratioSizingInfo.aspectRatioHeight
                        / ratioSizingInfo.aspectRatioWidth);
            }

            return rmi;
        }
    }
}