溫馨提示
請拖動到文章末尾,長按識別「抽獎」小程序。現(xiàn)金紅包等你來拿。
作者:猛猛的小盆友
鏈接:
https://juejin.im/post/5ca9f65e6fb9a05e472b9cab
本文由作者授權(quán)發(fā)布。
五一很快就結(jié)束了,大家五一都去哪里玩了?馬上就要回到工作崗位上,收拾好心情,認(rèn)真的投入到一周忙碌的工作中。
目錄
一、前言
二、SVG小課堂
三、簡單使用
四、實戰(zhàn)
五、寫在最后
一、前言
SVG 在安卓5.0被引入,因為其放大后不會模糊的優(yōu)秀表現(xiàn),被使用也是越來越多。今天小盆友也來談?wù)勥@個優(yōu)秀的SVG,同時分享一些個人比較喜歡的知識小點。老規(guī)矩,先上實戰(zhàn)圖。
"手寫"掘金
地圖查閱器
二、SVG小課堂
1、SVG是什么
SVG 全稱?Scalable Vector Graphics?,翻譯一下即為?可縮放的矢量圖形。
2、優(yōu)點
SVG 的優(yōu)點很多,而且在不同的場景優(yōu)點也會有所不同,小盆友覺得 SVG 給我?guī)淼膬?yōu)點如下幾點
縮放均不失真,這帶來的好處是我們不需要多套分辨率的圖標(biāo);
文件相對較小,相較于 JPEG 和 GIF 格式的文件會小些;
以 XML 為結(jié)構(gòu),可修改也可擴(kuò)展,用最簡單的記事本也能進(jìn)行相應(yīng)的修改;
高交互性,這一點在實戰(zhàn)時就能體現(xiàn)出來啦;
3、缺點
這個缺點,說的并不是SVG的缺點,而是在 Android 中使用SVG的缺點或局限。
(1) 動畫兼容問題
前言中提到?SVG 是在5.0之后引入,雖然作為一個圖標(biāo)資源并不會有兼容問題。
但是如果對 SVG 進(jìn)行使用動畫時,則需要進(jìn)行兼容性處理。否在 5.0 以下會閃退,畢竟 4.4 的占有率還 10.3%左右(如下圖,圖片來自 Android Studio 的統(tǒng)計)。
至于如何使用和兼容,我們在下一小節(jié)進(jìn)行說明。
2、動畫限制問題
動畫限制這一點其實準(zhǔn)確來說,不屬于缺點,小盆友認(rèn)為是不夠靈活。
因為SVG的動畫是通過屬性動畫進(jìn)行執(zhí)行的,我們知道屬性動畫最終是反射調(diào)用到類的 setXxx(Xxx就是我們設(shè)置的屬性名稱),所以如果該類沒有對應(yīng)的方法則是沒有作用的。
對 “屬性動畫” 源碼興趣的童鞋可以移步小盆友的另一篇博文,帶有活力的屬性動畫源碼分析與實戰(zhàn)。
接下來的一個問題就是,屬性動畫反射回調(diào)的類是哪個類呢?這里有兩種情況,一種是針對 Group 標(biāo)簽,一種是針對 Path 標(biāo)簽。
但在說明具體具體類之前,我們有必要說明 Group 和 Path 標(biāo)簽的層級關(guān)系。
如下圖所示,葉子節(jié)點只能為Path標(biāo)簽,而?Group標(biāo)簽用于裝載Path標(biāo)簽或Group標(biāo)簽。值得一提的是 Vector 可以直接包含一個或多個Path, 而不一定需要包含Group。
接著我們來說說他們各自的具體反射類,Group標(biāo)簽 對應(yīng)的是:
VectorDrawableCompat$VGroup?// 類
其類的內(nèi)部方法如下,帶 set 開頭的方法,已經(jīng)用紅框圈出,這代表著我們?yōu)镚roup標(biāo)簽設(shè)置的屬性動畫所作用的屬性就只能局限于這幾個方法中。
Path標(biāo)簽對應(yīng)的是:
VectorDrawableCompat$VFullPath
繼承于 VectorDrawableCompat$VPath,這兩個類的內(nèi)部方法如下,同樣用紅框圈出 set 開頭的方法,所以我們通過屬性動畫對Path標(biāo)簽進(jìn)行控制的只能這幾個屬性。
小結(jié)一下,這些方法能滿足我們一些簡單的動畫,但是設(shè)計師來了一個較為騷氣的交互,這時我們比較尷尬了,因為我們沒法進(jìn)行擴(kuò)展,沒法設(shè)置我們自己想要的動畫邏輯。
三、簡單使用
我們先來闡述如何將SVG常規(guī)使用起來。但在這之前我們需要說明一下 ?SVG 中繪制 Path 的語法。
1、繪制語法
path 的 pathData屬性內(nèi)裝載的就是路徑數(shù)據(jù),其語法如下:
M = moveto(M X,Y) :將畫筆移動到指定的坐標(biāo)位置 L = lineto(L X,Y) :畫直線到指定的坐標(biāo)位置 H = horizontal lineto(H X):畫水平線到指定的X坐標(biāo)位置 V = vertical lineto(V Y):畫垂直線到指定的Y坐標(biāo)位置 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():關(guān)閉路徑
小盆友個人認(rèn)為,這些語法作為一個了解即可,并不需要記憶,因為 SVG 的資源文件一般不需要我們程序猿自行繪制,只是偶爾需要修改一下,所以要求并不是很高。
現(xiàn)在有很多在線編輯SVG工具,可以通過繪制后,將路徑數(shù)據(jù)拷貝下來稍作修改,便可使用。
“手寫”掘金 的 SVG資源就是小盆友從掘金官網(wǎng)獲取后,進(jìn)行一些簡單的修改,所以只需要了解,需要修改時會運(yùn)用就行。
2、作為靜態(tài)圖片資源
在 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 標(biāo)簽中的:?
android:width 和 android:height
?表示的是?SVG的大小,而:
android:viewportWidth 和 android:viewportHeight
?表示的是將:
android:width 和 android:height
劃分成多少個等份,隨后的 Group 和 Path 的坐標(biāo)則是基于這一比例進(jìn)行編寫。
group 和 path 我們在前面已經(jīng)提過了,就不再贅述。
我們舉個簡單的例子,用 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的機(jī)子上也有測試過。
3、作為動態(tài)圖片資源
SVG 的動畫是比較有趣的,但我們在?“動畫限制問題”?小節(jié)中提到,存在著兼容問題,5.0之前的版本不能使用SVG動畫。
所以我們需要新建一個?drawable-anydpi-v21?文件夾,來存放我們的動畫資源,具體存放結(jié)構(gòu)和代碼如下:
animated-vector?起著?扣接 SVG靜態(tài)資源 和 屬性動畫?的作用。
// 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 最終就是調(diào)用了 VectorDrawableCompat$VPath 中的 setPathData,而參數(shù)類型便為 pathType。忘記的童鞋可以回 “動畫限制問題” ?小節(jié)查看下。
如果只是把我們這里使用的 menu資源放在?drawable-anydpi-v21?文件夾下,運(yùn)行于 4.4的機(jī)子時,會報找不到相應(yīng)資源的錯誤。
所以我們需要在?drawable?文件夾下,建一個相同名字的資源 menu資源,只是里面的內(nèi)容不是?animated-vector?作為根標(biāo)簽,而是使用和 ic_menu資源 完全一樣的內(nèi)容。
最終在代碼中進(jìn)行兼容處理 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之前版本就只是簡單圖片切換,就不上圖了:
四、實戰(zhàn)
上一小節(jié)我們知道,對 SVG 添加動畫,簡單方便,但是也說明了使用系統(tǒng)自帶的這一套操作無法實現(xiàn)較為復(fù)雜的交互,所以我們只能自己動手,才能豐衣足食了。
還記得小盆友在介紹優(yōu)點時,說到SVG的格式是XML,這就是我們自己動手的切入點。因為格式為XML,所以可以自行解析,拿取其中的pathData數(shù)據(jù)轉(zhuǎn)為Path路徑,接下來就可以做很多有趣的事情。我們?nèi)谌氲綄崙?zhàn)中來體會這一趣事。
1、"手寫"掘金
效果圖
Github入口:
https://github.com/zincPower/UI2018
編碼思路
(1)解析 SVG 文件首先需要將 “掘金”這一SVG進(jìn)行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的數(shù)據(jù)在上一步中獲取到DOM樹之后,進(jìn)行遍歷DOM節(jié)點獲取到 Path 數(shù)據(jù),保存其填充的顏色和將 pathData 的數(shù)據(jù)翻譯成 Path對象進(jìn)行保存起來。
這里需要借助?PathParser?類將?pathData?的數(shù)據(jù)翻譯成?Path對象?,但是PathParser類 被打上了注解?@hide,我們無法直接使用,所以只能是將其拷貝一份放置我們的目錄下來使用。具體核心代碼如下:
// 遍歷所有的 Path 節(jié)點
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)進(jìn)行縮放根據(jù) SVG圖像大小 和 畫布大小,進(jìn)行偏移和縮放,讓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 和 屬性動畫,讓其進(jìn)行勾勒后填充屬性動畫開啟后,每次刷新都通過 PathMeasure 對當(dāng)前需要勾勒的Path進(jìn)行裁剪繪制,達(dá)到一步步勾勒的效果。核心代碼如下:
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講解與實戰(zhàn)
2、地圖查閱器
效果圖
Github入口:https://github.com/zincPower/UI2018
編碼思路
(1)解析SVG數(shù)據(jù)與“手寫”掘金的事例一樣,第一步也是解析數(shù)據(jù),通過 ?PathParser?類將svg的數(shù)據(jù)轉(zhuǎn)為Path對象,而顏色填充則由我們設(shè)置的數(shù)組決定。
同時還要保存好svg圖像的大小,具體核心代碼如下:
// 用于記錄整個 svg 的實際大小
float left = -1;
float top = -1;
float right = -1;
float bottom = -1; // 計算出 path 的 rect
RectF rect = new RectF(); // 遍歷所有的 Path 節(jié)點
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中心根據(jù)畫布的大小 和 svg的大小,將我們的畫布進(jìn)行偏移和縮放,使我們的地圖大小合適且居中放置(這里借助了矩陣,但最終會將該矩陣作用于我們的畫布)
// 移至畫布中心
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); // 進(jìn)行縮放
if (!mLastRectF.isEmpty()) { mScale = calculateScale( mLastRectF.width(), mLastRectF.height(), getWidth(), getHeight());
}
mCanvasMatrix.preScale( mScale, mScale, lastLeftMargin + mLastRectF.width() / 2, lastTopMargin + mLastRectF.height() / 2);
(3)如何交互至此我們的地圖就已經(jīng)能正常顯示了,但還需要交互。交互最主要的問題是我們?nèi)绾沃肋x中的是哪塊區(qū)域。具體通過一下代碼進(jìn)行判斷,便可知道我們是否觸碰了 該P(yáng)ath所包含的區(qū)域:
/** * 是否在觸碰的范圍內(nèi) * * @param item 地圖的每個數(shù)據(jù)項 * @param x 觸碰點的x軸 * @param y 觸碰點的y軸 * @return true:在范圍內(nèi);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)剩余操作獲得了點擊的區(qū)域,如何進(jìn)行動畫的過渡就是計算邏輯問題了。小盆友這里就不再展開講這塊的邏輯。這里用一句話概括,就是通過比較?上一次選中的Path區(qū)域?和?這次選中的Path區(qū)域?進(jìn)行?中心坐標(biāo)偏移和縮放。
五、寫在最后
SVG 也是一把利器,揮舞得當(dāng)可以讓自己的App展現(xiàn)出別人所想不到的交互效果,希望這篇文章能讓你體會到不一樣的SVG。如果你有所收獲就給我一個贊??并關(guān)注我吧,如果發(fā)現(xiàn)有那些欠妥的地方,請留言區(qū)與我討論,我們共同進(jìn)步。
高級UI系列的Github地址:請進(jìn)入:
https://github.com/zincPower/UI2018
如果喜歡的話給我一個star吧?
推薦閱讀
Canvas中的裁剪師講解與實戰(zhàn)Android高級UI
Android控件人生第一站,小紅書任意拖拽標(biāo)簽控件
長按識別小程序,參與抽獎
▼
更多現(xiàn)金紅包,請長按二維碼▼
目前100000+人已關(guān)注加入我們
???????
???????
紅包天天有,別忘了點個「在看」
總結(jié)
以上是生活随笔為你收集整理的放荡不羁SVG讲解与实战之Android高级UI的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。