此前自定义View中用的比较多的是对view位置的控制和功能性的融合,视觉上和动画上的使用要少一些,因此最近心血来潮准备用原生的view绘制些动画效果出来。
自定义View基础流程
这里就懒得去查资料了,大致靠记忆写一下,依次是 onCreate->onMeasure->onLayout->onDraw。
View分类
首先,我们常见的View无非两种,一种是View(独立的控件,不能存在子控件),一种是ViewGroup(大多是充当容器的作用,可以包含子空间,例如:XXLayout)。但其实从本质上来说所有View都是继承于View
,包括ViewGroup也仅仅是种“可以包含view的View”,可能看上去比较拗口,谁让我是一个偏科的工科男,当然是选择原谅我啦o(>﹏<)o。
构造方法
View的构造方法通常根据传入参数个数不同会有三个,分别是Context context, AttributeSet attrs, int defStyleAttr
。
- Context是基础,只要和调用系统参数有有一丝丝联系的地方肯定会有它。
- AttributeSet用于XML文件解析,可以理解成在布局XML中调用才会有。
- defStyleAttr用于解析XML文件中的自定义参数。
测量
onMeasure用来向父容器声明自己的大小,这个方法很重要也很常用,大致就是告诉父容器自己有多大,是以什么属性进行放置,会在父容器的onLayout中参与计算。
定位
onLayout用于设置子View的Layout位置,因此,只有ViewGroup才会执行这个方法。
绘制
onDraw中对View进行具体的绘画操作,决定View最终的展示效果。
draw(Canvas canvas)`,会传入一个Canvas对象,该对象即是整个View画布,所以我们需要画任何效果都是通过canvas.drawXXX()来实现的,这里以绘制一条动态显示的心电图为例进行讲解。
心电图实现
设计
心电图属于一种不规则的线条图形,其实就是由多条线段组成而线又可以由点组成,因此实现方式有很多种,最简单的是通过Canvas.drawPath(Path,Paint)进行绘制。
drawPath
drawPath用于路径的绘制,路径也就是由多个点连接而成的不规则图形,可以相交成闭合的多边形,也可以不相交成为一段线段,该方法有两个参数(Path,Paint)。
- Path,用于记录和保存路径的坐标集合,path.moveTo(x,y)设置Path的起始点坐标,path.lineTo(x,y)设置Path下个点的坐标并且使用直线将前后两点相连。心电图因为都是直线所以使用这两个方法就够了,除了这两个方法还有别的可以画曲线的这里就不详细介绍了。
Paint
Paint,画笔对象,面向对象编程有个特点也是优点,那就是十分贴近我们的生活,既然我们已经有了Canvas画布,那么按照现实生活中的逻辑,我们就还需要用笔在画布上进行绘画才能真正显示出图像,因此我们就需要生成一个Paint对象。
其常见的方法如下:
-
setColor(int color); 设置颜色(为画笔设置一种纯色,这个没什么好说的)
-
setFlags(int flags); 预设一些属性,如
ANTI_ALIAS_FLAG
抗锯齿。 -
setStyle(Style style); 设置画笔填充类型,共三种:
FILL
填充,STROKE
描边,FILL_AND_STROKE
填充并描边。如果设置填充则画出来的图形是实心的,**不仅仅是针对封闭图形,线段同样有效。**相反,设置描边则仅画出外边框即轮廓。 -
setStrokeWidth(float witdh);设置画笔宽度(粗细)。
-
setAntiAlias(boolean aa); 设置抗锯齿,和setFlags(ANTI_ALIAS_FLAG)效果相同。
-
setDither(boolean d); 设置防抖动,开启后会让图像颜色显示更加平滑,和抗锯齿一样会效果更多的性能,同样也可以通过setFlag设置。
-
setShadowLayer(float radius, float dx, float dy, int shadowColor); 设置阴影,第一个参数为阴影效果因数,越大效果越明显,0则没有,而且设置0还会报错(Excuse me?),第二第三个参数为阴影的偏移量,第三个是阴影的颜色。
-
setShader(Shader shader); 设置着色器,可以看成setColor的豪华升级版。通过这个方法可以设置出很多炫酷的色彩效果。
Shader
BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY); 顾名思义,它是将bitmap作为画笔颜色,需要传入三个对象,第一个不用说,后面两个分别是X轴和Y轴的处理模式。一共有三种模式:CLAMP、MIRROR和REPETA。
- CLAMP 其官方解释为replicate the edge color if the shader draws outside of its original bounds。就是如果着色区域超出了我们设置的Bitmap区域则将Bitmap边缘最后的像素的颜色作为超出区域的颜色。
- MIRROR 其官方解释为repeat the shader’s image horizontally and vertically。着色区域超出了我们设置的Bitmap区域则将Bitmap镜像翻转之后进行着色。
- REPETA 其官方解释为repeat the shader’s image horizontally and vertically, alternating mirror images so that adjacent images always seam。和Mirror类似,不过不会进行镜像处理,而是不停地重复排列。
Gradient
- LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile); 线性渐变,这个是一种比较简单的渐变着色器。参数依次表示颜色起点坐标、颜色结束坐标、起始颜色、结束颜色、处理模式。除了可以设置两种颜色渐变,还有另外一个重载构造方法可以设置多种颜色渐变这里就不展示了。起点坐标和结束坐标决定了颜色从什么位置开始渐变,并且根据渐变长度不同,渐变的程度也随着变化。处理模式和BitmapShader类似。
- SweepGradient(); 梯度渐变,也称之为扫描式渐变,因为其效果有点类似雷达的扫描效果。
- RadialGradient(); 径向渐变,径向渐变说的简单点就是个圆形中心向四周渐变的效果。
- ComposeShader(); 混合渐变,顾名思义可以将多个着色效果混合,需要传入三个参数,前两个为需要混合的着色器对象,最后一个为混合模式。和OpenGL中的
Blend
类似,有兴趣自己去玩玩儿,这里也不具体介绍。
Effect
setPathEffect(PathEffect effect); 从名字上可以看出是专门给Path设置效果的一个方法,(具体是不是只针对Path有效没具体验证过- -!)。
- CornerPathEffect(float radius); 将路径的转角变得圆滑。
- DiscretePathEffect(float segmentLength, float deviation); 离散路径效果,其会在路径上绘制很多“杂点”的突出来模拟一种类似生锈铁丝的效果。第一个参数指定这些突出的“杂点”的密度,值越小杂点越密集,第二个参数则是“杂点”突出的大小,值越大突出的距离越大反之越小。
- DashPathEffect(float intervals[], float phase); 间断的路径效果,将一条连续的路径转换成一条类似虚线的间断线。第一个参数为间断长度的数组,数组的第一个值为实线长度,第二个为虚线,第三个为实线以此类推。第二个参数为间断效果的偏移量,通过不断的修改该值可以达到线段在动的动画效果。
- PathDashPathEffect(Path shape, float advance, float phase, Style style); 和
DashPathEffect
类似,不过前者始终是以线的表现形式,而PathDashPathEffect可以通过传入Path的不同,定义间断出来的形状如圆点,方块等等。 - ComposePathEffect(PathEffect outerpe, PathEffect innerpe); 将两个效果混合,会先将路径变成innerpe的效果,再去复合outerpe的路径效果,即:outerpe(innerpe(Path));
- SumPathEffect(PathEffect first, PathEffect second); 和
ComposePathEffect
类似,会把两种路径效果加起来再作用于路径。
通过上面的介绍,大致流程是这样的:
先通过drawPath画出心电图类似的上下折线图形。
然后设置Paint的Shader属性,通过LinearGradient为Paint添加一个带有透明的线性渐变的特效。
最后通过一个线程循环定时改变线性颜色开始和结束的偏移量达到动态绘制心电图的效果。
同时为了提升显示效果可以为Paint设置一些别的辅助效果和参数。
接下来就是 ShowTime :
下面直接上代码:
public class MyView extends View {
private Paint mPaint;
private int mWindowWidth;
private int mWindowHeight;
private int mOffset;
private Handler mHandler = new Handler();
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mOffset = 0;
mPaint = new Paint();
//设置空心
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
//设置线宽
mPaint.setStrokeWidth(15f);
//设置抗锯齿
mPaint.setAntiAlias(true);
//设置防抖动
mPaint.setDither(true);
//设置阴影
// mPaint.setShadowLayer(25f, 5f, 10f, Color.BLACK);
//初始化渐变颜色,因为要达到真正的透明效果,所以使用两个透明渐变到红色
final int[] colors = new int[]{Color.argb(0, 0, 0, 0), Color.argb(0, 0, 0, 0), Color.RED};
//启用一根新线程进行定时刷新
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
mOffset += 5;
//添加离散效果,让线条变得更加曲折
DiscretePathEffect discretePathEffect = new DiscretePathEffect(3f, 5f);
//添加转角圆滑
CornerPathEffect cornerPathEffect = new CornerPathEffect(90f);
//设置组合PathEffect
mPaint.setPathEffect(new ComposePathEffect(cornerPathEffect, discretePathEffect));
//设置线性渐变
mPaint.setShader(new LinearGradient(mOffset, 0, 800 + mOffset, 15f, colors, null, Shader.TileMode.REPEAT));
//刷新视图
mHandler.post(new Runnable() {
@Override
public void run() {
invalidate();
}
});
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
//获得屏幕尺寸
DisplayMetrics displayMetrics = new DisplayMetrics();
((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
mWindowWidth = displayMetrics.widthPixels;
mWindowHeight = displayMetrics.heightPixels;
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
//设置折线路径
Path path = new Path();
//起始路径
path.moveTo(0, mWindowHeight / 2);
//经过路径
path.lineTo(50, mWindowHeight / 2 + 50);
path.lineTo(100, mWindowHeight / 2 - 50);
path.lineTo(150, mWindowHeight / 2 + 100);
path.lineTo(200, mWindowHeight / 2 - 100);
path.lineTo(250, mWindowHeight / 2 + 150);
path.lineTo(300, mWindowHeight / 2 - 150);
path.lineTo(350, mWindowHeight / 2 + 150);
path.lineTo(400, mWindowHeight / 2 - 150);
path.lineTo(450, mWindowHeight / 2 + 150);
path.lineTo(500, mWindowHeight / 2 - 150);
path.lineTo(550, mWindowHeight / 2 + 100);
path.lineTo(600, mWindowHeight / 2 - 100);
path.lineTo(650, mWindowHeight / 2 + 50);
path.lineTo(700, mWindowHeight / 2 - 50);
path.lineTo(mWindowWidth, mWindowHeight / 2);
//drawPath
canvas.drawPath(path, mPaint);
}
}