目錄
一、前言
二、SVG小課堂
三、簡單使用
四、實戰
五、寫在最後
一、前言
SVG 在安卓5.0被引入,因為其放大後不會模糊的優秀表現,被使用也是越來越多。今天小盆友也來談談這個優秀的SVG,同時分享一些個人比較喜歡的知識小點。老規矩,先上實戰圖。
"手寫"掘金
地圖查閱器二、SVG小課堂
1、SVG是什麼
SVG 全稱 Scalable Vector Graphics ,翻譯一下即為 可縮放的向量圖形。
2、優點
SVG 的優點很多,而且在不同的場景優點也會有所不同,小盆友覺得 SVG 給我帶來的優點如下幾點
- 縮放均不失真,這帶來的好處是我們不需要多套解析度的圖示;
- 檔案相對較小,相較於 JPEG 和 GIF 格式的檔案會小些;
- 以 XML 為結構,可修改也可擴充套件,用最簡單的記事本也能進行相應的修改;
- 高互動性,這一點在實戰時就能體現出來啦;
3、缺點
這個缺點,說的並不是SVG的缺點,而是在 Android 中使用SVG的缺點或侷限。
(1) 動畫相容問題
前言中提到 SVG 是在5.0之後引入,雖然作為一個圖示資源並不會有相容問題。
但是如果對 SVG 進行使用動畫時,則需要進行相容性處理。否在 5.0 以下會閃退,畢竟 4.4 的佔有率還 10.3%左右(如下圖,圖片來自 Android Studio 的統計)。
至於如何使用和相容,我們在下一小節進行說明。
2、動畫限制問題
動畫限制這一點其實準確來說,不屬於缺點,小盆友認為是不夠靈活。
因為SVG的動畫是通過屬性動畫進行執行的,我們知道屬性動畫最終是反射呼叫到類的 setXxx(Xxx就是我們設定的屬性名稱),所以如果該類沒有對應的方法則是沒有作用的。
對 “屬性動畫” 原始碼興趣的童鞋可以移步小盆友的另一篇博文,帶有活力的屬性動畫原始碼分析與實戰。
接下來的一個問題就是,屬性動畫反射回撥的類是哪個類呢?這裡有兩種情況,一種是針對 Group 標籤,一種是針對 Path 標籤。但在說明具體具體類之前,我們有必要說明 Group 和 Path 標籤的層級關係。
如下圖所示,葉子節點只能為Path標籤,而 Group標籤用於裝載Path標籤或Group標籤。值得一提的是 Vector 可以直接包含一個或多個Path, 而不一定需要包含Group。
接著我們來說說他們各自的具體反射類,Group標籤 對應的是 VectorDrawableCompat$VGroup 類,其類的內部方法如下,帶 set 開頭的方法,已經用紅框圈出,這代表著我們為Group標籤設定的屬性動畫所作用的屬性就只能侷限於這幾個方法中。Path標籤對應的是 VectorDrawableCompat$VFullPath,而 VectorDrawableCompat$VFullPath 繼承於 VectorDrawableCompat$VPath,這兩個類的內部方法如下,同樣用紅框圈出 set 開頭的方法,所以我們通過屬性動畫對Path標籤進行控制的只能這幾個屬性。
小結一下,這些方法能滿足我們一些簡單的動畫,但是設計師來了一個較為騷氣的互動,這時我們比較尷尬了,因為我們沒法進行擴充套件,沒法設定我們自己想要的動畫邏輯。三、簡單使用
我們先來闡述如何將SVG常規使用起來。但在這之前我們需要說明一下 SVG 中繪製 Path 的語法。
1、繪製語法
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資源就是小盆友從掘金官網獲取後,進行一些簡單的修改,所以只需要瞭解,需要修改時會運用就行。
2、作為靜態圖片資源
在 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畫出 如下圖形,並將其使用
具體的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的機子上也有測試過。
3、作為動態圖片資源
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、"手寫"掘金
效果圖
Github入口:傳送門編碼思路
(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講解與實戰
2、地圖查閱器
效果圖
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吧?
歡迎加我微信,我們可以進行更多更有趣的交流