(十七)SVG 例項-可互動式中國地圖
一、效果
二、分析
第二步 將 SVG 資源轉換成相應的 Android 程式碼
用http://inloop.github.io/svg2android/ 網站,將 SVG 轉化成 Xml檔案,放置在 /res/raw/ 下。
第三步 利用 Xml 解析 SVG 的程式碼 封裝成 JavaBean 最重要的得到 Path
利用 Xml 解析,把中國地圖的 Path 封裝成一個個省的 JavaBean。
第四步 重寫 OnDraw 方法 利用 Path 繪製中國地圖
第五步 重寫 OnTouchEvent 方法,記錄手指觸控位置,判斷這個位置是否坐落在某個省份上
三、省份 JavaBean
我們先來封裝一個省份的 JavaBean,以便對各個省份進行繪製。
public class Provice {
/**
* 繪製路徑
*/
protected Path path;
/**
* 繪製顏色
*/
private int drawColor;
public Provice(Path path) {
this.path = path;
}
void draw (Canvas canvas, Paint paint, boolean isSelect) {
if (isSelect) {
//選中時,繪製描邊效果
paint.setStrokeWidth(2);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
paint.setShadowLayer(8,0,0,0xffffff);
canvas.drawPath(path,paint);
//選中時,繪製地圖
paint.clearShadowLayer();
paint.setColor(drawColor);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(2 );
canvas.drawPath(path, paint);
}else {
//非選中時,繪製描邊效果
paint.clearShadowLayer();
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
paint.setColor(drawColor);
canvas.drawPath(path, paint);
//非選中時,繪製地圖
paint.setStyle(Paint.Style.STROKE);
int strokeColor = 0xFFD0E8F4;
paint.setColor(strokeColor);
canvas.drawPath(path, paint);
}
}
/**
* 是否被選中
*/
public boolean isSelect(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);
}
public Path getPath() {
return path;
}
public void setPath(Path path) {
this.path = path;
}
public int getDrawColor() {
return drawColor;
}
public void setDrawColor(int drawColor) {
this.drawColor = drawColor;
}
}
這段程式碼相對還是比較簡單,在 draw 中不論選中與否都進行兩次繪製,是因為當邊沿顏色設定與中間顏色不一樣的話,是沒辦法直接進行繪製,只能分兩次進行邊沿和中間內容的繪製。
isSelect 就是對一個點是否在一個區域中進行判斷,這個方法比較常見,就不具體介紹了。拿 Path 路徑與該路徑邊界組成的矩形進行相裁剪,獲取到的就是該 Path 所圍成的區域。
四、Xml 資料封裝成 JavaBean
這個主要是分兩部,一個是讀取 Xml 中 Path 的資料,即對 Xml 資料進行解析。二是將獲取到的 Path 路徑資料封裝成 Provice 類。
1.讀取 Xml 資料
InputStream inputStream = context.getResources().openRawResource(R.raw.china_svg);
try {
//取得 DocumentBuilderFactory 例項
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//從 factory 獲取 DocumentBuilder 例項
DocumentBuilder builder = factory.newDocumentBuilder();
//解析輸入流,得到 Document 例項
Document doc = builder.parse(inputStream);
Element rootElement = doc.getDocumentElement();
NodeList items = rootElement.getElementsByTagName("path");
for (int i = 0; i < items.getLength(); i ++) {
Element element= (Element) items.item(i);
String pathData=element.getAttribute("android:pathData");
}
} catch (Exception e) {
e.printStackTrace();
}
這即是對一個 Xml 資料進行讀取的方法,跟 File 讀寫一樣,都是固定套路。
2. 封裝 JavaBean
由 Xml 解析器解析出來的 Path 資料是一個字串,我們需要把這串字串解析成對應的 Path ,然後呼叫建構函式建立對應的 Provice。
選取其中一個省份資料進行分析:
<path
android:fillColor="#CCCCCC"
android:strokeColor="#ffffff"
android:strokeWidth="0.5"
android:pathData="M546.46,257.82L546.2,258.07L545.19,257.69L545.32,258.18L543.93,257.5L543.54,257.65L542.41,258.89L541.38,259.38L542.02,260.56L540.61,260.4L539.44,259.53L539.28,258.7L538.66,258.32L537.92,258.56L536.13,258.13L535.82,258.52L535.34,258.01L535.11,258.57L534.23,258.84L533.87,259.42L533.2,259.42L532.34,257.98L530.81,258.02L529.74,257.1L529.84,255.69L529,255.13L530.84,254.47L530.2,254L530.15,252.87L528.99,252.2L529.29,251L531.38,249.47L533.13,249.19L533.53,248.49L534.28,249.13L534.78,247.81L535.79,247.12L535.17,245.59L534.67,245.52L533.4,244.1L533.46,243.33L532.94,243.15L533.23,242.5L535.15,241.24L536.1,241.87L537.59,241.28L539.29,238.42L540,238.94L541.47,238.45L542.04,238.74L542.1,238.37L540.49,236.54L540.53,236.05L540.92,235.88L541.32,236.49L542.23,236.64L541.97,235.28L543.78,235.25L543.95,234.15L544.4,233.9L545.04,234.33L544.79,235.26L545.4,236.52L546.36,237.49L547.05,237.66L548.92,239.98L550.77,239.84L552.86,240.63L554.07,240.2L555.26,240.44L555.25,240.92L554.58,240.66L554.36,240.98L554.53,241.89L552.78,241.9L551.97,242.41L552.11,242.94L551.46,243.23L551.9,244.24L551.74,245.25L553.34,247.62L553.79,247.69L553.79,247.69L553.79,248.62L551.45,250.07L551.45,250.07L549.71,250.41L548.96,250.95L548.26,250.65L546.19,250.97L545.91,251.98L546.21,253.02L547.91,253.88L548.31,254.99L547.73,255.44L547.66,256.41L547.66,256.41L547.72,256.71L547.07,256.96z" />
地圖的 Path 是由很多條短的直線連線而成的,我們解析 Xml 獲取到資料是表示這些連線點的字串,需要把這個字串轉化成對應的 Path。網路上有提供對應的的工具類,當然也可以自己進行程式碼編寫解析,程式碼較長,但不是很難,這邊也不展示。
3.另起執行緒
考慮到讀取地圖資料是一個耗時的操作,故另起一個執行緒進行資料的讀取以及解析。
最終程式碼:
private Thread loadThread=new Thread(){
@Override
public void run() {
InputStream inputStream = context.getResources().openRawResource(R.raw.china_svg);
try {
//取得 DocumentBuilderFactory 例項
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//從 factory 獲取 DocumentBuilder 例項
DocumentBuilder builder = factory.newDocumentBuilder();
//解析輸入流,得到 Document 例項
Document doc = builder.parse(inputStream);
Element rootElement = doc.getDocumentElement();
NodeList items = rootElement.getElementsByTagName("path");
for (int i = 0; i < items.getLength(); i ++) {
Element element= (Element) items.item(i);
String pathData=element.getAttribute("android:pathData");
Path path = PathParser.createPathFromPathData(pathData);
proviceList.add(new Provice(path));
}
} catch (Exception e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
};
private int[] colorArray = new int[]{0xFF239BD7, 0xFF30A9E5, 0xFF80CBF1, 0xFFB0D7F8};
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (proviceList == null) {
return;
}
for (int i = 0; i < proviceList.size(); i++) {
int color;
int flag = i %4;
switch (flag) {
case 1:
color = colorArray[0];
break;
case 2:
color = colorArray[1];
break;
case 3:
color = colorArray[2];
break;
default:
color = colorArray[3];
break;
}
proviceList.get(i).setDrawColor(color);
}
postInvalidate();
}
};
PathParser 即是 Path 路徑字串轉化為 Path 物件的工具類。在需要載入地圖的時候,啟動這個執行緒即可。
把資料解析成 JavaBean 之後,需要通知進行重新整理操作。在這裡,我們對各個省份的繪製顏色進行隨機設定,實際中可以根據某一資料進行顏色繪製的選擇。
五、onDraw()
onDraw()一直是核心重點。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (proviceList != null) {
//繪製沒有被選擇的省份
for (Provice item : proviceList) {
if (item != selectItem) {
item.draw(canvas, paint, false);
}
}
//繪製選中的省份
if (selectItem != null) {
selectItem.draw(canvas, paint, true);
}
}
}
onDraw()分別進行了選中與未選中省份的繪製,由於前面已經封裝好 Provide 類的繪製方法 draw(),這邊直接呼叫即可。
注:必須先繪製未選中的省份,否則在選中與未選中的邊沿上,未選中的邊沿顏色會把選中的邊沿顏色給覆蓋掉。
六、OnTouchEvent()
上面已經把 SVG 的地圖資料轉化為 Path 繪製出來,接下來需要對螢幕的觸控事件進行處理,選中對應區域的省份,重繪地圖。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (proviceList != null) {
Provice provice = null;
for ( Provice item : proviceList) {
if (item.isSelect((int) (event.getX()), (int) (event.getY()))) {
provice = item;
break;
}
}
if (provice != null) {
selectItem = provice;
postInvalidate();
}
}
return true;
}
OnTouchEvent() 方法也比較簡單,最開始的時候省份已經封裝了判斷點是否在該省份 Path 路徑內的方法,所以這邊只要遍歷省份,呼叫該方法即可,最後進行重繪。
這時候基本實現了大體功能:把 SVG 地圖資料繪製出來,實現了點選選中功能。
可以在 onDraw 方法前後記錄時間去獲取繪製地圖的所需要的時間,是在0~1 毫秒之間,我們初始化的時候把 Path 儲存在 List 集合裡面,後面只需要再次繪製儘可,繪製的速度是特別快的。
這裡,xml 的寬高都是設定為 match_parent,可以發現,螢幕寬度還是過小,導致地圖無法完全顯示,所以還需要重寫 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 以及對地圖進行縮放。
七、onMeasure()
首先,要確認一下過去的 SVG 中國地圖的具體邊界,在 Provice 類的 isSelect(int x, int y) 就有獲取各個省份的邊界,可以在這邊進行一個測試,記錄邊界值,並取最值,獲取中國地圖的邊界。這邊用的邊界值為:左:0 上:0 右:773 下:568。即中國地圖寬度為 773,高度為 568。
private static int Mleft=0, Mright=0, Mtop=0, Mbotton=0;
public boolean isSelect(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));
if (rectF.right > Mright) {
Mright = (int)rectF.right;
}
if (rectF.bottom > Mbotton) {
Mbotton = (int)rectF.bottom;
}
System.out.println( Mright + " " + Mbotton);
return region.contains(x,y);
}
加的這些是為了計算中國地圖大小,實際不需要。
在 onMeasure()方法裡面計算縮放比例。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
scale = Math.min(width/mapWidth, height/mapHeight);
}
onDraw()方法的時候要進行縮放後在進行繪製
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (proviceList != null) {
canvas.scale(scale, scale);
for (Provice item : proviceList) {
if (item != selectItem) {
item.draw(canvas, paint, false);
}
}
if (selectItem != null) {
selectItem.draw(canvas, paint, true);
}
}
}
onTouchEvent(MotionEvent event) 判定的時候也要新增對應的縮放
@Override
public boolean onTouchEvent(MotionEvent event) {
if (proviceList != null) {
Provice provice = null;
for ( Provice item : proviceList) {
if (item.isSelect((int) (event.getX() / scale), (int) (event.getY() / scale))) {
provice = item;
break;
}
}
if (provice != null) {
selectItem = provice;
postInvalidate();
}
}
return true;
}