本系列将介绍以下内容:
1 SVG简介
SVG的全称是Scalable Vector Graphics(可缩放矢量图),它是矢量图,是专门用于网络的矢量图形标准。
与矢量图对应的是位图Bitmap。位图由一个个像素点组成,当图片放大到一定大小时会出现马赛克现象。而矢量图是由一个个点组成,经过数学计算利用直线和曲线绘制而成,无论如何放大都不会出现马赛克现象。
SVG与Bitmap对比:
1、SVG使用XML格式定义图形,可被多种工具读取和修改,如记事本。
2、SVG由点存储,由计算机根据点信息绘图,不会失真,无须根据分辨率适配多套图标。
3、SVG的占用空间比Bitmap小。如一张500px*500px的图像,转成SVG后占用空间大小约20KB,而PNG图像需要约732KB。
4、SVG可转换为Path路径,与Path动画结合组成更丰富的动画。
2 vector标签
在Android中,SVG矢量图是使用标签定义的,存放在res/drawable目录中。
如定义一个vector文件svg_line.xml:
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<path
android:name="bar"
android:pathData="M50,25 L100,25"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"
android:trimPathStart="0" />
</vector>
或
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="50">
<group
android:pivotX="50"
android:pivotY="25"
android:rotation="90">
<path
android:name="bar"
android:pathData="M50,23 L100,23"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray" />
</group>
</vector>
vector标签指定的是画布大小,path标签则指定的是路径内容,group标签用于定义一系列路径或者将path标签分组。更多标签属性及其用法请自行学习。
3 引入SVG图像
Android是不支持SVG图像解析的,必须将SVG图像转换为vector标签描述。可以通过在线转换网站将SVG图像转换为对应的vector标签代码,也可以通过Android Studio引入。如图所示:
4 显示SVG图像
4.1 引入兼容包
dependencies {
implementation libs.appcompat
}
使用Gradle Plugin 2.0以上的版本:
android {
namespace 'com.example.mysvg'
compileSdk 34
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
}
4.2 使用SVG图像
1、对于ImageView和ImageButton这样的控件,要兼容SVG图像,只需要将android:src属性替换为app:srcCompat属性即可。
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/app_name"
app:srcCompat="@drawable/svg_line" />
或,在代码中使用:
imageView.setImageResource(R.drawable.svg_line);
2、在Button和RadioButton中使用
此时需要通过selector标签来实现,如定义文件selector_svg_line.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/svg_line" android:state_pressed="true" />
<item android:drawable="@drawable/svg_line" />
</selector>
在Button中使用:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/selector_svg_line" />
使用时,23.2.0版本需要在代码中添加静态代码块,新版已不需要:
public class SVGShowActivity extends AppCompatActivity {
/*
该代码块放在前部是解决23.2.0版本中的bug,新版不需要了
*/
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.svg_show_activity);
// 直接用Button即可
}
}
5 动态Vector
1、首先还是需要一个vector文件svg_line.xml(见上文),然后定义一个Animator文件以表示对该vector图像做动画,如res/anim/anim_trim_start.xml:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1" />
注意:objectAnimator 所指定的动画是对应vector中path标签的,该示例的动画效果是动态改变path标签的android:trimPathStart属性值,从0变到1。
2、有了图像和动画这两个文件后,需要使用专门用于vector图像与动画关联的animated-vector标签,如res/drawable/line_animated_vector.xml:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/svg_line">
<target
android:name="bar"
android:animation="@anim/anim_trim_start" />
</animated-vector>
在animated-vector标签中,通过android:drawable属性指定vector图像文件。
通过target标签将路径与动画关联,其中,target标签的android:name属性就是指定的vector文件中的path标签的android:name,这两个name属性值必须相同,代表着对该path标签做动画。target标签的android:animation属性用来指定path标签所对应的动画。
animated-vector标签中可以有多个target标签,每个target标签可以将一个path与Animator相关联。
示例,布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始动画" />
<ImageView
android:id="@+id/anim_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/svg_line" />
</LinearLayout>
逻辑代码(详见注释):
package com.example.mysvg;
import android.graphics.drawable.Animatable;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
public class SVGAnimActivity extends AppCompatActivity {
/*
该代码块放在前部是解决23.2.0版本中的bug,新版不需要了
*/
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.svg_anim_activity);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ImageView imageView = findViewById(R.id.anim_img);
// 加载animated-vector文件
AnimatedVectorDrawableCompat animatableDrawable = AnimatedVectorDrawableCompat.create(
SVGAnimActivity.this,
R.drawable.line_animated_vector
);
imageView.setImageDrawable(animatableDrawable);
((Animatable) imageView.getDrawable()).start();
}
});
}
}
效果图:
6 搜索动画示例
vector文件res/drawable/vector_search_bar.xml:
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="150dp"
android:height="24dp"
android:viewportWidth="150"
android:viewportHeight="24">
<!--搜索条-->
<path
android:name="search"
android:pathData="M141,17 A9,9 0 1,1 142,16 L149,23"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray" />
<!--底部横线-->
<path
android:name="bar"
android:pathData="M0,23 L149,23"
android:strokeWidth="2"
android:strokeColor="@android:color/darker_gray"
android:trimPathStart="1" />
</vector>
起点动画文件res/anim/anim_bar_trim_start.xml:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:propertyName="trimPathStart"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
终点动画文件res/anim/anim_search_trim_end.xml:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
关联文件res/drawable/animated_vecotr_search.xml:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/vector_search_bar">
<target
android:name="search"
android:animation="@anim/anim_search_trim_end" />
<target
android:name="bar"
android:animation="@anim/anim_bar_trim_start" />
</animated-vector>
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:orientation="vertical">
<EditText
android:id="@+id/edit"
android:layout_width="150dp"
android:layout_height="24dp"
android:background="@null"
android:hint="点击输入" />
<ImageView
android:id="@+id/anim_img"
android:layout_width="150dp"
android:layout_height="24dp" />
</FrameLayout>
代码:
package com.example.mysvg;
import android.graphics.drawable.Animatable;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
public class SearchEditActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.svg_edit_search_activity);
final ImageView imageView = findViewById(R.id.anim_img);
imageView.setFocusable(true);
imageView.setFocusableInTouchMode(true);
imageView.requestFocus();
imageView.requestFocusFromTouch();
EditText editText = findViewById(R.id.edit);
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
// 加载animated-vector文件
AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(
SearchEditActivity.this,
R.drawable.animated_vecotr_search
);
imageView.setImageDrawable(animatedVectorDrawableCompat);
((Animatable) imageView.getDrawable()).start();
}
}
});
}
}
效果图:
参考文献:
[1] UML中的类图及类图之间的关系
[2] 启舰.Android自定义控件开发入门与实战[M].北京:电子工业出版社,2018
微信公众号:TechU