Header
先来看一下效果如何
很多人都看到过类似这样的视频播放按钮,那么,这样的按钮是如何实现的呢?其实也就两个知识点,一个是自定义控件,一个是SVG矢量动画,那么下面就来讲讲如何实现这个控件。
Body
1.SVG
何为SVG,可缩放矢量图形(Scalable Vector Graphics)。Android在5.0版本之后添加了对SVG矢量图形的支持,被越来越多的用到了Android的各个场景之中,不同于普通的位图,矢量图形是不会因为拉伸等情况而失去其原有的样貌,这也在适配方面提供了不少便利。
vector标签
在Android 5.0之后,我们可以通过VectorDrawable来实现矢量图形,图像体积小,而且缩放不失真。另外,可以通过使用
AnimatedVectorDrawable和属性动画来做矢量动画。
来看下vector标签的几个属性
1.name :指定矢量图形的名称
2.width :指定矢量图形的默认宽度,一般使用dp数值,如果在layout布局文件中将ImageView的layout_width设置为wrap_content,同时src设置为该矢量图形的话,则ImageView的宽度就是此处width的宽度
3.height :指定矢量图形的默认高度,效果和width的效果一样
4.viewportHeight :指定视图空间的高度,算是虚拟坐标系的高度,后续坐标系的信息都基于此数据
5.viewportWidth :指定视图空间的宽度,算是虚拟坐标系的宽度,效果和第4条一样
6.alpha :指定图像的透明度(0——1)
这是几个比较主要的属性。那么,再来说一下width和viewportWidth的区别是什么?width指的是显示在屏幕上可以直观看到的宽度数据,而viewportWidth呢,其实就是将宽度变为坐标系,将宽度等分为viewportWidth的数值,图像中所有的路径以坐标的形式进行定位,这样不论矢量图形的宽高如何设置,其图形内部的路径都是等比例缩放的,这样不论图形实际大小,图像都不会被拉伸,不会失真。
group标签
group标签可以包裹多个path,可以将这些path捆绑起来,来执行一些共同的行为
来看下group的几个属性
1.name :指定group的名称
2.pivotY : 指定旋转动画的轴Y坐标
3.pivotX : 指定旋转动画的轴X坐标
4.rotation : 指定group的旋转角度
5.scaleX : 指定group在横向上的缩放比例,值为0.5则表示缩小一半,值为2则表示放大一倍
6.scaleY : 指定group在纵向上的缩放比例,值为0.5则表示缩小一半,值为2则表示放大一倍
7.translateX : 指定group在横向上的平移距离
8.translateY : 指定group在纵向上的平移距离
path标签
path标签则是描述图形的路径,通过路径的各个动作执行就可以画出图形的具体图像
1.name : 指定path路径的名称
2.pathData : path的具体内容
3.fillColor : 路径所包围的内容的内部颜色,如果不设置,则只绘制轮廓边线,而不填充内部
3.trimPathEnd : 表示整段路径的显示结束位置(0——1)如数值为0.5,则显示路径从头开始到一半。
4.trimPathStart : 表示整段路径的显示开始位置(0——1)如数值为0.5,则显示从路径一半开始到结束位置
如若trimPathStart和trimPathEnd都设置了,则显示start起始到end结束
5.strokeColor :路径边框颜色,如果不设置,则不绘制轮廓边线
6.strokeWidth : 路径轮廓边线宽度
7.strokeLineCap : 指定曲线的首尾外观。取值说明有三个:butt(默认值,直线边缘)、round(圆形边缘)、square(方形边缘)
8.trimPathoffset:指定几何路径的绘制偏移。取值为0.0到1.0,表示线条从trimPathOffset+trimPathStart处一直绘制到trimPathOffset+trimPathEnd处
以上为几条比较重要的属性
路径
那么矢量图象的具体绘制路径在哪里定义呢?就是path标签下的pathData属性。其值要符合SVG的标准,那么就说下SVG标准的几个语法
命令 | 参数 | 说明 |
M | x,y | 将画笔移动到指定位置x,y |
L | x,y | 从当前点画一条直线到x,y |
H | x | 从当前点画水平线到指定x位置 |
V | y | 从当前点画垂直线到指定y位置 |
C | x1,y1,x2,y2,endX,endY | 三次贝塞尔曲线(当前位置到endX,endY,然后中间部分分别向x1,y1和x2,y2方向突出) |
S | x2,y2,endX,endY | 三次贝塞尔曲线 |
Q | x,y,endX,endY | 二次贝塞尔曲线(当前位置到endX,endY,然后中间部分曲线向x,y方向突出) |
A | rX,rY,xRotation,flag1,flag2,x,y | 弧线(rX 横轴半径,rY 纵轴半径,xRotation 圆弧旋转角度,也就是圆弧的X轴和水平方向顺时针方向的夹角,可以想象成。一个水平的椭圆饶中心点顺时针旋转XROTATION 的角度,flag1 1表示大角度弧度,0表示小角度弧度,flag2 顺时针1,逆时针0,x,y为终点坐标) |
T | endX,endY | 映射前面路径的重点 |
Z | 无 | 关闭路径(将初始位置和结束位置连接起来,形成一个闭合路径) |
使用上面的指令时,需要注意的几点:
- 坐标轴以(0,0)位中心,X轴水平向右,Y轴水平向下。
- 所有指令大小写均可,大写绝对定位,参照全局坐标系,小写相对定位,参照父容器坐标系。
- 指令和数据间的空格可以无视。
- 同一指令出现多次可以用一个。
大概了解了这些,就基本可以绘制一些简单的矢量图形,那么下面我们就来进行开头图形的绘制。
1.绘制一个三角形播放图形,绘制一个暂停图形
播放按钮大概就是这么个样子,那么我们就来看看使用SVG标准命令来绘制吧
首先,画笔初始位置在(0,0)位置,要先把画笔移动到(3,2)的位置,然后画一条直线到(7,5),再从(7,5)的位置画一条直线到(3,8)的位置,最后闭合路径就可以得到这个三角形图像。
play.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<group
android:name="playgroup"
android:pivotX="5"
android:pivotY="5">
<path
android:name="play"
android:fillColor="#ffffff"
android:pathData="M 3.0,2.0 L 7.0,5.0 L 3.0,8.0 Z"/>
</group>
</vector>
具体的xml内容如上,这样就可以画出一个三角形图像,看下预览效果
但是,这样是不行的,因为后面我们需要把这个三角形变为两个矩形,所以这个三角形也要最好是两个部分,所以需要这样绘制
这样的话,就先将画笔移动到(3,2)位置,画直线到(7,5)位置,再画直线到(3,5)位置,再画回起始位置(3,2),移动画笔到(3,8)位置,画直线到(7,5)位置,再画直线到(3,5)位置,再到(3,8)位置,这样就完成了两个三角形。
play.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<group
android:name="playgroup"
android:pivotX="5"
android:pivotY="5">
<path
android:name="play"
android:fillColor="#ffffff"
android:pathData="M 3.0,2.0 L 3.0,2.0 L 7.0,5.0 L 3.0,5.0 L 3.0,2.0 M 3.0,8.0 L 3.0,8.0 L 7.0,5.0 L 3.0,5.0 L 3.0,8.0"/>
</group>
</vector>
效果和上面的是一样的。
接下来就是绘制暂停按钮
有人看为什么暂停键是两条横着的矩形,大家看Header里的效果,起始暂停初始的时候是横着的,不过是做了一个旋转动画,所以在绘制初始图片的时候,需要横向绘制。这个就不多说了,直接上代码
pause.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<group
android:name="pausegroup"
android:pivotX="5"
android:pivotY="5">
<path
android:name="pause"
android:fillColor="#ffffff"
android:pathData="M 2.0,2.0 L 8.0,2.0 L 8.0,4.0 L 2.0,4.0 L 2.0,2.0 M 2.0,8.0 L 8.0,8.0 L 8.0,6.0 L 2.0,6.0 L 2.0,8.0"/>
</group>
</vector>
好,到这儿,我们需要的两个矢量图形就准备好了,接下来要做的就是编写动画,让两个图形可以进行变换。
2. objectAnimator(属性动画)
首先我们看play变换为pause,如何形变
animator_play_to_pause
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:propertyName="pathData"
android:valueType="pathType"
android:valueFrom="M 3.0,2.0 L 3.0,2.0 L 7.0,5.0 L 3.0,5.0 L 3.0,2.0 M 3.0,8.0 L 3.0,8.0 L 7.0,5.0 L 3.0,5.0 L 3.0,8.0"
android:valueTo="M 2.0,2.0 L 8.0,2.0 L 8.0,4.0 L 2.0,4.0 L 2.0,2.0 M 2.0,8.0 L 8.0,8.0 L 8.0,6.0 L 2.0,6.0 L 2.0,8.0">
</objectAnimator>
来看下objectAnimator的这几个属性:
1.duration :表示该动画执行时长
2.propertyName : 表示该动画的目标属性
3.valueType :表示该动画的变化数值类型(pathType colorType intType floatType)
4.valueFrom :表示起始数值
5.valueTo :表示终止数值
该objectAnimator所表示的动画就是,在300毫秒内,将valueFrom的路径变化为valueTo的路径。也就可以把两个三角形变为两个矩形。
这是从play变为pause,当然还有pause变为play,代码基本一样,只是需要把valueFrom和valueTo的数据对调位置就可以了,这里就不贴代码了。
从效果图中我们看到,除了形变以外,还有一个旋转动画,那么接下来就定义一下旋转动画
animator_rotation_play_to_pause.xml
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="450"
android:propertyName="rotation"
android:valueType="floatType"
android:valueFrom="0"
android:valueTo="-90">
</objectAnimator>
当然,还有反方向的旋转,道理一样,将valueFrom和valueTo对调位置
3. animated-vector
我们有了矢量图形,也有了动画,那如何将这两者结合起来呢?就用到了animated-vector标签,起始就是AnimatedVectorDrawable。从play转为pause,首先要把三角转变为矩形,并且逆时针旋转90度,直接上代码
animated_vector_play_to_pause.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="NewApi"
android:drawable="@drawable/play">
<target
android:animation="@animator/animator_play_to_pause"
android:name="play"/>
<target
android:animation="@animator/animator_rotatation_play_to_pause"
android:name="playgroup"/>
</animated-vector>
来看下animated-vector的属性:
1.drawable : 表示要进行操作的图像
2.target : 1.animation : 表示要执行的动画 2.name : 表示要对哪一部分进行动画(name的数值要和vector中的path或者group的名称要对应,否则会找不到目标图像)
上面表示的就是 要对play所对应的path进行路径形变,然后要对playgroup所对应的组进行旋转操作。
当然,还有pause转play的动画
animated_vector_pause_to_play.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/pause">
<target
android:animation="@animator/animator_pause_to_play"
android:name="play"/>
<target
android:animation="@animator/animator_rotation_pause_to_play"
android:name="playgroup"/>
</animated-vector>
4.animated-selector
由于,我们的控件是有两种图像,两种动画的执行,两种存在的状态,所以这里我们需要animated-selector,动画选择器来把两个状态的图像和动画联系起来,并通过操作控件的某个属性来将两种状态进行切换。
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/play"
android:drawable="@drawable/play"
android:state_checked="false"/>
<item
android:id="@+id/pause"
android:drawable="@drawable/pause"
android:state_checked="true"/>
<transition
android:fromId="@+id/play"
android:toId="@+id/pause"
android:drawable="@drawable/animated_vector_play_to_pause"/>
<transition
android:fromId="@+id/pause"
android:toId="@+id/play"
android:drawable="@drawable/animated_vector_pause_to_play"/>
</animated-selector>
上面的属性我就不说了,网上一大把,看下基本就知道啥意思了。这样适量动画这一块的工作就算结束了。
自定义控件
自定义控件,具体的我就不在这里阐述了,可以点击这里去看我的另一篇文章,里面有关于自定义控件的东西,网上也是一大把
现在这里呢,也不是完全的自定义控件,而是对原有的控件进行一个改造,对imageview进行改造。
1.创建一个view并且继承imageview,用来承载我们刚才做好的矢量图形
public class PlayButton extends AppCompatImageView {
public PlayButton(Context context) {
super(context);
}
public PlayButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PlayButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
定义一个boolean成员变量记录当前状态,定义一个监听提供给使用者方便在不同状态下进行其他操作,创建play和pause方法,不仅可以通过点击本身切换状体,也可以通过调用api来进行状态改变。(还要防止快速点击的问题),我就直接上代码吧,没啥好说的
public class PlayButton extends AppCompatImageView {
//当前播放状态 true正在播放 false暂停播放
private boolean isPlay = false;
//提供给使用者的点击监听
private OnPlayOrPauseClick onPlayOrPauseClick;
public PlayButton(Context context) {
super(context);
}
public PlayButton(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public PlayButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void initView() {
//初始化图像,将选择器配置进去
setImageResource(R.drawable.animated_selector_play_and_pause);
//设置点击事件
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
changeState();
}
});
}
//根据当前状态进行切换
private void changeState(){
if (!isFastClick()) {
isPlay = !isPlay;
final int[] state = {android.R.attr.state_checked * (isPlay ? 1 : -1)};
setImageState(state, true);
if (null != onPlayOrPauseClick) {
onPlayOrPauseClick.onClick(!isPlay);
}
}
}
//播放
public void toPlay(){
if (isPlay){
return;
}else{
changeState();
}
}
//暂停
public void toPause(){
if (!isPlay){
return;
}else{
changeState();
}
}
private long lastClickTime;
//防止快速点击
public boolean isFastClick() {
boolean flag = true;
long currentClickTime = System.currentTimeMillis();
if ((currentClickTime - lastClickTime) >= 500) {
flag = false;
}
lastClickTime = currentClickTime;
return flag;
}
//获取当前播放状态
public boolean isPlay() {
return isPlay;
}
public void setOnPlayOrPauseClick(OnPlayOrPauseClick onPlayOrPauseClick) {
this.onPlayOrPauseClick = onPlayOrPauseClick;
}
public interface OnPlayOrPauseClick {
void onClick(boolean isPlay);
}
}
自此,控件就完成了,来,测一下
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context="com.will.svg.MainActivity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="20dp">
<TextView
android:id="@+id/play"
android:layout_width="90dp"
android:layout_height="90dp"
android:background="#999999"
android:text="play"
android:gravity="center"
android:layout_marginRight="20dp"/>
<TextView
android:id="@+id/pause"
android:layout_width="90dp"
android:layout_height="90dp"
android:text="pause"
android:gravity="center"
android:background="#999999"/>
</LinearLayout>
<com.will.svg.PlayButton
android:id="@+id/play_image_button"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="#999999"/>
</LinearLayout>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final PlayButton button = findViewById(R.id.play_image_button);
button.setOnPlayOrPauseClick(new PlayButton.OnPlayOrPauseClick() {
@Override
public void onClick(boolean isPlay) {
if (isPlay){
Toast.makeText(MainActivity.this,"要关闭喽",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(MainActivity.this,"要打开喽",Toast.LENGTH_SHORT).show();
}
}
});
TextView play = findViewById(R.id.play);
TextView pause = findViewById(R.id.pause);
play.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
button.toPlay();
}
});
pause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
button.toPause();
}
});
}
}
大功告成,效果就是Header中gif的效果。
End
好了,如果能把这个实现出来,也算是对SVG有那么一点皮毛的认识了,后面会有更深入的探索吧