半栈工程师Android技术知识Android开发

放荡不羁SVG讲解与实战——Android高级UI

2019-04-07  本文已影响42人  9dfaf364d57f

目录
一、前言
二、SVG小课堂
三、简单使用
四、实战
五、写在最后

一、前言

SVG 在安卓5.0被引入,因为其放大后不会模糊的优秀表现,被使用也是越来越多。今天小盆友也来谈谈这个优秀的SVG,同时分享一些个人比较喜欢的知识小点。老规矩,先上实战图。

"手写"掘金


地图查阅器

二、SVG小课堂

1、SVG是什么

SVG 全称 Scalable Vector Graphics ,翻译一下即为 可缩放的矢量图形

2、优点

SVG 的优点很多,而且在不同的场景优点也会有所不同,小盆友觉得 SVG 给我带来的优点如下几点

3、缺点

这个缺点,说的并不是SVG的缺点,而是在 Android 中使用SVG的缺点或局限

(1) 动画兼容问题

前言中提到 SVG 是在5.0之后引入,虽然作为一个图标资源并不会有兼容问题。

但是如果对 SVG 进行使用动画时,则需要进行兼容性处理。否在 5.0 以下会闪退,毕竟 4.4 的占有率还 10.3%左右(如下图,图片来自 Android Studio 的统计)。

至于如何使用和兼容,我们在下一小节进行说明。


2、动画限制问题

动画限制这一点其实准确来说,不属于缺点,小盆友认为是不够灵活。

因为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标签进行控制的只能这几个属性。



小结一下,这些方法能满足我们一些简单的动画,但是设计师来了一个较为骚气的交互,这时我们比较尴尬了,因为我们没法进行扩展,没法设置我们自己想要的动画逻辑。

三、简单使用

我们先来阐述如何将SVG常规使用起来。但在这之前我们需要说明一下 SVG 中绘制 Path 的语法。

1、绘制语法

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资源就是小盆友从掘金官网获取后,进行一些简单的修改,所以只需要了解,需要修改时会运用就行。

2、作为静态图片资源

在 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的机子上也有测试过。

3、作为动态图片资源

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之前版本就只是简单图片切换,就不上图了:


四、实战

上一小节我们知道,对 SVG 添加动画,简单方便,但是也说明了使用系统自带的这一套操作无法实现较为复杂的交互,所以我们只能自己动手,才能丰衣足食了。

还记得小盆友在介绍优点时,说到SVG的格式是XML,这就是我们自己动手的切入点。因为格式为XML,所以可以自行解析,拿取其中的pathData数据转为Path路径,接下来就可以做很多有趣的事情。我们融入到实战中来体会这一趣事。

1、"手写"掘金

效果图


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讲解与实战

2、地图查阅器

效果图

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区域 进行 中心坐标偏移和缩放

五、写在最后

SVG 也是一把利器,挥舞得当可以让自己的App展现出别人所想不到的交互效果,希望这篇文章能让你体会到不一样的SVG。如果你有所收获就给我一个赞❤️并关注我吧,如果发现有那些欠妥的地方,请留言区与我讨论,我们共同进步。

高级UI系列的Github地址:请进入传送门,如果喜欢的话给我一个star吧😄

欢迎加我微信,我们可以进行更多更有趣的交流


上一篇下一篇

猜你喜欢

热点阅读