上一次,我们实现了下雨的效果,这次,我们来实现音量变化的效果。先看看效果图:
接下来,就开始我们的实现流程吧。
第一步:创建BaseMusicView
首先,我们将上次使用的 BaseRainView.class 拷贝一下(该文件内容在博主的另一篇博文《粒子效果之雨的实现》中有讲解),修改名称为 BaseMusicView,并修改相应方法名等内容如下:
import android.content.Context;
import android.graphics.Canvas;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/**
* @author ailsa
* <p>
* 2019/3/12 0012
* <p>
* BaseMusicView,音量效果的基础View
*/
public abstract class BaseMusicView extends View {
/**
* 自定义线程,实现音量条的移动效果
*/
private MThread thread;
public BaseMusicView(Context context) {
super(context);
}
public BaseMusicView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 初始化所有音量条 [ 子类实现 ]
*/
protected abstract void initVolumes();
/**
* 绘制所有音量条 [ 子类实现 ]
*
* @param canvas 画布
*/
protected abstract void drawVolumes(Canvas canvas);
/**
* 移动所有音量条 [ 子类实现 ]
*/
protected abstract void moveVolumes();
@Override
protected void onDraw(Canvas canvas) {
if (thread == null) {
initVolumes(); // 初始化所有音量条
thread = new MThread();
thread.start();
} else {
drawVolumes(canvas); // 绘制所有音量条
}
}
class MThread extends Thread {
@Override
public void run() {
long workTime;
while (true) {
moveVolumes(); // 移动所有音量条
postInvalidate(); // 调用onDraw()重绘
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
第二步:创建单个音量条
接下来,我们创建一个 Volume.class 文件,实现单个音量条效果。
同单个雨点的实现一样,我们需要 drawSingleVolume(Canvas canvas) 、 moveSingleVolume() 方法实现单个音量条的绘制和变化。
我们知道,音量条是一个个小矩形纵向排列组成,在使用 drawRect() 方法绘制矩形之前,我们先分析一下如何在界面上绘制一个矩形:
根据上图大家可以看到,left 参数就是矩形左边距离屏幕左边的距离;right 参数就是 left +矩形宽度;和我们的理解不同的是——因为android屏幕是从上至下为Y轴正方形,所以 top 参数应该是矩形底部距离屏幕上方的距离,而 bottom 参数应该是矩形顶部距离屏幕上方的距离
。这一点大家一定要注意了,千万不要将 top、bottom 参数和屏幕的底部、顶部弄反了。
分析完 drawRect() 的四个坐标参数后,我们来分析一下如何构建一个由多个小矩形组成的音量条:
从上图我们可以清晰的了解到每个矩形的 left、right 坐标和单个矩形是一样的。不同的是每个矩形的 top、bottom 参数发生了变化。从下至上,第一个矩形的 top = screenHeight,bottom = screenHeight - rectHeight;第二个矩形的top = screenHeight - rectHeight - rectTopSpace,bottom = screenHeight - rectHeight * 2 - rectTopSpace…以此类推,我们可以得到这两个公式:
top = screenHeight - rectNum * (rectHeight + rectTopSpace);
bottom = screenHeight - ((rectNum + 1) * rectHeight + rectNum * rectTopSpace);
【注】 rectNum是指当前绘制的矩形是第几个,从零开始。为什么从零开始呢?这是因为第一个矩形的 top = screenHeight,如果 rectNum 从1开始,将会导致矩形坐标计算错误,绘制出不正确的矩形。
得到计算方式后,我们开始创建第一个音量条吧。
首先,声明参数,并在构造函数里初始化:
/**
* 每个矩形的宽度和高度、边距
*/
private int rectHeight;
private int rectWidth;
private int rectTopSpace;
private int rectLeftSpace;
/**
* 屏幕高度和宽度
*/
private int screenHeight;
/**
* 画笔
*/
private Paint paint;
/**
* 第几个矩形
*/
private int rectNum;
SingleVolume(int height) {
this.screenHeight = height;
init();
}
private void init() {
paint = new Paint();
paint.setColor(0xffffffff); // 页面背景为黑色,此处设置画笔颜色为白色
rectHeight = 20;
rectWidth = 30;
rectLeftSpace = 10;
rectTopSpace = 5;
}
其次,在 drawSingleVolume() 中绘制音量条:
public void drawSingleVolume(Canvas canvas) {
for (int i = 0; i < verticalCount; i++) { // 使用for循环绘制多个矩形
paint.setColor(0xffffffff);
int left = rectLeftSpace;
int top = screenHeight - rectNum * (rectHeight + rectTopSpace);
int right = rectLeftSpace + rectWidth;
int bottom = screenHeight - ((rectNum + 1) * rectHeight + rectNum * rectTopSpace);
canvas.drawRect(left, top, right, bottom, paint);
rectNum++;
}
}
我们在此处使用了 for 循环判断每个矩形的 top、bottom 参数,并进行绘制。判断条件是 i < verticalCount
,其中,verticalCount 是指单个音量条在竖直方向上的数量。那这个verticalCount应该如何定义呢??
verticalCount肯定不可能是固定的,因为我们要实现的是变化的音量条。另外,每个小矩形的大小也不能不一致,所以,我们可以这样来初始化verticalCount参数:
verticalCount = (screenHeight - random.nextInt(screenHeight)) / (rectHeight + rectTopSpace);
为什么是这样来定义呢??
首先,我们随机在屏幕上生成一个高度值,但是因为屏幕是由上至下为正方向,而我们绘制的矩形由下至上为正方向,所以应该用屏幕高度减去随机生成的高度值,在剩下的区域内绘制我们需要的音量条。另外,因为每两个小矩形之间有一个间距,所以将一个矩形高度和一个间距看做一个整体,用可绘制区域的高度除以这个整体的高度,就得到 verticalCount 的值了。这样,保证了绘制出来的音量条中的每个矩形以及矩形之间的间距是保持一致的。
音量条的绘制逻辑处理完成后,我们接着添加一下变化逻辑。
音量条变化主要是由组成音量条的矩形个数的变化来实现的,大家仔细看一下音量条的变化过程即可知道。所以,我们直接将刚刚初始化 verticalCount 的那行代码放在 moveSingleVolume() 中即可:
public void moveSingleVolume() {
verticalCount = (screenHeight - random.nextInt(screenHeight)) / (rectHeight + rectTopSpace);
}
如此,单个音量条的实现过程就编写完成了。我们接下来添加多个音量条。
第三步:创建多个音量条
同样的,我们需要创建一个 MusicView.class 文件,让它继承 BaseMusicView 。然后添加两个参数—— volumes 和 volumeCount,分别代表多个音量条的集合和音量条的数量,并实现 initVolumes() 、drawVolumes() 、moveVolumes() 三个方法:
import android.content.Context;
import android.graphics.Canvas;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import java.util.ArrayList;
import java.util.List;
/**
* @author ailsa
* <p>
* 2019/3/12 0012
* <p>
* MusicView,音量条的具体实现
*/
public class MusicView extends BaseMusicView {
/**
* 音量条集合
*/
private List<SingleVolume> volumes;
/**
* 音量数量
*/
private int volumeCount;
public MusicView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
volumes = new ArrayList<>();
volumeCount = 20;
}
@Override
protected void initVolumes() {
for (int i = 0; i < volumeCount; i++) {
volumes.add(new SingleVolume(getHeight()));
}
}
@Override
protected void drawVolumes(Canvas canvas) {
for (SingleVolume volume : volumes) {
volume.drawSingleVolume(canvas);
}
}
@Override
protected void moveVolumes() {
for (SingleVolume volume : volumes) {
volume.moveSingleVolume();
}
}
}
当我们运行项目时仍然发现只有一个音量条,并没有我们预想的20个音量条。原因是什么呢??
因为我们绘制的每个音量条的 left、right 坐标都是一样的,导致重叠了。那我们怎么将这20个音量条并列显示在屏幕中呢??先来看一个分析图:
根据上图,我们可以轻松的得到每个音量条的left、right参数的计算公式:
left = rectLeftSpace * N + rectWidth * (N-1);
right = (rectLeftSpace + rectWidth) * N;
但是此处我们不用N(第几个音量条)来计算,我们使用 lastVolumeRight 参数——上一个音量条的右坐标,这个参数由外部调用 getLastVolumeRight() 后传入:
/**
* 上一个音量条的右坐标
*/
private int lastVolumeRight;
SingleVolume(int height, int width, int lastVolumeRight) {
this.screenHeight = height;
this.lastVolumeRight = lastVolumeRight;
init();
}
public int getLastVolumeRight() {
return lastVolumeRight + rectWidth + rectLeftSpace;
}
所以,drawSingleVolume() 中的left、right参数应该修改为:
public void drawSingleVolume(Canvas canvas) {
for (int i = 0; i < verticalCount; i++) {
int left = rectLeftSpace + lastRectRight;
int right = lastRectRight + rectLeftSpace + rectWidth;
...
}
}
我们还注意到一个现象,这些音量条好像直接绘制到了屏幕顶端,高度并没有在屏幕范围内上下变化,这又是为什么呢??
我们忘了在每一次重新绘制时将 rectNum 初始化为 0 了,因为我们在 drawSingleVolume() 的for循环中使用了rectNum++;
在竖直方向上绘制矩形,而 BaseMusicView 中使用了while循环调用 postInvalidate() 在每一次变化后重新绘制音量条,所以如果不在每一次重新绘制时将rectNum置为0,那么 rectNum 的值会在上一次绘制结束时的值上累加,最终在屏幕上绘制出来的音量条就是铺满整个屏幕的。
考虑到屏幕中的音量条变化得过快,所以我们可以将 BaseMusicView 中Thread的休眠时间提取成参数:
/**
* Thread睡眠时间
*/
protected int sleepTime;
public BaseMusicView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
sleepTime = 30;
}
我们就可以在 MusicView 中使用sleepTime改变变化速度了:
@Override
protected void drawVolumes(Canvas canvas) {
for (SingleVolume volume : volumes) {
volume.drawSingleVolume(canvas);
}
sleepTime = 500; // 改变音量条变化速度的参数
}
进行到这里,一组向上变化的音量条就完成了。为了达到效果,我们还需要添加反方向的音量条。
第四步:绘制反方向的音量条
根据文章第一部分展示的效果图,我们可以分析出以下几点:
- 上下方向的音量条组均分屏幕高度;
- 向下方向的音量条的左右坐标与向上方向的一致;
- 向上方向的音量条的top、bottom参数是在screenHeight/2的基础上累减计算;
- 向下方向的音量条的top、bottom参数是在screenHeight/2的基础上累加计算。
基于这几条分析结果,我们可以修改***SingleVolume.class***文件中的代码如下:
public void drawSingleVolume(Canvas canvas) {
rectNum = 0;
for (int i = 0; i < verticalCount; i++) {
// 向上方向的音量条
int left = rectLeftSpace + lastRectRight;
int top = screenHeight / 2 - rectNum * (rectHeight + rectTopSpace);
int right = lastRectRight + rectLeftSpace + rectWidth;
int bottom = screenHeight / 2 - ((rectNum + 1) * rectHeight + rectNum * rectTopSpace);
canvas.drawRect(left, top, right, bottom, paint);
// 向下方向的音量条
int mirrorLeft = left;
int mirrorTop = screenHeight / 2 + rectNum * (rectHeight + rectTopSpace);
int mirrorRight = right;
int mirrorBottom = screenHeight / 2 + ((rectNum + 1) * rectHeight + rectNum * rectTopSpace);
canvas.drawRect(mirrorLeft, mirrorTop, mirrorRight, mirrorBottom, paint);
rectNum++;
}
}
public void moveSingleVolume() {
verticalCount = (screenHeight / 2 - random.nextInt(screenHeight / 2)) / (rectHeight + rectTopSpace);
}
private void init() {
...
verticalCount = (screenHeight / 2 - random.nextInt(screenHeight / 2)) / (rectHeight + rectTopSpace);
}
为了区分上下方向的音量条,我们在for循环中分别给上下方向的音量条设置一个画笔颜色:
for (int i = 0; i < verticalCount; i++) {
// 向上方向的音量条
paint.setColor(0xffffffff);
...
// 向下方向的音量条
paint.setColor(0xff0000ff);
...
}
然后我们再运行项目看一下效果,这个时候我们会屏幕中绘制出来的上下方向的音量条没有间距分割,导致屏幕中间部分拼接在一起了。我们需要给一个分割值,假设为10,为了保证上下部分是均分的,在计算上下方向的音量条的top、bottom参数时,我们应该在上方向-5,在下方向+5,所以修改SingleVolume的内容如下:
public void drawSingleVolume(Canvas canvas) {
rectNum = 0;
for (int i = 0; i < verticalCount; i++) {
// 向上方向的音量条
int left = rectLeftSpace + lastRectRight;
int top = screenHeight / 2 - 5 - rectNum * (rectHeight + rectTopSpace);
int right = lastRectRight + rectLeftSpace + rectWidth;
int bottom = screenHeight / 2 - 5 - ((rectNum + 1) * rectHeight + rectNum * rectTopSpace);
canvas.drawRect(left, top, right, bottom, paint);
// 向下方向的音量条
int mirrorLeft = left;
int mirrorTop = screenHeight / 2 + 5 + rectNum * (rectHeight + rectTopSpace);
int mirrorRight = right;
int mirrorBottom = screenHeight / 2 + 5 + ((rectNum + 1) * rectHeight + rectNum * rectTopSpace);
canvas.drawRect(mirrorLeft, mirrorTop, mirrorRight, mirrorBottom, paint);
rectNum++;
}
}
public void moveSingleVolume() {
verticalCount = (screenHeight / 2 - 10 - random.nextInt(screenHeight / 2 - 10)) / (rectHeight + rectTopSpace);
}
private void init() {
...
verticalCount = (screenHeight / 2 - 10 - random.nextInt(screenHeight / 2 - 10)) / (rectHeight + rectTopSpace);
}
至此,整个项目就完成了,我们就可以看到文章第一部分展示的效果了。
最后,我们可以对整个项目进行一个优化,下面给出几点建议:
- 将 screenHeight - 2 / 10 等运算代码提取成参数,避免重复计算;
- 将每个小矩形的宽、高、左间距、上间距由外部传入,让用户实现控制;
- 将音量条的颜色、数量等参数设置成属性,让用户可以在xml中使用。
当然,还可以进行其他的优化,此处只提供一个学习的实例,欢迎大家自行实现更加优雅漂亮的效果。
项目地址:https://github.com/Ailsa2019/starfiled