目錄php
1、前言java
2、SVG小課堂android
3、簡單使用git
4、實戰github
5、寫在最後canvas
SVG 在安卓5.0被引入,由於其放大後不會模糊的優秀表現,被使用也是愈來愈多。今天小盆友也來談談這個優秀的SVG,同時分享一些我的比較喜歡的知識小點。老規矩,先上實戰圖。數組
"手寫"掘金 微信
SVG 全稱 Scalable Vector Graphics ,翻譯一下即爲 可縮放的矢量圖形。ide
SVG 的優勢不少,並且在不一樣的場景優勢也會有所不一樣,小盆友以爲 SVG 給我帶來的優勢以下幾點svg
這個缺點,說的並非SVG的缺點,而是在 Android 中使用SVG的缺點或侷限。
前言中提到 SVG 是在5.0以後引入,雖然做爲一個圖標資源並不會有兼容問題。
可是若是對 SVG 進行使用動畫時,則須要進行兼容性處理。否在 5.0 如下會閃退,畢竟 4.4 的佔有率還 10.3%左右(以下圖,圖片來自 Android Studio 的統計)。
至於如何使用和兼容,咱們在下一小節進行說明。
動畫限制這一點其實準確來講,不屬於缺點,小盆友認爲是不夠靈活。
由於SVG的動畫是經過屬性動畫進行執行的,咱們知道屬性動畫最終是反射調用到類的 setXxx(Xxx就是咱們設置的屬性名稱),因此若是該類沒有對應的方法則是沒有做用的。
對 「屬性動畫」 源碼興趣的童鞋能夠移步小盆友的另外一篇博文,帶有活力的屬性動畫源碼分析與實戰。
接下來的一個問題就是,屬性動畫反射回調的類是哪一個類呢?這裏有兩種狀況,一種是針對 Group 標籤,一種是針對 Path 標籤。但在說明具體具體類以前,咱們有必要說明 Group 和 Path 標籤的層級關係。
以下圖所示,葉子節點只能爲Path標籤,而 Group標籤用於裝載Path標籤或Group標籤。值得一提的是 Vector 能夠直接包含一個或多個Path, 而不必定須要包含Group。
Path標籤對應的是 VectorDrawableCompat$VFullPath,而 VectorDrawableCompat$VFullPath 繼承於 VectorDrawableCompat$VPath,這兩個類的內部方法以下,一樣用紅框圈出 set 開頭的方法,因此咱們經過屬性動畫對Path標籤進行控制的只能這幾個屬性。
咱們先來闡述如何將SVG常規使用起來。但在這以前咱們須要說明一下 SVG 中繪製 Path 的語法。
path 的 pathData屬性內裝載的就是路徑數據,其語法以下
M = moveto(M X,Y) :將畫筆移動到指定的座標位置
L = lineto(L X,Y) :畫直線到指定的座標位置
H = horizontal lineto(H X):畫水平線到指定的X座標位置 V = vertical lineto(V Y):畫垂直線到指定的Y座標位置 C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三階貝賽曲線
S = smooth curveto(S X2,Y2,ENDX,ENDY):三階貝賽曲線 Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二階貝賽曲線 T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射 A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧線 Z = closepath():關閉路徑
複製代碼
小盆友我的認爲,這些語法做爲一個瞭解便可,並不須要記憶,由於 SVG 的資源文件通常不須要咱們程序猿自行繪製,只是偶爾須要修改一下,因此要求並非很高。
如今有不少在線編輯SVG工具,能夠經過繪製後,將路徑數據拷貝下來稍做修改,即可使用。
「手寫」掘金 的 SVG資源就是小盆友從掘金官網獲取後,進行一些簡單的修改,因此只須要了解,須要修改時會運用就行。
在 Android 中的常使用的模版爲
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="xxdp" android:height="yydp" android:viewportWidth="xx" android:viewportHeight="yy">
<group>
<path android:fillColor="#006CFF" android:pathData="xxxx" />
....more path or group
</group>
....more path or group
</vector>
複製代碼
在 vector 標籤中的 android:width
和 android:height
表示的是 SVG的大小,而 android:viewportWidth
和 android:viewportHeight
表示的是將 android:width
和 android:height
劃分紅多少個等份,隨後的 Group 和 Path 的座標則是基於這一比例進行編寫。
group 和 path 咱們在前面已經提過了,就再也不贅述。
咱們舉個簡單的例子,用 SVG畫出 以下圖形,並將其使用
// ic_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:name="top"
android:pathData="
M 20,20
L 50,20 80,20"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
<path
android:name="middle"
android:pathData="
M 20,50
L 50,50 80,50"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
<path
android:name="bottom"
android:pathData="
M 20,80
L 50,80 80,80"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
</vector>
複製代碼
使用其實和普通的圖片資源同樣,ic_menu資源 即是咱們的 SVG 圖形
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="10dp"
android:src="@drawable/ic_menu" />
複製代碼
這裏不存在兼容問題,小盆友在4.4的機子上也有測試過。
SVG 的動畫是比較有趣的,但咱們在 「動畫限制問題」 小節中提到,存在着兼容問題,5.0以前的版本不能使用SVG動畫。
因此咱們須要新建一個 drawable-anydpi-v21
文件夾,來存放咱們的動畫資源,具體存放結構和代碼以下
animated-vector
起着
扣接 SVG靜態資源 和 屬性動畫 的做用。
// menu.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_menu">
<target
android:name="top"
android:animation="@animator/top_anim" />
<target
android:name="bottom"
android:animation="@animator/bottom_anim" />
</animated-vector>
// top_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:propertyName="pathData"
android:valueFrom="
M 20,20
L 50,20 80,20"
android:valueTo="
M 20,50
L 50,20 50,20"
android:valueType="pathType" />
// bottom_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:propertyName="pathData"
android:valueFrom="
M 20,80
L 50,80 80,80"
android:valueTo="
M 20,50
L 50,80 50,80"
android:valueType="pathType" />
複製代碼
值得一提的是,這裏的 pathData 最終就是調用了 VectorDrawableCompat$VPath 中的 setPathData,而參數類型便爲 pathType。忘記的童鞋能夠回 「動畫限制問題」 小節查看下。
若是隻是把咱們這裏使用的 menu資源放在 drawable-anydpi-v21
文件夾下,運行於 4.4的機子時,會報找不到相應資源的錯誤。因此咱們須要在 drawable
文件夾下,建一個相同名字的資源 menu資源,只是裏面的內容不是 animated-vector
做爲根標籤,而是使用和 ic_menu資源 徹底同樣的內容。
最終在代碼中進行兼容處理 5.0以後的版本開啓動畫,以前的版本切換圖片資源
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
((Animatable) img1.getDrawable()).start();
} else {
img1.setImageDrawable(
ContextCompat.getDrawable(SvgUseActivity.this, R.drawable.ic_back));
}
複製代碼
5.0以後版本的效果以下。5.0以前版本就只是簡單圖片切換,就不上圖了:
上一小節咱們知道,對 SVG 添加動畫,簡單方便,可是也說明了使用系統自帶的這一套操做沒法實現較爲複雜的交互,因此咱們只能本身動手,才能豐衣足食了。
還記得小盆友在介紹優勢時,說到SVG的格式是XML,這就是咱們本身動手的切入點。由於格式爲XML,因此能夠自行解析,拿取其中的pathData數據轉爲Path路徑,接下來就能夠作不少有趣的事情。咱們融入到實戰中來體會這一趣事。
效果圖
編碼思路
(1)解析 SVG 文件 首先須要將 「掘金」這一SVG進行XML解析,咱們藉助 DocumentBuilderFactory
類,爲咱們解析獲取一棵DOM樹。
// 從 XML文檔 生成 DOM對象樹
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document document = null;
try {
document = factory.newDocumentBuilder().parse(inputStream);
} catch (SAXException |
IOException |
ParserConfigurationException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
(2)獲取並保存Path的數據 在上一步中獲取到DOM樹以後,進行遍歷DOM節點獲取到 Path 數據,保存其填充的顏色和將 pathData 的數據翻譯成 Path對象進行保存起來。
這裏須要藉助 PathParser
類將 pathData
的數據翻譯成 Path對象 ,可是PathParser類 被打上了註解 @hide,咱們沒法直接使用,因此只能是將其拷貝一份放置咱們的目錄下來使用。具體核心代碼以下
// 遍歷全部的 Path 節點
for (int i = 0; i < pathNodeList.getLength(); ++i) {
Element pathNode = (Element) pathNodeList.item(i);
// path 的 svg 路徑
String pathData = pathNode.getAttribute(PATH_DATA);
// path 的 顏色
String colorData = pathNode.getAttribute(FILL_COLOR);
// 解析 path
Path path = null;
try {
path = PathParser.createPathFromPathData(pathData);
} catch (Exception e) {
e.printStackTrace();
}
// path 解析出錯,退出
if (path == null) {
mHandle.sendEmptyMessage(InnerHandler.ERROR);
return;
}
int color = Color.parseColor(colorData);
path.computeBounds(rect, true);
left = left == -1 ? rect.left : Math.min(left, rect.left);
right = right == -1 ? rect.right : Math.max(right, rect.right);
top = top == -1 ? rect.top : Math.min(top, rect.top);
bottom = bottom == -1 ? rect.bottom : Math.max(bottom, rect.bottom);
PathData item = new PathData();
item.path = path;
item.color = color;
pathDataList.add(item);
}
複製代碼
(3)進行縮放 根據 SVG圖像大小 和 畫布大小,進行偏移和縮放,讓SVG圖像大小合適且居中顯示於畫布中。核心代碼以下
float mScale = calculateScale(mSvgRect.width(), mSvgRect.height(), getWidth(), getHeight());
// 移至中心
mCanvasMatrix.preTranslate(getWidth() / 2, getHeight() / 2);
mCanvasMatrix.preTranslate(-mSvgRect.width() / 2, -mSvgRect.height() / 2);
mCanvasMatrix.preScale(
mScale,
mScale,
mSvgRect.width() / 2,
mSvgRect.height() / 2);
canvas.setMatrix(mCanvasMatrix);
複製代碼
(4)藉助 PathMeasure 和 屬性動畫,讓其進行勾勒後填充 屬性動畫開啓後,每次刷新都經過 PathMeasure 對當前須要勾勒的Path進行裁剪繪製,達到一步步勾勒的效果。核心代碼以下
PathData pathData = mPathDataList.get(index);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(pathData.color);
mPaint.setStrokeWidth(mLineWidth / mScale);
mPathMeasure.setPath(pathData.path, false);
mPathMeasure.getSegment(0,
mPathMeasure.getLength() * process,
mAnimPath,
true);
canvas.drawPath(mAnimPath, mPaint);
複製代碼
PathMeasure的使用,能夠查看小盆友的另外一篇博文:PathMeasure的API講解與實戰
效果圖
Github入口:傳送門
編碼思路
(1)解析SVG數據 與「手寫」掘金的事例同樣,第一步也是解析數據,經過 PathParser
類將svg的數據轉爲Path對象,而顏色填充則由咱們設置的數組決定。
同時還要保存好svg圖像的大小,具體核心代碼以下:
// 用於記錄整個 svg 的實際大小
float left = -1;
float top = -1;
float right = -1;
float bottom = -1;
// 計算出 path 的 rect
RectF rect = new RectF();
// 遍歷全部的 Path 節點
for (int i = 0; i < pathNodeList.getLength(); ++i) {
Element pathNode = (Element) pathNodeList.item(i);
// path 的 svg 路徑
String pathData = pathNode.getAttribute(DATA);
// path 的 title
String title = pathNode.getAttribute(TITLE);
// 省略一些代碼
path.computeBounds(rect, true);
left = left == -1 ? rect.left : Math.min(left, rect.left);
right = right == -1 ? rect.right : Math.max(right, rect.right);
top = top == -1 ? rect.top : Math.min(top, rect.top);
bottom = bottom == -1 ? rect.bottom : Math.max(bottom, rect.bottom);
ItemData itemData = new ItemData(path,
ContextCompat.getColor(getContext(), mMapColor[i % colorSize]),
title);
mapDataList.add(itemData);
}
mSvgRect.left = left;
mSvgRect.top = top;
mSvgRect.right = right;
mSvgRect.bottom = bottom;
複製代碼
(2)縮放地圖至View中心 根據畫布的大小 和 svg的大小,將咱們的畫布進行偏移和縮放,使咱們的地圖大小合適且居中放置(這裏藉助了矩陣,但最終會將該矩陣做用於咱們的畫布)
// 移至畫布中心
mCanvasMatrix.preTranslate(getWidth() / 2, getHeight() / 2);
// 移外邊
float lastLeftMargin = mLastRectF.left - mSvgRect.left;
float lastTopMargin = mLastRectF.top - mSvgRect.top;
mCanvasMatrix.preTranslate(-lastLeftMargin, -lastTopMargin);
// 移至中心
mCanvasMatrix.preTranslate(-mLastRectF.width() / 2, -mLastRectF.height() / 2);
// 進行縮放
if (!mLastRectF.isEmpty()) {
mScale = calculateScale(
mLastRectF.width(),
mLastRectF.height(),
getWidth(),
getHeight());
}
mCanvasMatrix.preScale(
mScale,
mScale,
lastLeftMargin + mLastRectF.width() / 2,
lastTopMargin + mLastRectF.height() / 2);
複製代碼
(3)如何交互 至此咱們的地圖就已經能正常顯示了,但還須要交互。交互最主要的問題是咱們如何知道選中的是哪塊區域。具體經過一下代碼進行判斷,即可知道咱們是否觸碰了 該Path所包含的區域
/** * 是否在觸碰的範圍內 * * @param item 地圖的每一個數據項 * @param x 觸碰點的x軸 * @param y 觸碰點的y軸 * @return true:在範圍內;false:在範圍外 */
private boolean isTouch(ItemData item, float x, float y) {
item.path.computeBounds(mTouchRectF, true);
mTouchRegion.setPath(
item.path,
new Region((int) mTouchRectF.left,
(int) mTouchRectF.top,
(int) mTouchRectF.right,
(int) mTouchRectF.bottom)
);
return mTouchRegion.contains((int) x, (int) y);
}
複製代碼
(4)剩餘操做 得到了點擊的區域,如何進行動畫的過渡就是計算邏輯問題了。小盆友這裏就再也不展開講這塊的邏輯。這裏用一句話歸納,就是經過比較 上一次選中的Path區域 和 此次選中的Path區域 進行 中心座標偏移和縮放。
SVG 也是一把利器,揮舞得當可讓本身的App展示出別人所想不到的交互效果,但願這篇文章能讓你體會到不同的SVG。若是你有所收穫就給我一個贊❤️並關注我吧,若是發現有那些欠妥的地方,請留言區與我討論,咱們共同進步。
高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄
歡迎加我微信,咱們能夠進行更多更有趣的交流