今天将为大家带来 粗略版 酷狗音乐 歌词播放的效果。我们一步一步来。首先做这个是因为有一次公司项目中需要做一个汽车扫描效果的时候,想到来做这个歌词播放效果的。那么我们这次先上效果图:
好的上面的文字是我们要实现的效果,在那之前先说说这个汽车扫描的实现,这样或许更容易理解后面的歌词播放原理。好的,那么我先开始汽车扫描部分的思路说明,先上代码:
package com.example.scanview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
public class CarCheckView extends View {
/**
* 定义背景bitmap,扫描过后的bitmap和扫描线
*/
private Bitmap car, lineCar, line;
/**
* 画笔
*/
private Paint paint;
/**
* 图形操作工具类
*/
private Matrix matrix;
/**
* Y坐标
*/
private int y = 60;
/**
* 是否正在扫描
*/
private boolean isCanning = false;
public CarCheckView(Context context) {
super(context);
initData();
}
public CarCheckView(Context context, AttributeSet attrs) {
super(context, attrs);
initData();
}
/**
* 注意了:美工切图时car和carline这两个图的宽高大小要一致
*/
private void initData() {
car = BitmapFactory.decodeResource(getResources(), R.drawable.car);
lineCar = BitmapFactory.decodeResource(getResources(), R.drawable.carline);
line = BitmapFactory.decodeResource(getResources(), R.drawable.check_line);
paint = new Paint();
paint.setAntiAlias(true);
matrix = new Matrix();
}
public boolean isCanning() {
return isCanning;
}
/**
* @param isCanning
*
* true 开始扫描 : false 停止扫描
*/
public void setScan(boolean isCanning) {
this.isCanning = isCanning;
if (isCanning) {
} else {
y = 60;
}
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
matrix.reset();
// 不断得扫描
if (y <= car.getHeight())
y += 3;
else
y = 60;
if (isCanning) {// 如果开始扫描了,才显示扫描线,和扫描后的bm
/**
* draw扫描后出现的bm,这里无需改变
*/
canvas.drawBitmap(lineCar, matrix, paint);// 三个参数的,默认为left:0,top:0
/**
* 最后draw扫描线,这里扫描线的位置要相对于car居中,所以取car和line的宽度的一半就是
*/
canvas.drawBitmap(line, (car.getWidth() - line.getWidth()) / 2, y - (line.getHeight() >> 1), paint);
invalidate();
}
/**
* draw扫描后逐渐隐藏的bm,这里要对Y(top)作改变
*/
scanImage(canvas, car, y);
}
private void scanImage(Canvas canvas, Bitmap bm, int y) {
Rect src = new Rect();
Rect dst = new Rect();
src.left = 0;
src.top = y;
src.right = bm.getWidth();
src.bottom = bm.getHeight();
dst.left = 0;
dst.top = y;
dst.right = bm.getWidth();
dst.bottom = bm.getHeight();
canvas.drawBitmap(bm, src, dst, paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(car.getWidth(), car.getHeight());
}
}
那么首先这个是一个自定义的view,我们先写一个CarCheckView继承于View,然后在初始化的时候先初始化一些必要的数据。看上面的效果图可以发现,该view需要绘制3个图,要准备3张切图资源,一个是真实车身图,另外一个是线型车身图,还有就是一条扫描线了。
那么这边主要就对ondraw()方法进行说明,在ondraw()方法中我们要考虑的是先绘制哪一张图,一定要注意绘制的顺序,看效果真实车身是在最上面的,所以这个最后再来draw,其它2个则不用考虑,然后车身图跟线型车身图的宽高大小是一致的,位置也是一致的,还有就是设置扫描线的位置,具体在代码中也有注释说明。扫描线是draw在车身上面再高一点的距离,然后慢慢向下进行扫描,
所以这边draw线的Y坐标是个变量,而且跟draw感觉大小可变的真实车身图的Y坐标是一致的,那么draw真实车身,这边我们需要用带4个参数的方法进行绘制了,这4个参数的作用代码中已作说明,因为我们是由上往下进行一个扫描效果,那么我们就控制draw真实车身图的区域和显示在屏幕上的区域,即是中间的那两个参数所表示的区域,这边这样说可能比较难于理解,大家可以代入不同的值进行设置再看看效果即可。最后再设置一些临界值就可以了,扫描线超过车身时,从新退回来继续扫描,设置扫描开始,扫描结束等,那么这样就完成了汽车扫描部分了。
好了,那么说到这里的时候,不知各位同学是否已经对歌词播放效果的实现有了一定的思路了呢?
那么下面开始我们的歌词播放效果部分内容,还是一样先上代码:
package com.example.scanview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class MyGround extends RelativeLayout {
private TextView redText, blueText;
private Bitmap blueBitmap;
private Bitmap redBitmap;
private Paint paint;
/**
* 是否正在播放歌词
*/
private boolean isSinging = false;
private int left = 0;
public MyGround(Context context) {
super(context);
initPaint();
}
public MyGround(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
paint = new Paint();
paint.setStrokeWidth(5);
paint.setColor(Color.BLACK);
}
/**
* viewgroup若是要实现wrap_content效果,则应该先测量子view的大小,然后根据子view的大小再去测量自身的大小
*
* 如果直接测量自身的大小会占屏幕宽高
*
* 要想父view的大小随子view的宽高变化而变化,必须先测量子view的大小
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 直接测量自身大小
// int measureWidth = getMeasureWHSize(widthMeasureSpec);
// int measureHeight = getMeasureWHSize(heightMeasureSpec);
// setMeasuredDimension(measureWidth, measureHeight);
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
v.measure(widthMeasureSpec, heightMeasureSpec);
}
setMeasuredDimension(getChildAt(0).getMeasuredWidth(), getChildAt(0).getMeasuredHeight());
}
/**
* 获取测量值
*
* @param measureSpec
* @return
*/
private int getMeasureWHSize(int measureSpec) {
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = size;
break;
}
return result;
}
@Override
/**
* 默认会执行两次
*/
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// 当布局发生改变时,才向下执行
if (!changed)
return;
int childs = getChildCount();
for (int i = 0; i < childs; i++) {
View v = getChildAt(i);
v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
}
if (redBitmap == null || blueBitmap == null) {
redText = (TextView) findViewById(R.id.text1);
blueText = (TextView) findViewById(R.id.text2);
redBitmap = getBitmapByView(redText);
blueBitmap = getBitmapByView(blueText);
redText.setVisibility(View.INVISIBLE);
blueText.setVisibility(View.INVISIBLE);
}
}
private Bitmap getBitmapByView(View view) {
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
return view.getDrawingCache();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onDraw(Canvas canvas) {
drawBackgroupText(canvas);
drawFrontgroupText(canvas);
}
/**
* canvas.drawBitmap(bitmap, src, dst,paint)
* 参数1、 位图
* 参数2、 为要画的bitmap的区域
* 参数3、为要显示在屏幕上的区域,即是绘制在屏幕的什么地方
* 参数4、画笔
*
* @param canvas
*/
private void drawFrontgroupText(Canvas canvas) {
Rect srcBlue = new Rect();
Rect dstBlue = new Rect();
srcBlue.left = left;
srcBlue.top = 0;
srcBlue.right = blueBitmap.getWidth();
srcBlue.bottom = blueBitmap.getHeight();
dstBlue.left = left;
dstBlue.top = 0;
dstBlue.right = blueBitmap.getWidth();
dstBlue.bottom = blueBitmap.getHeight();
canvas.drawBitmap(blueBitmap, srcBlue, dstBlue, paint);
if (isSinging) {
left++;
if (left <= srcBlue.right)
invalidate();
}
}
private void drawBackgroupText(Canvas canvas) {
Rect srcRed = new Rect();
Rect dstRed = new Rect();
srcRed.left = 0;
srcRed.top = 0;
srcRed.right = redBitmap.getWidth();
srcRed.bottom = redBitmap.getHeight();
dstRed.left = 0;
dstRed.top = 0;
dstRed.right = redBitmap.getWidth();
dstRed.bottom = redBitmap.getHeight();
canvas.drawBitmap(redBitmap, srcRed, dstRed, paint);
}
/**
* @param isSinging
*
* true 开始播放歌词 : false 停止播放歌词
*/
public void setSing(boolean isSinging) {
this.isSinging = isSinging;
left = 0;
postInvalidate();
}
public boolean isSinging() {
return isSinging;
}
}
这边我是这样做的,如果其他大虾有更好更容易的方法,请多多赐教。我们这边为什么要继承于ViewGroup?回看汽车扫描部分的代码,draw那个车身大小可变的车身图所用到的canvas的方法所接收的参数是一个bitmap类型,那么歌词是可变的,我们总不可能叫美工切歌词的图,既然这样那我们只能用文本可随时变化的textview了
好的,这边的实现跟汽车扫描部分的实现原理基本上是一样的。那么我们只看其中几个不同的地方和需要注意的细节问题。
好了,我们先在textview中设置要播放的歌词,再调用上面的方法getBitmapByView获取view的缩略图,再将textview隐藏掉,然后在textview的位置draw上刚刚获取的view的缩略图就可以了,这边是要两个textview来存放的,一个是未播放时的颜色,另一个是播放过后的颜色,当播放完毕的时候再从新操作一遍就可以了,我这边为了方便省事就没去做了,
还有得就是汽车扫描部分是从上到下,而歌词播放部分则是由左及右,变量改成left即可,另外还需注意的地方就是,我们这个viewgroup的宽高大小要依据于这两个textview的大小来定量,所以在onMeasure()方法中我们要先去测量两个子Textview的大小,然后获取两个子textview的测量大小来设置viewgroup自身的测量值。
另外viewgroup一开始时的onMeasure会连续执行2次,onlayout执行一次,然后onMeasure再执行2次,onlayout再执行一次,考虑到性能方面大家可以自己作标志让其只加载一次就好,当然我这边也没做,图省事了。
好了,还有最后一个需要注意的地方,因为我们这个是自定义Viewgroup,跟自定义view还是有差距的,自定义view的view启动时ondraw()方法默认会启动一次,而Viewgroup的则不会,但是在xml布局时设置上Viewgroup的背景则会执行,这个我在这里也解释不了,估计要去查看源码才能说明了。
好了,到这里基本上大功告成了!
源码点击下载