放蕩不羈SVG講解與實戰——Android高級UI

目錄php

1、前言java

2、SVG小課堂android

3、簡單使用git

4、實戰github

5、寫在最後canvas

1、前言

SVG 在安卓5.0被引入,由於其放大後不會模糊的優秀表現,被使用也是愈來愈多。今天小盆友也來談談這個優秀的SVG,同時分享一些我的比較喜歡的知識小點。老規矩,先上實戰圖。數組

"手寫"掘金 微信

地圖查閱器

2、SVG小課堂

一、SVG是什麼

SVG 全稱 Scalable Vector Graphics ,翻譯一下即爲 可縮放的矢量圖形ide

二、優勢

SVG 的優勢不少,並且在不一樣的場景優勢也會有所不一樣,小盆友以爲 SVG 給我帶來的優勢以下幾點svg

  • 縮放均不失真,這帶來的好處是咱們不須要多套分辨率的圖標;
  • 文件相對較小,相較於 JPEG 和 GIF 格式的文件會小些;
  • 以 XML 爲結構,可修改也可擴展,用最簡單的記事本也能進行相應的修改;
  • 高交互性,這一點在實戰時就能體現出來啦;

三、缺點

這個缺點,說的並非SVG的缺點,而是在 Android 中使用SVG的缺點或侷限

(1) 動畫兼容問題

前言中提到 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。

接着咱們來講說他們各自的具體反射類, Group標籤 對應的是 VectorDrawableCompat$VGroup 類,其類的內部方法以下,帶 set 開頭的方法,已經用紅框圈出, 這表明着咱們爲Group標籤設置的屬性動畫所做用的屬性就只能侷限於這幾個方法中

Path標籤對應的是 VectorDrawableCompat$VFullPath,而 VectorDrawableCompat$VFullPath 繼承於 VectorDrawableCompat$VPath,這兩個類的內部方法以下,一樣用紅框圈出 set 開頭的方法,因此咱們經過屬性動畫對Path標籤進行控制的只能這幾個屬性。

小結一下,這些方法能知足咱們一些簡單的動畫,可是設計師來了一個較爲騷氣的交互,這時咱們比較尷尬了,由於咱們無法進行擴展,無法設置咱們本身想要的動畫邏輯。

3、簡單使用

咱們先來闡述如何將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:widthandroid:height 表示的是 SVG的大小,而 android:viewportWidthandroid:viewportHeight 表示的是將 android:widthandroid: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的機子上也有測試過。

三、做爲動態圖片資源

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以前版本就只是簡單圖片切換,就不上圖了:

4、實戰

上一小節咱們知道,對 SVG 添加動畫,簡單方便,可是也說明了使用系統自帶的這一套操做沒法實現較爲複雜的交互,因此咱們只能本身動手,才能豐衣足食了。

還記得小盆友在介紹優勢時,說到SVG的格式是XML,這就是咱們本身動手的切入點。由於格式爲XML,因此能夠自行解析,拿取其中的pathData數據轉爲Path路徑,接下來就能夠作不少有趣的事情。咱們融入到實戰中來體會這一趣事。

一、"手寫"掘金

效果圖

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講解與實戰

二、地圖查閱器

效果圖

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區域 進行 中心座標偏移和縮放

5、寫在最後

SVG 也是一把利器,揮舞得當可讓本身的App展示出別人所想不到的交互效果,但願這篇文章能讓你體會到不同的SVG。若是你有所收穫就給我一個贊❤️並關注我吧,若是發現有那些欠妥的地方,請留言區與我討論,咱們共同進步。

高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄

歡迎加我微信,咱們能夠進行更多更有趣的交流

相關文章
相關標籤/搜索