SVG技術(shù)入門:線條動(dòng)畫實(shí)現(xiàn)原理
這是一個(gè)有點(diǎn)神奇的技術(shù):一副線條構(gòu)成的畫能自動(dòng)畫出自己,非常的酷源哩。SVG 意為可縮放矢量圖形(Scalable Vector Graphics)捉偏,是使用 XML 來(lái)描述二維圖形和繪圖程序的語(yǔ)言,可以任意放大圖形顯示立叛,但絕不會(huì)以犧牲圖像質(zhì)量為代價(jià);可在svg圖像中保留可編輯和可搜尋的狀態(tài);平均來(lái)講,svg文件比其它格式的圖像文件要小很多贡茅,因而下載也很快秘蛇。可以提前看兩個(gè)動(dòng)畫效果感受一下
登錄效果.gif
搜索效果.gif
1.用 SVG 的優(yōu)勢(shì):
1.SVG 可被非常多的工具讀取和修改(比如記事本),由于使用xml格式定義顶考,所以可以直接被當(dāng)作文本文件打開(kāi)赁还,看里面的數(shù)據(jù);
2.SVG 與 JPEG 和 GIF 圖像比起來(lái)驹沿,尺寸更小艘策,且可壓縮性更強(qiáng),SVG 圖就相當(dāng)于保存了關(guān)鍵的數(shù)據(jù)點(diǎn)渊季,比如要顯示一個(gè)圓朋蔫,需要知道圓心和半徑,那么SVG 就只保存圓心坐標(biāo)和半徑數(shù)據(jù)却汉,而平常我們用的位圖都是以像素點(diǎn)的形式根據(jù)圖片大小保存對(duì)應(yīng)個(gè)數(shù)的像素點(diǎn)驯妄,因而SVG尺寸更小合砂;
3.SVG 是可伸縮的青扔,平常使用的位圖拉伸會(huì)發(fā)虛,壓縮會(huì)變形,而SVG格式圖片保存數(shù)據(jù)進(jìn)行運(yùn)算展示微猖,不管多大多少谈息,可以不失真顯示;
4.SVG 圖像可在任何的分辨率下被高質(zhì)量地打印;
5.SVG 可在圖像質(zhì)量不下降的情況下被放大;
6.SVG 圖像中的文本是可選的励两,同時(shí)也是可搜索的(很適合制作地圖);
7.SVG 可以與 Java 技術(shù)一起運(yùn)行;
8.SVG 是開(kāi)放的標(biāo)準(zhǔn);
9.SVG 文件是純粹的 XML;
2.SVG的使用
既然SVG是公認(rèn)的xml文件格式定義的黎茎,那么我們則可以通過(guò)解析xml文件拿到對(duì)應(yīng)SVG圖的所有數(shù)據(jù)
2.1 添加一個(gè)svg文件
app:svg指定了一個(gè)SVG文件,這個(gè)文件放在raw目錄下面:
path 類型的SVG 數(shù)據(jù):
代碼看上去很復(fù)雜当悔,如果說(shuō)它們是代碼的話傅瞻,但是我們可以注意到,這種書寫方式盲憎,有點(diǎn)類似于html嗅骄,都是使用標(biāo)簽使用最多的標(biāo)簽是path,也就是路徑
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="M250 150 L150 350 L350 350 Z" />
</svg>
SVG 里關(guān)于path 有哪些指令:
M = moveto 相當(dāng)于 android Path 里的moveTo(),用于移動(dòng)起始點(diǎn)
L = lineto 相當(dāng)于 android Path 里的lineTo()饼疙,用于畫線
H = horizontal lineto 用于畫水平線
V = vertical lineto 用于畫豎直線
C = curveto 相當(dāng)于cubicTo(),三次貝塞爾曲線
S = smooth curveto 同樣三次貝塞爾曲線溺森,更平滑
Q = quadratic Belzier curve quadTo(),二次貝塞爾曲線
T = smooth quadratic Belzier curveto 同樣二次貝塞爾曲線窑眯,更平滑
A = elliptical Arc 相當(dāng)于arcTo()屏积,用于畫弧
Z = closepath 相當(dāng)于closeTo(),關(guān)閉path
SVG里還定義了一些基本的圖形和效果:(介紹和使用可以查看W3School)
我們根據(jù)最后要做的效果,利用PS等作圖軟件設(shè)計(jì)制作出想要的圖形磅甩,然后使用矢量圖軟件導(dǎo)出圖片的SVG數(shù)據(jù)炊林,也可以在svg文件里通過(guò)代碼來(lái)實(shí)現(xiàn)自己想要的動(dòng)畫效果查看svg文件編寫
2.2 SVG的解析和解析后的繪制
已經(jīng)有人完成了這個(gè)工作,在Github上有開(kāi)源控件 可以使用
在布局文件里面添加自定義控件
<?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">
<org.curiouscreature.android.roadtrip.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>
activity中繪制
final PathView pathView = (PathView) findViewById(R.id.pathView);
pathView.getPathAnimator().
delay(100).
duration(1500).
interpolator(new AccelerateDecelerateInterpolator()).
start();
3.解析源碼了解
3.1 繪制過(guò)程所需要的兩個(gè)類
SVG解析工具類SvgUtils主要代碼:
/**
* Loading the svg from the resources.
* 從資源中加載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.
* 繪制SVG畫布
*/
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.
* 渲染SVG畫布卷要,捕捉所有的路徑進(jìn)行渲染
*/
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.
* 繪制到畫布
*/
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);
}
SVG控件類PathView 主要代碼:
@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();
}
}
@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);
}
3.2 androidsvg 插件對(duì)svg 指令的解析
查看androidsvg的源碼了解他的解析渣聚,查看svg解析器SVGParser類可以看到他的解析,對(duì)svg文件里的指令采用Android Path 里的對(duì)應(yīng)方法進(jìn)行置換 僧叉,如:使用parsePath(String val)方法解析繪圖路徑奕枝,源碼中是使用ascii碼來(lái)對(duì)比,內(nèi)容可以看成如下
public Path parsePath(String s) throws ParseException {
mCurrentPoint.set(Float.NaN, Float.NaN);
mPathString = s;
mIndex = 0;
mLength = mPathString.length();
PointF tempPoint1 = new PointF();
PointF tempPoint2 = new PointF();
PointF tempPoint3 = new PointF();
Path p = new Path();
p.setFillType(Path.FillType.WINDING);
boolean firstMove = true;
while (mIndex < mLength) {
char command = consumeCommand();
boolean relative = (mCurrentToken == TOKEN_RELATIVE_COMMAND);
switch (command) {
case 'M':
case 'm': {
// m指令瓶堕,相當(dāng)于android 里的 moveTo()
boolean firstPoint = true;
while (advanceToNextToken() == TOKEN_VALUE) {
consumeAndTransformPoint(tempPoint1,
relative && mCurrentPoint.x != Float.NaN);
if (firstPoint) {
p.moveTo(tempPoint1.x, tempPoint1.y);
firstPoint = false;
if (firstMove) {
mCurrentPoint.set(tempPoint1);
firstMove = false;
}
} else {
p.lineTo(tempPoint1.x, tempPoint1.y);
}
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'C':
case 'c': {
// c指令隘道,相當(dāng)于android 里的 cubicTo()
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
consumeAndTransformPoint(tempPoint1, relative);
consumeAndTransformPoint(tempPoint2, relative);
consumeAndTransformPoint(tempPoint3, relative);
p.cubicTo(tempPoint1.x, tempPoint1.y, tempPoint2.x, tempPoint2.y,
tempPoint3.x, tempPoint3.y);
}
mCurrentPoint.set(tempPoint3);
break;
}
case 'L':
case 'l': {
// 相當(dāng)于lineTo()進(jìn)行畫直線
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
consumeAndTransformPoint(tempPoint1, relative);
p.lineTo(tempPoint1.x, tempPoint1.y);
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'H':
case 'h': {
// 畫水平直線
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
float x = transformX(consumeValue());
if (relative) {
x += mCurrentPoint.x;
}
p.lineTo(x, mCurrentPoint.y);
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'V':
case 'v': {
// 畫豎直直線
if (mCurrentPoint.x == Float.NaN) {
throw new ParseException("Relative commands require current point", mIndex);
}
while (advanceToNextToken() == TOKEN_VALUE) {
float y = transformY(consumeValue());
if (relative) {
y += mCurrentPoint.y;
}
p.lineTo(mCurrentPoint.x, y);
}
mCurrentPoint.set(tempPoint1);
break;
}
case 'Z':
case 'z': {
// 封閉path
p.close();
break;
}
}
}
return p;
}