有趣的自定义View —— SVG区域点击交互

一,前期基础知识储备

实现效果如下:两张SVG图片,实现点击交互,被点击的区域显示特征色。

有趣的自定义View —— SVG区域点击交互_第1张图片 SVG图片点击交互

 

1)SVG是什么

SVG 文件是纯粹的 XML文件。

  • SVG 指可伸缩矢量图形 (Scalable Vector Graphics)
  • SVG 用来定义用于网络的基于矢量的图形
  • SVG 使用 XML 格式定义图形
  • SVG 图像在放大或改变尺寸的情况下其图形质量不会有所损失
  • SVG 是万维网联盟的标准
  • SVG 与诸如 DOM 和 XSL 之类的 W3C 标准是一个整体

栅格图

栅格图也称位图,它由像素点组成,每个像素点分配特定的色值和位置。我们平时生活和工作中遇到的图像大部分都是栅格图,它对图片在空间和亮度上都做了离散化。

矢量图

矢量的定义为既有大小又有方向的几何对象,那矢量图顾名思义为由矢量组成的图像。矢量图由矢量定义的直线和曲线组成,可以放在特定位置使用颜色填充,它基于数学公式计算获得,所以无论放大还是缩小都不会失真。

2)SVG优势是什么?

与其他图像格式相比,使用 SVG 的优势在于:

  • SVG 可被非常多的工具读取和修改(比如记事本)
  • SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强。
  • SVG 是可伸缩的
  • SVG 图像可在任何的分辨率下被高质量地打印
  • SVG 可在图像质量不下降的情况下被放大
  • SVG 图像中的文本是可选的,同时也是可搜索的(很适合制作地图)
  • SVG 可以与 Java 技术一起运行
  • SVG 是开放的标准
  • SVG 文件是纯粹的 XML

3)SVG工具——用以创建SVG图片

① PNG 转SVG图片:在线网站 https://www.pngtosvg.com/

上传一张平常的PNG图片,然后网站将其转换为SVG图片;

② SVG转vector标签:在线网站 https://inloop.github.io/svg2android/ 

通常情况下,可以直接使用一张SVG图片(标签为svg);但有时需要转为vector标签使用。

有趣的自定义View —— SVG区域点击交互_第2张图片4)SVG内部指令

仅作了解,Android开发者使用上述两个工具即可实现正常使用。

  • M = moveto 相当于 android Path 里的moveTo(),用于移动起始点
  • L = lineto 相当于 android Path 里的lineTo(),用于画线
  • H = horizontal lineto 用于画水平线
  • V = vertical lineto 用于画竖直线
  • C = curveto 相当于cubicTo(),三次贝塞尔曲线
  • S = smooth curveto 同样三次贝塞尔曲线,更平滑
  • Q = quadratic Belzier curve quadTo(),二次贝塞尔曲线
  • T = smooth quadratic Belzier curveto 同样二次贝塞尔曲线,更平滑
  • A = elliptical Arc 相当于arcTo(),用于画弧
  • Z = closepath 相当于closeTo(),关闭path作者:MinicupSimon

坐标轴为以(0,0)为中心,X轴水平向右,Y轴水平向下;
所有指令大小写均可。大写绝对定位,参照全局坐标系;小写相对定位,参照父容器坐标系;
指令和数据间的空格可以省略;同一指令出现多次可以只用一个。

5)直接将SVG作为图片源需要的配置

// 增加配置信息
android {
  defaultConfig {
    vectorDrawables.useSupportLibrary = true
  }
}

// View中使用声明

 

二,上代码,具体实现

1)下载分别代表China和USA的SVG图片;(文末资源中有给出)

https://www.amcharts.com/svg-maps/?map=china

有趣的自定义View —— SVG区域点击交互_第3张图片

2)将下载得到的SVG格式图片转为vector标签;(使用上面的网站)

有趣的自定义View —— SVG区域点击交互_第4张图片 手动作图 — 提醒你们

3)自定义View继承View

核心思路:读取SVG图片中的“pathData”信息,将其转为Android中的Path路径,最后借助Canvas绘制出来。

有趣的自定义View —— SVG区域点击交互_第5张图片

自定义View代码如下:

package com.seotm.coloring.china.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.graphics.PathParser;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.seotm.coloring.china.bean.SvgItem;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;


/**
 * ① 不是直接使用svg图片,需要将svg图片转换为vector标签页才能使用 利用网站在线转换;
 * ② svg图片本身要有足够的颜色信息,意味着初始的那张png图片必须是色彩丰富的,不能是黑白的;
 */
public class SvgView extends View {

    private Context context;//上下文
    private List itemList;//各省地图列表 各省地图颜色 与路径
    private Paint paint;    //初始化画笔
    private SvgItem select; //选中的省份
    private RectF totalRect;//中国地图的矩形范围
    private float scale = 1.0f;//中国地图的缩放比例
    private int resId;

    public SvgView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    public SvgView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void init(Context context, int resId) {
        this.context = context;
        paint = new Paint();
        paint.setAntiAlias(true);
        itemList = new ArrayList<>();
        loadThread.start();
        this.resId = resId;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取当前控件的高度 让地图宽高适配当前控件
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (totalRect != null) {
            double mapWidth = totalRect.width();
            scale = (float) (width / mapWidth); //获取控件高度为了让地图能缩放到和控件宽高适配
        }
        setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    }

    /*不是直接使用svg图片,需要将svg图片转换为vector标签页才能使用*/
    private Thread loadThread = new Thread() {
        @Override
        public void run() {
            final InputStream inputStream = context.getResources().openRawResource(resId);// 读取地图svg 把要解析的XML文档转化为输入流,以便 DOM 解析器解析它
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 获取DocumentBuilderFactory实例,调用 DocumentBuilderFactory.newInstance() 方法得到创建 DOM 解析器的工厂
            DocumentBuilder builder = null;
            try {
                builder = factory.newDocumentBuilder(); // 调用工厂对象的 newDocumentBuilder方法得到 DOM 解析器对象
                Document doc = builder.parse(inputStream);// 解析svg的输入流 调用 DOM 解析器对象的 parse()方法解析XML文档,得到代表整个文档的Document对象,进行可以利用DOM特性对整个XML文档进行操作了
                Element rootElement = doc.getDocumentElement(); // 得到 XML文档的根节点
                NodeList items = rootElement.getElementsByTagName("path");

                NodeList items2 = rootElement.getChildNodes(); // 得到节点的子节点
                String str = rootElement.getAttribute("");
                //获取地图的整个上下左右位置,
                float left = -1;
                float right = -1;
                float top = -1;
                float bottom = -1;
                List list = new ArrayList<>();
                for (int i = 0; i < items.getLength(); i++) {
                    Element element = (Element) items.item(i);
                    String pathData = element.getAttribute("android:pathData"); // 取得节点的属性值 可以读取path内的具体属性值
                    String idData = element.getAttribute("android:id"); // 具体属性值 — id
                    String colorData = element.getAttribute("android:fillColor"); // 具体属性值 — color

                    Log.d("HorseView", "run_000: " + pathData);
                    Log.d("HorseView", "run_111: " + colorData);
                    Log.d("HorseView", "run_222: " + idData);
                    @SuppressLint("RestrictedApi")
                    Path path = PathParser.createPathFromPathData(pathData);

                    SvgItem SvgItem = new SvgItem(path);//设置路径
                    SvgItem.setDrawColor(colorData);//设置颜色
                    //取每个省的上下左右 最后拿出最小或者最大的来充当 总地图的上下左右
                    RectF rect = new RectF();
                    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);
                    list.add(SvgItem);
                }
                itemList = list;
                totalRect = new RectF(left, top, right, bottom);//设置地图的上下左右位置

                //加载完以后刷新界面
                Handler handler = new Handler(Looper.getMainLooper());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        requestLayout();
                        invalidate();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        handleTouch(event.getX() / scale, event.getY() / scale);
        return super.onTouchEvent(event);
    }

    private void handleTouch(float x, float y) {
        if (itemList == null) {
            return;
        }
        SvgItem selectItem = null;
        for (SvgItem SvgItem : itemList) {
            if (SvgItem.isTouch(x, y)) {
                selectItem = SvgItem;
            }
        }
        if (selectItem != null) {
            select = selectItem;
            postInvalidate();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (itemList != null) {
            canvas.save();
            canvas.scale(scale, scale);//把画布缩放匹配到本控件的宽高
            for (SvgItem SvgItem : itemList) {
                if (SvgItem != select) {
                    SvgItem.drawItem(canvas, paint, false);
                } else {
                    SvgItem.drawItem(canvas, paint, true);
                }
            }
        }
    }
}

下面开始重点解析实现的关键: DocumentBuilderFactory解析XML

有趣的自定义View —— SVG区域点击交互_第6张图片

 

① javax.xml.parsers 包中的DocumentBuilderFactory用于创建DOM模式的解析器对象 , DocumentBuilderFactory是一个抽象工厂类,它不能直接实例化,但该类提供了一个newInstance方法 ,这个方法会根据本地平台默认安装的解析器,自动创建一个工厂的对象并返回;
② 调用 DocumentBuilderFactory.newInstance() 方法得到创建 DOM 解析器的工厂;

DocumentBuilderFactory doc=DocumentBuilderFactory.newInstance();

 

③ 调用工厂对象的 newDocumentBuilder方法得到 DOM 解析器对象;

DocumentBuilder db=doc.newDocumentBuilder();

④ 把要解析的 XML 文档转化为输入流,以便 DOM 解析器解析它;

final InputStream inputStream = context.getResources().openRawResource(resId);

⑤  调用 DOM 解析器对象的 parse() 方法解析 XML 文档,得到代表整个文档的 Document 对象,进行可以利用DOM特性对整个XML文档进行操作了;

Document doc = builder.parse(inputStream);

⑥ 得到 XML 文档的根节点;

Element rootElement = doc.getDocumentElement(); 

⑦ 得到节点的子节点;

NodeList items = rootElement.getElementsByTagName("path");

⑧ 得到子节点中的各个属性;

Element element = (Element) items.item(i);
String pathData = element.getAttribute("android:pathData");
String idData = element.getAttribute("android:id"); 
String colorData = element.getAttribute("android:fillColor");

⑨ 将得到的pathData转为Android的path类;

Path path = PathParser.createPathFromPathData(pathData);

至此,我们已成功从SVG图片中拿到path路径,然后借助Canvas绘制出来即可。这里有用到手指点击区域的判断,借助JavaBean实现,代码如下:

核心:一条path即为一个JavaBean,这样就可以控制各个path的绘制情况,可以绘制多个,也可以像Demo一样只绘制一个。

public class SvgItem {

    private Path path;

    /**
     * 绘制颜色
     * */
    private String drawColor;
    public void setDrawColor(String drawColor){
        this.drawColor = drawColor;
    }

    public SvgItem(Path path) {
        this.path = path;
    }

    public void drawItem(Canvas canvas, Paint paint, boolean isSelect){
        if (isSelect){
            //绘制内部颜色
            paint.clearShadowLayer();
            paint.setStrokeWidth(1);
            paint.setStyle(Paint.Style.FILL);
            paint.setColor(0xffffff00); // 填充的颜色为黄色
            canvas.drawPath(path,paint);
            //绘制边界
            paint.setStyle(Paint.Style.STROKE);
            paint.setColor(0xFFD0E8F4);
            canvas.drawPath(path,paint);
        }else {
            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(Color.parseColor(drawColor));
            paint.setStyle(Paint.Style.FILL);
            paint.setStrokeWidth(2);
            canvas.drawPath(path,paint);
        }
    }

    /*判断手指点击区域是否落入*/
    public boolean isTouch(float x,float 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((int)x,(int)y);
    }
}

4)Activity内使用,传入资源id即可

    
public class MainActivity1 extends AppCompatActivity {

    private SvgView svgView1, svgView2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_china);
        svgView1 = findViewById(R.id.img1);
        svgView1.init(this, R.raw.china);
        svgView2 = findViewById(R.id.img2);
        svgView2.init(this, R.raw.usa_low);
    }

}

 

最后,在此自定义View提出几个延伸的方向:

① 如何实现上一步颜色绘制的保存? —— 用集合装载已经被点击的区域的JavaBean,onDraw内绘制前在做判断;

② 如何实现图片的缩放? —— 建议只采用双指缩放,避免与点击矛盾;

有趣的自定义View —— SVG区域点击交互_第7张图片

如何绘制代表区域块的数字? —— 暂时还没有思绪,如何实现非规则path区域内的定位,还望不吝赐教。

 

你可能感兴趣的:(高级技巧-自定义View,有趣的自定义view,自定义View,SVG)