这次记录一下使用自定义的SurfaceView做一个简单的案例。
SurfaceView相较于普通的View,它的绘制操作在一个子线程中进行的,而普通的View是在UI线 程中绘制的,基于这个原因,SurfaceView的优势就是:可以避免UI线程的阻塞。由于SurfaceView的这个特性,很多游戏都是基于它来完成。
这里就有一个问题,那就是SurfaceView没有在UI线程进行绘制,也就是它没在OnDraw方法里面进行相关的绘制的操作,那SurfaceView的绘制在哪里进行的呢?
其实SurfaceView中包含一个专门用于绘制的Surface,这个Surface中包含了一个Canvas。通常我们可以这样拿到Canvas:通过getHolder()方法来获得一个SurfaceHolder,在这个Holde里面就含有Canvas,同时Holder还管理着SurfaceView的生命周期
SurfaceView的生命周期方法:
1.surfaceCreated() ,通常我们在这个方法里面进行一些变量的初始化和将绘制图形的线程进行开启。
2.surefaceChanged()
3.surfaceDestroyed() 在这个方法里面,我们通常将绘制图形的线程进行关闭
1.SurfaceView的固定格式
通常我们在使用SurfaceView的时候,都有一个固定的格式,如下:
package com.example.android_luckpan;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* Created by 前世诀别的一纸书 on 2017/2/25.
*/
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback , Runnable{
private SurfaceHolder mHolder = null;
private Canvas mCanvas = null;
private boolean mIsRuning = false;
private Thread t = null;
public SurfaceViewTemplate(Context context) {
super(context);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
super(context, attrs);
mHolder = getHolder();
mHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsRuning = true;
t = new Thread(this);
t.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsRuning =false;
}
@Override
public void run() {
while (mIsRuning)
{
draw();
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
if(mCanvas != null)
{
//draw something
}
} catch (Exception e) {
} finally {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
我们在使用SurfaceView的时候,通常是在这个模板代码上来添加我们的东西。
现在回到我们这个案例的主题上来。
2.定义变量
这个的变量有很多,同时初始化的位置也不相同。
private SurfaceHolder mHolder = null;
private Canvas mCanvas = null;
//线程开关--用来控制线程是否启动
private boolean mIsRuning = false;
//用来判断是否点击了停止按钮
private boolean mIsShouldEnd = false;
private Thread t = null;
//转盘上每一项上的图片
private int mImgs[] = new int[]{R.mipmap.danfan, R.mipmap.ipad, R.mipmap.f040, R.mipmap.iphone, R.mipmap.meizi, R.mipmap.f015};
//转盘上每一项的文字
private String strs[] = new String[]{"单反相机", "IPAD", "恭喜发财", "IPHONE", "服装一套", "恭喜发财"};
//转盘上每一项的颜色
private int mColors[] = new int[]{Color.parseColor("#FF4500"), Color.parseColor("#FF69B4"), Color.parseColor("#FF4500"), Color.parseColor("#FF69B4"), Color.parseColor("#FF4500"), Color.parseColor("#FF69B4")};
//将转盘上每一项的图片转换Bitmap类型
private Bitmap mImgsBitmap[] = null;
//转盘的背景图片
private Bitmap mBgBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.bg2);
//绘制圆盘每一项的画笔
private Paint mArcPaint = null;
//绘制转盘上每一项上文字的画笔
private Paint mTextPaint = null;
private int mPadding = 0;
private int mRadius = 0;
private int mItemCount = 6;
private int mCenter = 0;
//转盘开始转动的初始角度
private float mStartAngle = 0;
//转盘转动的速度
private volatile float mSpeed = 0;
private RectF mRange = null;
private int mTextColor = Color.parseColor("#FFF5EE");
private int mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics());
3.初始化变量和测量SurfaceView的大小
在测量SurfaceView的大小的时候,一定要注意:我们要保证SurfaceView是正方形的,因为转盘是圆形,正方形才好控制圆形的位置和大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = Math.min(getMeasuredHeight(), getMeasuredWidth());
mPadding = getPaddingLeft();
mRadius = width - 2 * mPadding;
mCenter = width / 2;
setMeasuredDimension(width, width);
}
我们在surfaceCreated方法中初始化一些变量
public void surfaceCreated(SurfaceHolder holder) {
Log.i("main", "1");
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setDither(true);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setDither(true);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
mRange = new RectF(mPadding, mPadding, mPadding + mRadius, mPadding + mRadius);
mImgsBitmap = new Bitmap[mItemCount];
for (int i = 0; i < mItemCount; i++) {
mImgsBitmap[i] = BitmapFactory.decodeResource(getContext().getResources(), mImgs[i]);
}
Log.i("main", "3");
mIsRuning = true;
t = new Thread(this);
t.start();
}
3.绘制背景图片
&emsp本质上我们应该在子线程的run方法里面绘制,我们将绘制的代码提取成了一个方法。
@Override
public void run() {
Log.i("main", "2");
while (mIsRuning) {
long start = System.currentTimeMillis();
draw();
long end = System.currentTimeMillis();
if (end - start < 50) {
try {
Thread.sleep(50 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在run方法我们要注意的是,这里的draw方法绘制所需的事件不足50毫秒的,会让子线程睡眠,也就是说我们每一次进行绘制的操作,至少会花费50毫秒的时间。同时要注意的是:如果这里不睡眠的,程序容易闪退或者卡顿,我经过测试的。
draw方面里面才是我们真正绘制的操作。
private void draw() {
mCanvas = mHolder.lockCanvas();
try {
if (mCanvas != null) {
//绘制背景图片
drawBg();
//mStartAngle = 0;
int tempAngle = 360 / mItemCount;
for(int i = 0; i < mItemCount; i++)
{
//绘制圆块
mArcPaint.setColor(mColors[i]);
mCanvas.drawArc(mRange, mStartAngle, tempAngle, true, mArcPaint);
//绘制文字
drawText((int) mStartAngle, tempAngle, strs[i]);
//绘制图片
drawIcon((int) mStartAngle, tempAngle, mImgsBitmap[i]);
mStartAngle += tempAngle;
}
mStartAngle += mSpeed;
if(mIsShouldEnd)
{
mSpeed--;
}
if(mSpeed <= 0)
{
mSpeed = 0;
mIsShouldEnd = false;
}
}
} catch (Exception e) {
} finally {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
绘制背景图片
private void drawBg() {
mCanvas.drawColor(0xffffffff);
mCanvas.drawBitmap(mBgBitmap, null, new Rect(mPadding / 2, mPadding / 2, getMeasuredWidth() - mPadding / 2, getMeasuredWidth() - mPadding / 2), null);
}
5.绘制文字
private void drawText(int mStartAngle, int tempAngle, String str) {
Path path = new Path();
path.addArc(mRange, mStartAngle, tempAngle);
/**
* hOffset -- 水平偏移量,沿着圆弧的偏移量
* vOffset -- 垂直偏移量, 向圆心的偏移量
*/
int textWidth = (int) mTextPaint.measureText(str);
int hOffset = (int) (mRadius * Math.PI / mItemCount / 2 - textWidth / 2);
int vOffset = mRadius / 2 / 6;
mCanvas.drawTextOnPath(str, path, hOffset,vOffset,mTextPaint);
}
6.绘制图片
private void drawIcon(int mStartAngle, int tempAngle, Bitmap bitmap) {
int imgWidth = mRadius / 2 / 4;
float angle = (float) ((mStartAngle + tempAngle / 2) * (Math.PI / 180));
int x = (int) (mCenter + mRadius / 2/ 2 * Math.cos(angle));
int y = (int) (mCenter + mRadius / 2 / 2 * Math.sin(angle));
Rect rect = new Rect(x - imgWidth / 2, y - imgWidth / 2, x + imgWidth / 2, y + imgWidth / 2);
mCanvas.drawBitmap(bitmap, null, rect, null);
}
在这里我们可以直接为mSpeed设置值,然后它就可以转动起来。同时这里还有一个问题,那就是怎么控制每次转盘转到指定的位置上呢?这里就要用到很多的数学知识了。
public void startPan(int index)
{
float angle = 360 * 1.0f / mItemCount;
float from = 270 - (index + 1) * angle;
float end = from + angle;
float targetFrom = 10 * 360 + from;
float targetEnd = 10 * 360 + end;
/**
* v1--
* (v1 + 0) * (v1 + 1) / 2 = targetFrom
*v1 * v1 +v1 - 2 * targetFrom = 0;
*
*
*/
float v1 = (float)((-1 + Math.sqrt(1 + 8 * targetFrom)) / 2);
float v2 = (float) ((-1 + Math.sqrt(1 + 8 * targetEnd) ) /2);
mSpeed = (float) (v1 + Math.random() * (v2 - v1));
//mSpeed = v1;
mIsShouldEnd = false;
}