本篇文章介绍自定义SurfaceView来实现如下的效果
由于对于SurfaceView不是很熟练,这次拿它来练手
一般View可以满足大部分的绘图需求,但如果需要并发执行复杂耗时的逻辑的时候,就会不断阻塞主线程,导致画面卡顿,为了避免这种问题的发生,我们应该使用SurfaceView来解决这个问题
SurfaceView使用介绍可以参考另外一篇博客:Android绘图机制与处理技巧(一)SurfaceView
创建自定义SurfaceView需要继承自SurfaceView,并实现SurfaceHolder.Callback, Runnable接口
public class CircleRecordSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
然后需要重写三个方法:surfaceCreated(),surfaceChanged(),surfaceDestroyed()和run()
定义的成员变量,具体用途可以看注释
//选择按钮图标
private boolean isChangeCenterBitmap = true;
//持续画图
private boolean isSustainedDraw = false;
private boolean isStart = true;
//是否画小圆点,默认为true
private boolean isDrawSmallCircle = true;
//小圆点颜色
private int smallCircleColor;
//是否画圆弧,默认为true
private boolean isDrawArc = true;
//圆弧颜色
private int arcColor;
private CompleteTimeCallBack completeTimeCallBack;
private SurfaceHolder holder = null;
//绘图属性---------
private Canvas canvas;
//录音按钮
private Paint pPaint;
private int px;//坐标x位置
private int py;//坐标y位置
//radius = defaultRadius * dp
private int radius;//半径
//defaultRadius 默认值为40
private int defaultRadius = 40;
//起始角度
private float startAngle = 270;
//进度
private float sweepAngle;
//小球起始角度默认等于进度条起始角度
private float angle, duration = 20;
private int startBitmap;
private int stopBitmap;
//中心图片的范围,默认为10,值越大图片越小
private int centerBitmap_margin = 10;
private int dp;
private Bitmap bitmap;
private boolean isGetBitmap = false;
long a, b, calculateTime, sleepTime = 60, correctSleepTime;
然后我们需要对SurfaceHolder以及一些其他的属性初始化
public void init() {
//获取mSurfaceHolder
holder = getHolder();
holder.addCallback(this);
//背景设为透明
if (!isInEditMode()) {
setZOrderOnTop(true);
}
holder.setFormat(PixelFormat.TRANSLUCENT);
//设置进度条
pPaint = new Paint();
pPaint.setAntiAlias(true);
pPaint.setStrokeWidth(4);
pPaint.setStyle(Paint.Style.STROKE);
angle = startAngle;
sweepAngle = 0;
dp = Resources.getSystem().getDisplayMetrics().densityDpi / 160;
}
重写SurfaceView的surfaceCreated()方法
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!isStart) {
reset();
}
radius = defaultRadius * dp;
isChangeCenterBitmap = true;
px = this.getWidth() / 2;
py = this.getHeight() / 2;
new Thread(this).start();
}
在这里根据子线程标志位做初始化,以及计算半径,中心点坐标等等,并开启线程
由于我们不用改变SurfaceView大小因此无需在surfaceChanged()方法中写逻辑
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
重写SurfaceView的surfaceDestroyed()方法,在这里将标志位设为false
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isStart = false;
}
然后重写run()方法,该方法是一个子线程,在这里通过不停循环才进行绘制界面
@Override
public void run() {
while (isStart) {
if (isSustainedDraw) {
canvas = holder.lockCanvas(); // 获得画布对象,开始对画布画画
if (canvas == null) {
continue;
}
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// canvas.drawColor(canvasColor); // 把画布填充指定颜色
drawCenterBitmap();
drawCircle();
holder.unlockCanvasAndPost(canvas); // 完成画画,把画布显示在屏幕上
calculateTime = calculateTime + sleepTime;
try {
b = a;
a = System.currentTimeMillis();
if (b == 0) {
correctSleepTime = sleepTime;
} else {
if ((a - b) >= sleepTime && (a - b) < 2 * sleepTime) {
correctSleepTime = sleepTime - (a - b - correctSleepTime);
} else if ((a - b) > 2 * sleepTime) {
correctSleepTime = 0;
//不睡眠
} else if ((a - b) < sleepTime) {
correctSleepTime = sleepTime - (a - b - correctSleepTime);
}
}
if (correctSleepTime > 0) {
Thread.sleep(correctSleepTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (isChangeCenterBitmap) {
canvas = holder.lockCanvas(); // 获得画布对象,开始对画布画画
if (canvas == null) {
continue;
}
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// canvas.drawColor(canvasColor); // 把画布填充指定颜色
drawCenterBitmap();
drawCircle();
holder.unlockCanvasAndPost(canvas); // 完成画画,把画布显示在屏幕上
}
}
}
有几个要注意的地方:
画按钮图形的方法
private void drawCenterBitmap() {
int area = radius - centerBitmap_margin * dp;
RectF imageRect = new RectF(px - area, py - area, px + area, py + area);
if (isChangeCenterBitmap) {
if (!isGetBitmap) {
if (isSustainedDraw) {
bitmap = BitmapFactory.decodeResource(getResources(), stopBitmap);
} else {
bitmap = BitmapFactory.decodeResource(getResources(), startBitmap);
}
}
isChangeCenterBitmap = false;
}
canvas.drawBitmap(bitmap, null, imageRect, pPaint);
}
通过isSustainedDraw判断后使用canvas.drawBitmap()方法绘制相应的图片
通过这个方法绘制灰色大圆、绿色圆点和圆弧
public void drawCircle() {
//绘制大圆
pPaint.setColor(Color.LTGRAY);
pPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(px, py, radius, pPaint);
//绘制原点
if (isDrawSmallCircle) {
pPaint.setColor(smallCircleColor);
pPaint.setStyle(Paint.Style.FILL);
//radians=angle * Math.PI / 180 角度转弧度公式
//Math.cos(radians) * defaultRadius cos计算x轴偏移量,sin计算y轴偏移量
float ballX = (float) (px + radius * Math.cos(angle * Math.PI / 180));
float ballY = (float) (py + radius * Math.sin(angle * Math.PI / 180));
canvas.drawCircle(ballX, ballY, 4 * dp, pPaint);
}
//绘制圆弧
if (isDrawArc) {
pPaint.setStyle(Paint.Style.STROKE);
// pPaint.setColor(Color.parseColor(arcColor));
pPaint.setColor(arcColor);
RectF rect = new RectF(px - radius, py - radius, px + radius, py + radius);
canvas.drawArc(rect, startAngle, sweepAngle, false, pPaint);//画弧形
}
float speed = 360 / (duration * (1000 / sleepTime));
// Log.i(TAG, "drawCircle: speed:"+speed);
angle = angle + speed;
if (angle > 360) {
angle = 0;
}
sweepAngle = sweepAngle + speed;
if (sweepAngle > 360) {
reset();
if (completeTimeCallBack != null) {
completeTimeCallBack.stop();
}
isSustainedDraw = false;
isChangeCenterBitmap = true;
}
}
画大圆较简单,设置好paint属性后使用drawCircle()方法即可
画圆弧也比较容易,设置好RectF类的坐标和paint属性后,使用drawArc()方法即可
需要注意的是画圆点,重点在于怎么计算圆点围绕中心旋转时的坐标:
我们可以使用Math.cos(radians) * radius来计算X轴偏移量,Math.sin(radians) * radius来计算Y轴偏移量,radians表示弧度,可以通过angle * Math.PI / 180来计算
用到的包装方法,可以设置一些属性
public void startDraw() {
isChangeCenterBitmap = true;
isSustainedDraw = true;
}
public void stopDraw() {
isChangeCenterBitmap = true;
isSustainedDraw = false;
}
public void reset() {
isStart = true;
angle = startAngle;
sweepAngle = 0;
}
//设置圆弧颜色,用#RRGGBB 或者 #AARRGGBB
public void setArcColor(int arcColor) {
this.arcColor = arcColor;
}
//设置小圆点颜色,用#RRGGBB 或者 #AARRGGBB
public void setSmallCircleColor(int smallCircleColor) {
this.smallCircleColor = smallCircleColor;
}
public void setDefaultRadius(int defaultRadius) {
this.defaultRadius = defaultRadius;
}
public void setStartBitmap(int startBitmap) {
this.startBitmap = startBitmap;
}
public void setStopBitmap(int stopBitmap) {
this.stopBitmap = stopBitmap;
}
public void setDuration(float duration) {
this.duration = duration;
}
xml布局
<com.gavinandre.customviewsamples.view.CircleRecordSurfaceView
android:id="@+id/circle_record_view"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_centerInParent="true"/>
使用方法
mCircleRecordView.setDuration(6);
mCircleRecordView.setStartBitmap(R.mipmap.audio_record_mic_btn);
mCircleRecordView.setStopBitmap(R.mipmap.audio_record_mic_btn_press);
mCircleRecordView.setArcColor(ContextCompat.getColor(this, R.color.record_green));
mCircleRecordView.setSmallCircleColor(ContextCompat.getColor(this, R.color.record_green));
mCircleRecordView.setDefaultRadius(50);
mCircleRecordView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mCircleRecordView.startDraw();
break;
case MotionEvent.ACTION_UP:
mCircleRecordView.reset();
mCircleRecordView.stopDraw();
break;
default:
break;
}
return true;
}
});
如果要写其他逻辑的话在onTouch方法里添加即可
代码示例:
CustomViewSamples