美工死不瞑目系列之SVG推鍋技巧!

1.前言

SVG,即Scalable Vector Graphics 可伸縮矢量圖形。這種圖像格式在前端中已經(jīng)使用的非常廣泛了,而在移動端的開發(fā)中,遇到一些復(fù)雜的自定義控件或者動畫效果,我們就可以考慮讓美工出套SVG圖,再按照固定的套路去解析即可。

2.Vector Drawable

2.1 矢量圖與位圖

先介紹下矢量圖像和位圖圖像的區(qū)別

1.矢量圖像:SVG是W3C 推出的一種開放標準的文本式矢量圖形描述語言,他是基于XML的專門為網(wǎng)絡(luò)而設(shè)計的圖像格式
SVG是一種采用XML來描述二維圖形的語言,所以它可以直接打開xml文件來修改和編輯。 

2.位圖圖像:位圖圖像的存儲單位是圖像上每一點的像素值,因而文件會比較大,像GIF、JPEG、PNG等都是位圖圖像格式。

也就是說,如果使用矢量圖,就不需要針對不同dpi的設(shè)備展示不同精度的圖片了,是不是很方便???

2.2 Vector Drawable簡介

在Andoird中,SVG的實現(xiàn)方式就是Vector Drawable。這是個在5.0時增加的新類,所以對之前版本的兼容會有些問題,之后會單獨拎出來講。
相對于普通的Drawable來說,Vector Drawable有以下幾個好處:

(1)Vector圖像可以自動進行適配,不需要通過分辨率來設(shè)置不同的圖片。
(2)Vector圖像可以大幅減少圖像的體積,同樣一張圖,用Vector來實現(xiàn),可能只有PNG的幾十分之一。
(3)使用簡單,很多設(shè)計工具,都可以直接導出SVG圖像,從而轉(zhuǎn)換成Vector圖像 功能強大。
(4)不用寫很多代碼就可以實現(xiàn)非常復(fù)雜的動畫 成熟、穩(wěn)定,前端已經(jīng)非常廣泛的進行使用了。

2.3 Vector Drawable基本語法

Vector Drawable實際上是一個XML文件,咱們先來看一個vector的例子

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="400dp"
    android:height="400dp"
    android:viewportHeight="400"
    android:viewportWidth="400">
    <path
        android:pathData="M 100 100 L 300 100 L 200 300 z"
        android:strokeColor="#000000"
        android:strokeWidth="5"
        android:fillColor="#FF0000"
        />
</vector>

這個vector畫了一個三角形,對照著上面的代碼,咱們來學習Vector Drawable的基本語法。首先說明一下,這些語法開發(fā)者不需要全部精通,只要能夠看懂即可,這些path標簽及數(shù)據(jù)生成都可以交給工具來實現(xiàn)。

2.3.1 pathData標簽

先看pathData標簽,這里定義了vector中path的繪制,也是最重要的一部分。語法如下,注意,’M’處理時,只是移動了畫筆, 沒有畫任何東西。

M = moveto(M X,Y) :將畫筆移動到指定的坐標位置,相當于 android Path 里的moveTo()
L = lineto(L X,Y) :畫直線到指定的坐標位置,相當于 android Path 里的lineTo()
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):弧線 ,相當于arcTo()
Z = closepath():關(guān)閉路徑(會自動繪制鏈接起點和終點)

2.3.2 path標簽

接著看下path標簽的內(nèi)容。稍微有個印象即可,需要時再對照著去理解。

android:name 定義該 path 的名字,這樣在其他地方可以通過名字來引用這個路徑
android:pathData 和 SVG 中 d 元素一樣的路徑信息。
android:fillColor 定義填充路徑的顏色,如果沒有定義則不填充路徑
android:strokeColor 定義如何繪制路徑邊框,如果沒有定義則不顯示邊框
android:strokeWidth 定義路徑邊框的粗細尺寸
android:strokeAlpha 定義路徑邊框的透明度
android:fillAlpha 定義填充路徑顏色的透明度
android:trimPathStart 從路徑起始位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathEnd 從路徑結(jié)束位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathOffset 設(shè)置路徑截取的范圍 
android:strokeLineCap 設(shè)置路徑線帽的形狀,取值為 butt, round, square.
android:strokeLineJoin 設(shè)置路徑交界處的連接方式,取值為 miter,round,bevel.
android:strokeMiterLimit 設(shè)置斜角的上限

2.3.4 vector標簽

根元素 vector標簽是用來定義這個矢量圖的,該元素包含如下屬性:

android:name 定義該drawable的名字
android:width 定義該 drawable 的內(nèi)部(intrinsic)寬度,支持所有 Android 系統(tǒng)支持的尺寸,通常使用 dp
android:height 定義該 drawable 的內(nèi)部(intrinsic)高度,支持所有 Android 系統(tǒng)支持的尺寸,通常使用 dp
android:viewportWidth 定義矢量圖視圖的寬度,視圖就是矢量圖 path 路徑數(shù)據(jù)所繪制的虛擬畫布
android:viewportHeight 定義矢量圖視圖的高度,視圖就是矢量圖 path 路徑數(shù)據(jù)所繪制的虛擬畫布
android:tint 定義該 drawable 的 tint 顏色。默認是沒有 tint 顏色的
android:tintMode 定義 tint 顏色的 Porter-Duff blending 模式,默認值為 src_in
android:autoMirrored 設(shè)置當系統(tǒng)為 RTL (right-to-left) 布局的時候,是否自動鏡像該圖片。比如 阿拉伯語。
android:alpha 該圖片的透明度屬性

2.3.5 group標簽

有時候我們需要對幾個路徑一起處理,這樣就可以使用 group 元素來把多個 path 放到一起。 group 支持的屬性如下:

android:name 定義 group 的名字
android:rotation 定義該 group 的路徑旋轉(zhuǎn)多少度
android:pivotX 定義縮放和旋轉(zhuǎn)該 group 時候的 X 參考點。該值相對于 vector 的 viewport 值來指定的。
android:pivotY 定義縮放和旋轉(zhuǎn)該 group 時候的 Y 參考點。該值相對于 vector 的 viewport 值來指定的。
android:scaleX 定義 X 軸的縮放倍數(shù)
android:scaleY 定義 Y 軸的縮放倍數(shù)
android:translateX 定義移動 X 軸的位移。相對于 vector 的 viewport 值來指定的。
android:translateY 定義移動 Y 軸的位移。相對于 vector 的 viewport 值來指定的。

通過上面的屬性可以看出, group 主要是用來設(shè)置路徑做動畫的關(guān)鍵屬性的。

2.4 一些常用的工具

上面的這些語法只要能看懂就可以了。我們會用一些成熟的工具來輔助SVG在移動端的開發(fā)。

1.先說美工這個最好的工具,SVG圖一般直接讓美工來幫你搞定就行了!像PS、Illustrator等等都支持導出SVG圖片

2.獲取到SVG后,我們要將其轉(zhuǎn)換為vector drawable對象,svg2android這個網(wǎng)站可以幫你輕松完成。

3.如果沒有SVG圖片怎么辦?可以使用SVG的編輯器來進行SVG圖像的創(chuàng)作和編寫。

4.獲取到資源后,使用AndroidStudio插件完成SVG添加,AS會自動生成兼容性圖片(高版本會生成xxx.xml的SVG圖片;低版本會自動生成xxx.png圖片)。具體過程看Vector Asset Studio的使用

5.最后介紹幾個可以獲取SVG資源的網(wǎng)站

http://www.shejidaren.com/8000-flat-icons.html
http://www.flaticon.com/
http://www.iconfont.cn/plus

2.5 適配中的一些坑

在正式開始擼代碼前,先解決適配問題。
由于vector drawable是5.0之后才出來的東西,所以我們需要對之前的版本進行兼容。假設(shè)大家都使用Android Studio 2.2以上的版本,并且gradle版本在2.0以上(應(yīng)該沒有原始人吧)。下面是配置的步驟:

    1.1、添加
·   defaultConfig {
        vectorDrawables.useSupportLibrary = true

    }
    1.2、添加
    compile 'com.android.support:appcompat-v7:25.3.1' //需要是23.2 版本以上的

    1.3、Activity需要繼承與AppCompatActivity

    1.4、布局文件當中添加
        xmlns:app="http://schemas.android.com/apk/res-auto"

    1.5、使用在Actvity前面添加一個flag設(shè)置
        static {
            AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
        }       

Vector Drawable可以理解為一張圖片,所以能設(shè)置到其他的控件之中。

1 ImageView、ImageButton
XML app:srcCompat(5.0以上可以直接使用background)
代碼里面使用無區(qū)別,直接setBackground即可。

2. Button 
不支持app:srcCompat
Xm使用在Button的selector中
    
3. RadioButton 
直接使用
    
4. textview的drawable  
直接使用

3.Vector Drawable的使用

3.1 Vector Drawable靜態(tài)使用

結(jié)合上文內(nèi)容,去阿里svg平臺隨便找張svg的圖片,既可以通過svg2android也可以通過AS自帶的插件將其轉(zhuǎn)化為vector drawable,接著配置項目兼容環(huán)境,再將這個vector在資源xml中用app:srcCompat賦值給ImageView,最后的結(jié)果就是這樣,無論怎樣放大都不會失真。

考拉.svg

如果你在自己的安卓機上也實現(xiàn)了這樣的效果,恭喜!關(guān)于Vector Drawable最基本的靜態(tài)使用已經(jīng)被你掌控了!

3.2 Vector Drawable動態(tài)使用

大聲告訴我,android中有幾種動畫的實現(xiàn)方式?除了幀、補間、屬性動畫以外,vector drawable也可以用來完成動畫效果,還記得之前講的path標簽嗎,這里面的屬性都可以作為動畫的變化條件,我們再展示一下:

android:name 定義該 path 的名字,這樣在其他地方可以通過名字來引用這個路徑
android:pathData 和 SVG 中 d 元素一樣的路徑信息。
android:fillColor 定義填充路徑的顏色,如果沒有定義則不填充路徑
android:strokeColor 定義如何繪制路徑邊框,如果沒有定義則不顯示邊框
android:strokeWidth 定義路徑邊框的粗細尺寸
android:strokeAlpha 定義路徑邊框的透明度
android:fillAlpha 定義填充路徑顏色的透明度
android:trimPathStart 從路徑起始位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathEnd 從路徑結(jié)束位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathOffset 設(shè)置路徑截取的范圍 
android:strokeLineCap 設(shè)置路徑線帽的形狀,取值為 butt, round, square.
android:strokeLineJoin 設(shè)置路徑交界處的連接方式,取值為 miter,round,bevel.
android:strokeMiterLimit 設(shè)置斜角的上限

剩下就是滿滿的套路了。
首先,獲取到一張vector圖片,比如這次使用的是一個對勾。我們給path標簽附上了name屬性,這是為了之后在動畫中找到這條path。

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:name="path_check"
        android:fillColor="#FF000000"
        android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

接著,用動畫vector包裝原來的vector圖片,其創(chuàng)建方式和vector相似,只不過最外層的標簽為animated-vector。我們還要為target標簽賦值,name屬性是前面命名的、vector中需要變化的地方,而animation自然就是屬性動畫了。

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
                 android:drawable="@drawable/ic_done_black_24dp">
    <target
        android:name="path_check"
        android:animation="@animator/check_animator"/>
</animated-vector>

歸根結(jié)底還是需要用到屬性動畫,我們通過xml的方式來完成它。注意要在res下創(chuàng)建animator文件夾,再將xml放入其中。這里變化的屬性是path標簽中trimPathEnd屬性。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
    android:duration="500"
    android:propertyName="trimPathEnd"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType"/>
</set>

最后,只要在代碼中將Drawable轉(zhuǎn)化為Animatable,并調(diào)用其start()方法開啟動畫即可。

  <ImageView
        android:id="@+id/iv"
        android:layout_width="240dp"
        android:layout_height="240dp"
        app:srcCompat="@drawable/check_animator"
        />
 imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Animatable animatable = (Animatable) imageView.getDrawable();
                animatable.start();
            }
        });

來看看效果圖吧

動態(tài)vector.gif

4.交互式中國地圖

我們已經(jīng)掌握了靜態(tài)與動態(tài)的SVG使用,接下來要學習更具挑戰(zhàn)性的交互式應(yīng)用。原圖長這樣:

中國地圖.svg

我們要實現(xiàn)的效果就是每個省份都能被點擊并凸顯出來。很顯然,這么一個復(fù)雜的圖形是android中其他的知識所不能解決的。先看看效果圖

2017-10-27_12_13_27.gif

下面分析思路。首先,解析SVG圖片,由于每個省份都是一個path,因此可以獲取到34個Path。又因為每個省份都有不同的顏色、被點擊時有不同的繪制方式,所以可以創(chuàng)建provinceItem對象來封裝這些參數(shù)和方法。最后是點擊事件的控制與判斷,如果當前觸摸點在某個省份內(nèi),就將其輪廓突出。

4.1 ProvinceItem

我們以小博大,先從ProvinceItem對象開始介紹。該對象有2個參數(shù),分別是從SVG中解析出來的path以及該path需要填充的顏色color。每個“省”都提供了繪制方法,用來讓外部的地圖控件調(diào)用,以此繪制普通狀態(tài)或者選中狀態(tài)。

  /**
     * 是否被選擇
     *
     * @param canvas
     * @param paint
     * @param isSelected
     */
    public void draw(Canvas canvas, Paint paint, boolean isSelected) {
        if (isSelected) {
            paint.setStrokeWidth(3);
            paint.setColor(Color.BLACK);
        }else {
            paint.setStrokeWidth(1);
            paint.setColor(0xFFD0E8F4);
        }
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(path,paint);

        paint.setColor(drawColor);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, paint);
    }

普通狀態(tài)和選中狀態(tài)的區(qū)別只是外部輪廓的顏色和粗細,使用paint分別繪制其輪廓和填充顏色即可。

重點在于判斷觸摸點是否在某個省的范圍內(nèi)。每個省都是不規(guī)則的圖形,說到不規(guī)則,是否想起之前講Canvas時介紹的Region?Region代表一塊區(qū)域,其面積的計算是使用微積分的原理,正好在此派上用場。

  public boolean isTouch(int x, int y) {
        RectF rectF=new RectF();
        path.computeBounds(rectF,true);
        Region region=new Region();
        region.setPath(path,new Region((int)rectF.left,(int)rectF.top,(int)rectF.right,(int)rectF.bottom));
        return region.contains(x,y);
    }

無論是多么不規(guī)則的圖形,總會有頂點的上下左右,我們通過path.computeBounds()計算出這個上下左右的邊界,再通過region.setPath()將path和上下左右傳入,即可獲取path所對應(yīng)的那塊region。

4.2 MapView

下面介紹外層的地圖控件MapView,在初始化方法中,loadThread用來從SVG中加載數(shù)據(jù),GestureDetectorCompat用來代理onTounch()中的觸摸事件,沒什么多余的意思,就是簡單不用謝swich語句而已……


    private void init(Context context) {
        this.mContext = context;
        mProvinceItems = new ArrayList<>();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        loadThread.start();
        mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                handleTouch(e.getX(), e.getY());
                return true;
            }
        });

    }

先看在子線程中加載數(shù)據(jù)的操作,由于svg是以xml的形式展現(xiàn)的,所以先要解析xml。這里使用了dom解析,當然你喜歡sax或者pull或者別的什么都無所謂。

Thread loadThread = new Thread() {
        @Override
        public void run() {
            List<ProvinceItem> items = new ArrayList<>();//用新的list防止加載時沖突導致crash
            InputStream inputStream = mContext.getResources().openRawResource(R.raw.map_china);
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            try {
                DocumentBuilder builder = factory.newDocumentBuilder();
                Document doc = builder.parse(inputStream);
                Element root = doc.getDocumentElement();
                NodeList list = root.getElementsByTagName("path");
                for (int i = 0; i < list.getLength(); i++) {
                    Element element = (Element) list.item(i);
                    String pathData = element.getAttribute("android:pathData");
                    Path path = PathParser.createPathFromPathData(pathData);
                    ProvinceItem item = new ProvinceItem(path);
                    items.add(item);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            mProvinceItems = items;
            mHandler.sendEmptyMessage(1);
        }
    };

這段代碼的重點其實在PathParser.createPathFromPathData(pathData)這行,其作用是將vector drawable中的path語法轉(zhuǎn)化為android中的Path類。這不是一件簡單的差事,但這又是一件需求很廣泛的差事,所以我選擇使用開源的類比如CSDN上就有的下載,這里限于篇幅我只把這個方法單獨拉出來溜溜,有興趣的同學去找個工具類自己學習吧:

 public static Path createPathFromPathData(String pathData) {
        Path path = new Path();
        PathDataNode[] nodes = createNodesFromPathData(pathData);
        if (nodes != null) {
            try {
                PathDataNode.nodesToPath(nodes, path);
            } catch (RuntimeException e) {
                throw new RuntimeException("Error in parsing " + pathData, e);
            }
            return path;
        }
        return null;
    }

回到我們的代碼中,loadThread在最后給handler發(fā)送了消息,handler作用很簡單,只是給不同的path隨機賦予顏色值

private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mProvinceItems == null) {
                return;
            }
            int totalNumber = mProvinceItems.size();
            for (int i = 0; i < totalNumber; i++) {
                int color;
                int flag = i % 4;
                switch (flag) {
                    case 1:
                        color = colorArray[1];
                        break;
                    case 2:
                        color = colorArray[2];
                        break;
                    case 3:
                        color = colorArray[3];
                        break;
                    default:
                        color = colorArray[0];
                        break;
                }
                mProvinceItems.get(i).setDrawColor(color);

            }
            postInvalidate();
        }
    };

handler的最后,postInvalidate()會導致重繪,進而調(diào)用onDraw()方法。這里又涉及到scale放大倍數(shù),由于svg本身的優(yōu)點就是隨便拉伸,因此給予MapView控件這個scale屬性是理所當然的。剩下就是分別繪制普通省份和被選中省份。

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mProvinceItems != null) {
            //放大倍數(shù)
            canvas.scale(scale, scale);
            for (ProvinceItem item : mProvinceItems) {
                if (item != selectedItem) {     
                    item.draw(canvas, mPaint, false);
                }
            }
            if (selectedItem != null) {
                selectedItem.draw(canvas, mPaint, true);
            }
        }
    }

最后來看看觸摸事件,onTouchEvent中直接回調(diào)mGestureDetectorCompat的方法。

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetectorCompat.onTouchEvent(event);
    }

這里為了好看封裝了下

mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                handleTouch(e.getX(), e.getY());
                return true;
            }
        });

最后一個重點,由于之前的canvas拉伸過,所以在處理點擊位置時需要還原。

  private void handleTouch(float x, float y) {
        if (mProvinceItems == null) {
            return;
        }
        ProvinceItem tmpItem = null;
        for (ProvinceItem item : mProvinceItems) {
            if (item.isTouch((int) (x / scale), (int) (y / scale))) {
                tmpItem = item;
                break;
            }
        }
        if (tmpItem != null) {
            selectedItem = tmpItem;
            postInvalidate();
        }
    }

5.總結(jié)

代碼分析完畢,是不是還挺簡單的?其實最難的部分美工已經(jīng)幫我們解決了,我們只要解析SVG獲取到相應(yīng)的屬性,在通過path啊,paint啊之流去處理這些屬性,就可以輕松的完成一些復(fù)雜的自定義控件了。

最后祝大家不會被美工分而食之!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容