Android实现炫酷SVG动画效果

来源:互联网 发布:哈尔滨程序员工资 编辑:程序博客网 时间:2024/04/27 22:14

svg是目前十分流行的图像文件格式了,svg严格来说应该是一种开放标准的矢量图形语言,使用svg格式我们可以直接用代码来描绘图像,可以用任何文字处理工具打开svg图像,通过改变部分代码来使图像具有交互功能,并可以随时插入到HTML中通过浏览器(如火狐浏览器)来观看。使用svg格式可让你设计激动人心的、高分辨率的Web图形页面。


svg格式具备目前网络流行的jpg和png等格式无法具备的优势:可以任意放大图形显示,但绝不会以牺牲图像质量为代价;可在svg图像中保留可编辑和可搜寻的状态;平均来讲,svg文件比其它格式的图像文件要小很多,因而下载也很快。


我们先来看几张Android上使用SVG的效果图:

                                        


从上面的图片看到,如果我们自己来实现这样的特效,非常的麻烦,不过接下来给大家介绍一个开源控件,就可以配合SVG实现这些效果。

首先我们来了解SVG文件的格式,举个例子:

<svg xmlns="http://www.w3.org/2000/svg" id="svg" class="svg" viewBox="0 0 960 480" preserveAspectRatio="xMinYMin meet">        <path fill="#B4BEC8" stroke="#B4BEC8" stroke-width="2px" stroke-miterlimit="10" d="M570.14 440.2l-29.165-28.99c-7.103-8.5-6.152-36.718-6.02-40.665H425.048c.133 3.947 1.082 32.164-6.018 40.666l-29.166 28.99c-1.237 1.404-1.712 2.505-1.623 3.37h-.054c.76 7.727 6.664 6.332 13.607 6.332H558.01c6.696 0 12.412 1.27 13.493-5.56.58-.953.274-2.282-1.364-4.14z" style="fill-opacity: 1; stroke-opacity: 0; -webkit-transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; stroke-dasharray: 474.095184326172px, 474.095184326172px; stroke-dashoffset: 0px;"></path>        <path fill="#C8D2DC" stroke="#C8D2DC" stroke-width="2px" stroke-miterlimit="10" d="M727.488 355.125c0 8.514-6.597 15.42-14.738 15.42h-465.5c-8.14 0-14.74-6.906-14.74-15.42V45.42c0-8.517 6.6-15.42 14.74-15.42h465.5c8.142 0 14.738 6.903 14.738 15.42v309.705z" style="fill-opacity: 1; stroke-opacity: 0; -webkit-transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; stroke-dasharray: 1645.18310546875px, 1645.18310546875px; stroke-dashoffset: 0px;"></path>        <path fill="#fff" stroke="#C8D2DC" stroke-width="2px" stroke-miterlimit="10" d="M489.01 343.713c-.042-4.223 3.447-6.254 3.605-6.352-1.963-2.866-5.018-3.263-6.102-3.31-2.602-.26-5.074 1.53-6.39 1.53s-3.356-1.49-5.506-1.448c-2.836.04-5.445 1.645-6.907 4.182-2.942 5.11-.75 12.672 2.116 16.814 1.4 2.02 3.072 4.305 5.268 4.22 2.114-.08 2.913-1.362 5.467-1.362 2.556 0 3.274 1.363 5.51 1.322 2.273-.04 3.716-2.064 5.105-4.098 1.61-2.35 2.273-4.63 2.313-4.748-.05-.02-4.434-1.7-4.48-6.75M484.807 331.31c1.168-1.41 1.953-3.37 1.738-5.327-1.68.068-3.713 1.12-4.916 2.53-1.08 1.247-2.027 3.245-1.77 5.16 1.87.143 3.784-.95 4.947-2.362" style="fill-opacity: 1; stroke-opacity: 0; -webkit-transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; stroke-dasharray: 115.244583129883px, 115.244583129883px; stroke-dashoffset: 0px;"></path>        <path fill="#3C4650" stroke="#3C4650" stroke-width="2px" stroke-miterlimit="10" d="M727.488 315.527V45.982c0-8.828-6.597-15.982-14.738-15.982h-465.5c-8.14 0-14.74 7.155-14.74 15.982v269.545H727.49z" style="fill-opacity: 1; stroke-opacity: 0; -webkit-transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; stroke-dasharray: 1547.85571289063px, 1547.85571289063px; stroke-dashoffset: 0px;"></path>        <path fill="#141E28" stroke="#141E28" stroke-width="2px" stroke-miterlimit="10" d="M251.2 48.887h457.205v245.52H251.2z" style="fill-opacity: 1; stroke-opacity: 0; -webkit-transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; transition: fill-opacity 1s ease-in-out, stroke-opacity 1s ease-in-out; stroke-dasharray: 1405.44995117188px, 1405.44995117188px; stroke-dashoffset: 0px;"></path>      </svg>

上面的代码很复杂,如果说它们是代码的话,但是我们可以注意到,这种书写方式,有点类似于html,都是使用标签

使用最多的标签是path,也就是路径


有的人也会想到,要实现照片上的动态效果,我们可以使用Android自带的绘图类和函数,复杂的曲线路径,我们可以使用path这个类来制定

那会不会SVG里面的path,其实也是这样,那么我们就可以将SVG中的path,对应到android,然后绘制出来就好了。


SVG里面还有各种标签:

包括line直线,circle圆,rect矩形,eliipse椭圆,polygon多边形,等等


这些只要我们又一个SVG文件,都可以将其转换成java代码

作为一个程序员,我们当然不能手动去做这个工作,那就涉及两个问题,一个是SVG的解析,一个是解析后的绘制


幸运的是,已经有人完成了这个工作,并且在Github上开源 https://github.com/geftimov/android-pathview


这篇文章将作为一个简单的例子,来使用上面的开源控件

为了解析SVG,我们需要将一个androidsvg.jar包含进我们的lib


下面我们来看这个控件的简单使用,作为一个自定义控件,我们只需要在布局文件里面添加

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:background="#ff0000"    android:layout_width="fill_parent"    android:layout_height="fill_parent">    <com.example.kaiyicky.myapplication.PathView        xmlns:app="http://schemas.android.com/apk/res-auto"        android:id="@+id/pathView"        android:layout_width="match_parent"        android:layout_height="match_parent"        app:pathColor="@android:color/white"        app:svg="@raw/ironman_white"        app:pathWidth="5"/></LinearLayout>

其实app:svg指定了一个SVG文件,我们可以把这个文章放在raw目录下面


然后来看Activity里面:

public class MainActivity extends FragmentActivity {        @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        final PathView pathView = (PathView) findViewById(R.id.pathView);//        final Path path = makeConvexArrow(50, 100);//        pathView.setPath(path);        pathView.setFillAfter(true);        pathView.useNaturalColors();        pathView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                pathView.getPathAnimator().                        delay(100).                        duration(1500).                        interpolator(new AccelerateDecelerateInterpolator()).                        start();            }        });    }    private Path makeConvexArrow(float length, float height) {        final Path path = new Path();        path.moveTo(0.0f, 0.0f);        path.lineTo(length / 4f, 0.0f);        path.lineTo(length, height / 2.0f);        path.lineTo(length / 4f, height);        path.lineTo(0.0f, height);        path.lineTo(length * 3f / 4f, height / 2f);        path.lineTo(0.0f, 0.0f);        path.close();        return path;    }}

看到注释的部分,调用了makeConvexArraw()方法,如果我们没有在xml文件里面指定svg文件,我们也可以在代码中手动指定绘制的路径

让代码跑起来,点击屏幕,于是就实现了以下效果:



就是这么简单,至于这么制作SVG文件,大家可以找美工帮忙,使用ps和ai,可以将图片转换成SVG


最后是源码下载地址:http://download.csdn.net/detail/kangaroo835127729/9016349


整个过程有两个类,一个是SVG解析工具类:

/** * Util class to init and get paths from svg. */public class SvgUtils {    /**     * It is for logging purposes.     */    private static final String LOG_TAG = "SVGUtils";    /**     * All the paths with their attributes from the svg.     */    private final List<SvgPath> mPaths = new ArrayList<SvgPath>();    /**     * The paint provided from the view.     */    private final Paint mSourcePaint;    /**     * The init svg.     */    private SVG mSvg;    /**     * Init the SVGUtils with a paint for coloring.     *     * @param sourcePaint - the paint for the coloring.     */    public SvgUtils(final Paint sourcePaint) {        mSourcePaint = sourcePaint;    }    /**     * Loading the svg from the resources.     *     * @param context     Context object to get the resources.     * @param svgResource int resource id of the svg.     */    public void load(Context context, int svgResource) {        if (mSvg != null) return;        try {            mSvg = SVG.getFromResource(context, svgResource);            mSvg.setDocumentPreserveAspectRatio(PreserveAspectRatio.UNSCALED);        } catch (SVGParseException e) {            Log.e(LOG_TAG, "Could not load specified SVG resource", e);        }    }    /**     * Draw the svg to the canvas.     *     * @param canvas The canvas to be drawn.     * @param width  The width of the canvas.     * @param height The height of the canvas.     */    public void drawSvgAfter(final Canvas canvas, final int width, final int height) {        final float strokeWidth = mSourcePaint.getStrokeWidth();        rescaleCanvas(width, height, strokeWidth, canvas);    }    /**     * Render the svg to canvas and catch all the paths while rendering.     *     * @param width  - the width to scale down the view to,     * @param height - the height to scale down the view to,     * @return All the paths from the svg.     */    public List<SvgPath> getPathsForViewport(final int width, final int height) {        final float strokeWidth = mSourcePaint.getStrokeWidth();        Canvas canvas = new Canvas() {            private final Matrix mMatrix = new Matrix();            @Override            public int getWidth() {                return width;            }            @Override            public int getHeight() {                return height;            }            @Override            public void drawPath(Path path, Paint paint) {                Path dst = new Path();                //noinspection deprecation                getMatrix(mMatrix);                path.transform(mMatrix, dst);                paint.setAntiAlias(true);                paint.setStyle(Paint.Style.STROKE);                paint.setStrokeWidth(strokeWidth);                mPaths.add(new SvgPath(dst, paint));            }        };        rescaleCanvas(width, height, strokeWidth, canvas);        return mPaths;    }    /**     * Rescale the canvas with specific width and height.     *     * @param width       The width of the canvas.     * @param height      The height of the canvas.     * @param strokeWidth Width of the path to add to scaling.     * @param canvas      The canvas to be drawn.     */    private void rescaleCanvas(int width, int height, float strokeWidth, Canvas canvas) {        final RectF viewBox = mSvg.getDocumentViewBox();        final float scale = Math.min(width                        / (viewBox.width() + strokeWidth),                height / (viewBox.height() + strokeWidth));        canvas.translate((width - viewBox.width() * scale) / 2.0f,                (height - viewBox.height() * scale) / 2.0f);        canvas.scale(scale, scale);        mSvg.renderToCanvas(canvas);    }    /**     * Path with bounds for scalling , length and paint.     */    public static class SvgPath {        /**         * Region of the path.         */        private static final Region REGION = new Region();        /**         * This is done for clipping the bounds of the path.         */        private static final Region MAX_CLIP =                new Region(Integer.MIN_VALUE, Integer.MIN_VALUE,                        Integer.MAX_VALUE, Integer.MAX_VALUE);        /**         * The path itself.         */        final Path path;        /**         * The paint to be drawn later.         */        final Paint paint;        /**         * The length of the path.         */        final float length;        /**         * The bounds of the path.         */        final Rect bounds;        /**         * The measure of the path, we can use it later to get segment of it.         */        final PathMeasure measure;        /**         * Constructor to add the path and the paint.         *         * @param path  The path that comes from the rendered svg.         * @param paint The result paint.         */        SvgPath(Path path, Paint paint) {            this.path = path;            this.paint = paint;            measure = new PathMeasure(path, false);            this.length = measure.getLength();            REGION.setPath(path, MAX_CLIP);            bounds = REGION.getBounds();        }    }}

一个是SVG控件类:

/** * PathView is an View that animate paths. */public class PathView extends View {    /**     * Logging tag.     */    public static final String LOG_TAG = "PathView";    /**     * The paint for the path.     */    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);    /**     * Utils to catch the paths from the svg.     */    private final SvgUtils svgUtils = new SvgUtils(paint);    /**     * All the paths provided to the view. Both from Path and Svg.     */    private List<SvgUtils.SvgPath> paths = new ArrayList<SvgUtils.SvgPath>(0);    /**     * This is a lock before the view is redrawn     * or resided it must be synchronized with this object.     */    private final Object mSvgLock = new Object();    /**     * Thread for working with the object above.     */    private Thread mLoader;    /**     * The svg image from the raw directory.     */    private int svgResourceId;    /**     * Object that build the animation for the path.     */    private AnimatorBuilder animatorBuilder;    /**     * The progress of the drawing.     */    private float progress = 0f;    /**     * If the used colors are from the svg or from the set color.     */    private boolean naturalColors;    /**     * If the view is filled with its natural colors after path drawing.     */    private boolean fillAfter;    /**     * The width of the view.     */    private int width;    /**     * The height of the view.     */    private int height;    /**     * Default constructor.     *     * @param context The Context of the application.     */    public PathView(Context context) {        this(context, null);    }    /**     * Default constructor.     *     * @param context The Context of the application.     * @param attrs   attributes provided from the resources.     */    public PathView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    /**     * Default constructor.     *     * @param context  The Context of the application.     * @param attrs    attributes provided from the resources.     * @param defStyle Default style.     */    public PathView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        paint.setStyle(Paint.Style.STROKE);        getFromAttributes(context, attrs);    }    /**     * Get all the fields from the attributes .     *     * @param context The Context of the application.     * @param attrs   attributes provided from the resources.     */    private void getFromAttributes(Context context, AttributeSet attrs) {        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PathView);        try {            if (a != null) {                paint.setColor(a.getColor(R.styleable.PathView_pathColor, 0xff00ff00));                paint.setStrokeWidth(a.getFloat(R.styleable.PathView_pathWidth, 8.0f));                svgResourceId = a.getResourceId(R.styleable.PathView_svg, 0);            }        } finally {            if (a != null) {                a.recycle();            }        }    }    /**     * Set paths to be drawn and animated.     *     * @param paths - Paths that can be drawn.     */    public void setPaths(final List<Path> paths) {        for (Path path : paths) {            this.paths.add(new SvgUtils.SvgPath(path, paint));        }        synchronized (mSvgLock) {            updatePathsPhaseLocked();        }    }    /**     * Set path to be drawn and animated.     *     * @param path - Paths that can be drawn.     */    public void setPath(final Path path) {        paths.add(new SvgUtils.SvgPath(path, paint));        synchronized (mSvgLock) {            updatePathsPhaseLocked();        }    }    /**     * Animate this property. It is the percentage of the path that is drawn.     * It must be [0,1].     *     * @param percentage float the percentage of the path.     */    public void setPercentage(float percentage) {        if (percentage < 0.0f || percentage > 1.0f) {            throw new IllegalArgumentException("setPercentage not between 0.0f and 1.0f");        }        progress = percentage;        synchronized (mSvgLock) {            updatePathsPhaseLocked();        }        invalidate();    }    /**     * This refreshes the paths before draw and resize.     */    private void updatePathsPhaseLocked() {        final int count = paths.size();        for (int i = 0; i < count; i++) {            SvgUtils.SvgPath svgPath = paths.get(i);            svgPath.path.reset();            svgPath.measure.getSegment(0.0f, svgPath.length * progress, svgPath.path, true);            // Required only for Android 4.4 and earlier            svgPath.path.rLineTo(0.0f, 0.0f);        }    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        synchronized (mSvgLock) {            canvas.save();            canvas.translate(getPaddingLeft(), getPaddingTop());            final int count = paths.size();            for (int i = 0; i < count; i++) {                final SvgUtils.SvgPath svgPath = paths.get(i);                final Path path = svgPath.path;                final Paint paint1 = naturalColors ? svgPath.paint : paint;                canvas.drawPath(path, paint1);            }            fillAfter(canvas);            canvas.restore();        }    }    /**     * If there is svg , the user called setFillAfter(true) and the progress is finished.     *     * @param canvas Draw to this canvas.     */    private void fillAfter(final Canvas canvas) {        if (svgResourceId != 0 && fillAfter && progress == 1f) {            svgUtils.drawSvgAfter(canvas, width, height);        }    }    @Override    protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        if (mLoader != null) {            try {                mLoader.join();            } catch (InterruptedException e) {                Log.e(LOG_TAG, "Unexpected error", e);            }        }        if (svgResourceId != 0) {            mLoader = new Thread(new Runnable() {                @Override                public void run() {                    svgUtils.load(getContext(), svgResourceId);                    synchronized (mSvgLock) {                        width = w - getPaddingLeft() - getPaddingRight();                        height = h - getPaddingTop() - getPaddingBottom();                        paths = svgUtils.getPathsForViewport(width, height);                        updatePathsPhaseLocked();                    }                }            }, "SVG Loader");            mLoader.start();        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        if (svgResourceId != 0) {            int widthSize = MeasureSpec.getSize(widthMeasureSpec);            int heightSize = MeasureSpec.getSize(heightMeasureSpec);            setMeasuredDimension(widthSize, heightSize);            return;        }        int desiredWidth = 0;        int desiredHeight = 0;        final float strokeWidth = paint.getStrokeWidth() / 2;        for (SvgUtils.SvgPath path : paths) {            desiredWidth += path.bounds.left + path.bounds.width() + strokeWidth;            desiredHeight += path.bounds.top + path.bounds.height() + strokeWidth;        }        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int heightMode = MeasureSpec.getMode(widthMeasureSpec);        int measuredWidth, measuredHeight;        if (widthMode == MeasureSpec.AT_MOST) {            measuredWidth = desiredWidth;        } else {            measuredWidth = widthSize;        }        if (heightMode == MeasureSpec.AT_MOST) {            measuredHeight = desiredHeight;        } else {            measuredHeight = heightSize;        }        setMeasuredDimension(measuredWidth, measuredHeight);    }    /**     * If the real svg need to be drawn after the path animation.     *     * @param fillAfter - boolean if the view needs to be filled after path animation.     */    public void setFillAfter(final boolean fillAfter) {        this.fillAfter = fillAfter;    }    /**     * If you want to use the colors from the svg.     */    public void useNaturalColors() {        naturalColors = true;    }    /**     * Animator for the paths of the view.     *     * @return The AnimatorBuilder to build the animation.     */    public AnimatorBuilder getPathAnimator() {        if (animatorBuilder == null) {            animatorBuilder = new AnimatorBuilder(this);        }        return animatorBuilder;    }    /**     * Get the path color.     *     * @return The color of the paint.     */    public int getPathColor() {        return paint.getColor();    }    /**     * Set the path color.     *     * @param color -The color to set to the paint.     */    public void setPathColor(final int color) {        paint.setColor(color);    }    /**     * Get the path width.     *     * @return The width of the paint.     */    public float getPathWidth() {        return paint.getStrokeWidth();    }    /**     * Set the path width.     *     * @param width - The width of the path.     */    public void setPathWidth(final float width) {        paint.setStrokeWidth(width);    }    /**     * Get the svg resource id.     *     * @return The svg raw resource id.     */    public int getSvgResource() {        return svgResourceId;    }    /**     * Set the svg resource id.     *     * @param svgResource - The resource id of the raw svg.     */    public void setSvgResource(int svgResource) {        svgResourceId = svgResource;    }    /**     * Object for building the animation of the path of this view.     */    public static class AnimatorBuilder {        /**         * Duration of the animation.         */        private int duration = 350;        /**         * Interpolator for the time of the animation.         */        private Interpolator interpolator;        /**         * The delay before the animation.         */        private int delay = 0;        /**         * ObjectAnimator that constructs the animation.         */        private final ObjectAnimator anim;        /**         * Listener called before the animation.         */        private ListenerStart listenerStart;        /**         * Listener after the animation.         */        private ListenerEnd animationEnd;        /**         * Animation listener.         */        private PathViewAnimatorListener pathViewAnimatorListener;        /**         * Default constructor.         *         * @param pathView The view that must be animated.         */        public AnimatorBuilder(final PathView pathView) {            anim = ObjectAnimator.ofFloat(pathView, "percentage", 0.0f, 1.0f);        }        /**         * Set the duration of the animation.         *         * @param duration - The duration of the animation.         * @return AnimatorBuilder.         */        public AnimatorBuilder duration(final int duration) {            this.duration = duration;            return this;        }        /**         * Set the Interpolator.         *         * @param interpolator - Interpolator.         * @return AnimatorBuilder.         */        public AnimatorBuilder interpolator(final Interpolator interpolator) {            this.interpolator = interpolator;            return this;        }        /**         * The delay before the animation.         *         * @param delay - int the delay         * @return AnimatorBuilder.         */        public AnimatorBuilder delay(final int delay) {            this.delay = delay;            return this;        }        /**         * Set a listener before the start of the animation.         *         * @param listenerStart an interface called before the animation         * @return AnimatorBuilder.         */        public AnimatorBuilder listenerStart(final ListenerStart listenerStart) {            this.listenerStart = listenerStart;            if (pathViewAnimatorListener == null) {                pathViewAnimatorListener = new PathViewAnimatorListener();                anim.addListener(pathViewAnimatorListener);            }            return this;        }        /**         * Set a listener after of the animation.         *         * @param animationEnd an interface called after the animation         * @return AnimatorBuilder.         */        public AnimatorBuilder listenerEnd(final ListenerEnd animationEnd) {            this.animationEnd = animationEnd;            if (pathViewAnimatorListener == null) {                pathViewAnimatorListener = new PathViewAnimatorListener();                anim.addListener(pathViewAnimatorListener);            }            return this;        }        /**         * Starts the animation.         */        public void start() {            anim.setDuration(duration);            anim.setInterpolator(interpolator);            anim.setStartDelay(delay);            anim.start();        }        /**         * Animation listener to be able to provide callbacks for the caller.         */        private class PathViewAnimatorListener implements Animator.AnimatorListener {            @Override            public void onAnimationStart(Animator animation) {                if (listenerStart != null) listenerStart.onAnimationStart();            }            @Override            public void onAnimationEnd(Animator animation) {                if (animationEnd != null) animationEnd.onAnimationEnd();            }            @Override            public void onAnimationCancel(Animator animation) {            }            @Override            public void onAnimationRepeat(Animator animation) {            }        }        /**         * Called when the animation start.         */        public interface ListenerStart {            /**             * Called when the path animation start.             */            void onAnimationStart();        }        /**         * Called when the animation end.         */        public interface ListenerEnd {            /**             * Called when the path animation end.             */            void onAnimationEnd();        }    }}


11 0