慕客网视频传送门:http://www.imooc.com/learn/444
好久都没去慕客网了,虽然这次学习的是一个比较老的视频了,但是总比不学的好。(末尾附源码)
在学习之前,先来了解一波SurfaceView是什么,以及其作用:Android中的Surface和SurfaceView,以及SurfaceView 基础用法
之后,就可以开始视频的学习了。
视频的开始,对SurfaceView与一般的View进行了对比,这才前面的博客也有所提及:
SurfaceView继承自View
一般view是在UI线程中绘制自己,通过onDreaw方法
而SurfaceView则是在一个子线程中对自己进行绘制 优势:避免造成UI线程阻塞
在SurfaceView中包含一个专门用于绘制的Surface,Surface中包含一个Canvas
之后讲解了实现自定义SurfaceView的关键点,即获得Canvas用于绘制。
同时还需要注意在surfaceCreated 中开启一个子线程进行绘制,在surfaceDestoryed 在方法中暂停子线程中的绘制。
实现SurfaceView的一般步骤:
1、在构造方法中初始化holder,并进行相关设置,如:
setFocusable(true);
setFocusableInTouchMode(true);
setKeepScreenOn(true);
2、然后在surfaceCreated中去启动子线程,在surfaceDestroyed中暂停子线程中的绘制
3、在子线程中实现绘制操作,绘制时先通过holder拿到Canvas,绘制结束后需要释放Canvas
以下就是通用代码的实现:
public class SurfaceViewImpl extends android.view.SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mHolder;
private Canvas mCanvas;
private Thread mDrawThread;//用于绘制的线程
private boolean isRunning;//作为子线程运行的控制开关
public SurfaceViewImpl(Context context) {
this(context, null);
}
public SurfaceViewImpl(Context context, AttributeSet attrs) {
super(context, attrs);
mHolder = getHolder();
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
setKeepScreenOn(true);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
isRunning = true;
mDrawThread = new Thread(this);
mDrawThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isRunning = false;
}
@Override
public void run() {
//不断进行绘制
while (isRunning) {
draw();
}
}
private void draw() {
/**
* try-catch与判空的原因:
* 当SurfaceView在主界面时,如果点击home或者back键,都会使得Surface销毁,
* 但是在销毁之后,有可能已经进入该方法执行相应的逻辑了,因此需要对mCanvas进行判空,
* 另外,由于Surface被销毁,但是线程却不是那么容易被关闭,继续执行draw something的操作,
* 此时就有可能会抛出某些异常
*/
try {
//首先拿到Canvas用于绘制
mCanvas = mHolder.lockCanvas();
if (mCanvas != null) {
//TODO draw something
}
} catch (Exception e) {
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
了解通用代码的实现后,就进入正题了,实现抽奖转盘(旋转的原理:以一定的时间间隔绘制转盘,但是每次绘制时转盘都会偏转固定的角度,连续起来,就像转盘在滚动),主要的代码如下,具体代码解释看注解(需要注意的是,在视频中有一个成员变量为mRadius,但本意是值转盘的直径,所以我这里改成了mDia):
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mHolder;
private Canvas mCanvas;
private Thread mDrawThread;//用于绘制的线程
private boolean isRunning;//作为子线程运行的控制开关
//背景图
private Bitmap mBcgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg2);
//盘块的奖项
private final String[] mAwardsName = getResources().getStringArray(R.array.AwardsName);
//盘块的奖项图片id
private final int[] mAwardsImgs = new int[]{R.drawable.danfan, R.drawable.ipad, R.drawable.f040, R.drawable.iphone, R.drawable.meizi, R.drawable.f040};
//盘块的奖项图片
private Bitmap[] mImgsBitmap;
//盘块的数量
private final int mItemCount = 6;
//判断是否点击了停止按钮的标志
private boolean isShouldEnd;
//转盘的中心位置
private int mCenter;
//直接以padding值为准(或者取left、right、top、bottom中设置的最小的)
private int mPadding;
//整个盘块的范围
private RectF mRange = new RectF();
//整个盘快的直径
private int mDia;
//绘制盘块、文本的画笔
private Paint mArcPaint, mTextPaint;
//盘块滚动的速度(即转盘每隔mSpeed设置的角度重绘一次,但绘制的时间间隔不变)
private double mSpeed;
//起始角度(设置为float而非int,因为转盘存在某些逻辑会使得mStartAngle带有小数,如果为int会失去精度对指定奖项时的计算产生影响)
private volatile float mStartAngle = 0;//可能会存在于两个线程,同时更新
private final int platePartColor1 = 0xFFFFC300, platePartColor2 = 0xFFF17E01;
public MySurfaceView(Context context) {
this(context, null);
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
mHolder = getHolder();
mHolder.addCallback(this);
//可获得焦点
setFocusable(true);
setFocusableInTouchMode(true);
//设置常量
setKeepScreenOn(true);
}
//强制将转盘设置为正方形,并设置一些相关参数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = Math.min(getMeasuredWidth(), getMeasuredHeight());
mPadding = getPaddingLeft();
//半径
mDia = width - mPadding * 2;
//中心点
mCenter = width / 2;
setMeasuredDimension(width, width);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
init();
isRunning = true;
mDrawThread = new Thread(this);
mDrawThread.start();
}
private void init() {
//初始化绘制盘快的画笔
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setDither(true);
//初始化文本画笔
mTextPaint = new Paint();
mTextPaint.setColor(0xffffffff);
mTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20, getResources().getDisplayMetrics()));//设置文字大小
//初始化盘快绘制的范围(mRadius已经减去了mPadding)
mRange = new RectF(mPadding, mPadding, mDia + mPadding, mDia + mPadding);
//初始化图片
mImgsBitmap = new Bitmap[mItemCount];
for (int i = 0; i < mItemCount; i++)
mImgsBitmap[i] = BitmapFactory.decodeResource(getResources(), mAwardsImgs[i]);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isRunning = false;
}
@Override
public void run() {
//不断进行绘制
while (isRunning) {
long start = System.currentTimeMillis();
draw();
long end = System.currentTimeMillis();
if (end - start < 100) {
try {
Thread.sleep(100 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void draw() {
/**
* try-catch与判空的原因:
* 当SurfaceView在主界面时,如果点击home或者back键,都会使得Surface销毁,
* 但是在销毁之后,有可能已经进入该方法执行相应的逻辑了,因此需要对mCanvas进行判空,
* 另外,由于Surface被销毁,但是线程却不是那么容易被关闭,继续执行draw something的操作,
* 此时就有可能会抛出某些异常
*/
try {
//首先拿到Canvas用于绘制
mCanvas = mHolder.lockCanvas();
if (mCanvas != null) {
//绘制背景
mCanvas.drawColor(0xffffffff);
mCanvas.drawBitmap(mBcgBitmap, null, new Rect(mPadding / 2, mPadding / 2, getMeasuredWidth() - mPadding / 2, getMeasuredWidth() - mPadding / 2), null);
//绘制盘块
float tmpAngle = mStartAngle;
float sweepAngle = 360 / mItemCount;
for (int i = 0; i < mItemCount; i++) {
//1、绘制盘块
mArcPaint.setColor((i % 2 == 0 ? platePartColor1 : platePartColor2));
mCanvas.drawArc(mRange, tmpAngle, sweepAngle, true, mArcPaint);
//2、绘制盘块上的文本(弧形的)
Path path = new Path();
path.addArc(mRange, tmpAngle, sweepAngle);
//垂直偏移量取半径的1/6
int vOffset = mDia / 8;
//利用水平偏移量使文字水平居中
/**
* 圆的周长/盘块数量=每个盘块弧的长度
* 之后再/2,即取一半
* 最后减去文字的长度的一半(减文字的长度之前需要注意文字长度的值的一半小于等于上一步所求的值)
*/
int hOffset = (int) (mDia * Math.PI / mItemCount / 2 - mTextPaint.measureText(mAwardsName[i]) / 2);
mCanvas.drawTextOnPath(mAwardsName[i], path, hOffset, vOffset, mTextPaint);
//3、绘制盘块图标
//设置图片的宽度为半径的1/8
int imgWidth = mDia / 8;
//求得弧度值(即图片所示的α)
float angle = (float) ((tmpAngle + sweepAngle / 2) * Math.PI / 180);
//求得图标中心点的坐标(而非图标左上角的坐标)
int x = (int) (mCenter + mDia / 2 / 2 * Math.cos(angle));//mDia / 2 / 2->自定义去半径的一半
int y = (int) (mCenter + mDia / 2 / 2 * Math.sin(angle));
//确定图标的位置
Rect rect = new Rect(x - imgWidth / 2, y - imgWidth / 2, x + imgWidth / 2, y + imgWidth / 2);
mCanvas.drawBitmap(mImgsBitmap[i], null, rect, null);
tmpAngle += sweepAngle;
// if(tmpAngle==360) tmpAngle=0;
}
//mSpeed设置为10角度,即转盘每隔10角度重绘一次
mStartAngle += mSpeed;
//如果点击了停止按钮,使得转盘缓缓停止
if (isShouldEnd)
mSpeed -= 1;
if (mSpeed <= 0) {
mSpeed = 0;
isShouldEnd = false;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}
/**
* 启动转盘
*/
public void startDial() {
mSpeed = 10;
isShouldEnd = false;
}
/**
* 启动转盘,指定停止时的奖项
*/
public void startDial(int index) {
int angle=360/mItemCount;
//计算指定index的中奖角度范围
int from=270-(index+1)*angle;
int end=from+angle;
//设置停下来需要旋转的角度范围(每次都停在指定奖项所在的范围内,而不是每次都停在指定奖项的同一点)
int targetFrom=1*360+from;//1*360+from中的2表示点击停止后再转一圈再停止
int targetEnd=targetFrom+60;
//为了实现上述所说的停在奖项对应区间的任意点
//且停止时所对应的奖项是靠mSpeed的值决定的
//所以需要使得mSpeed处于[targetFrom,targetEnd]所对应的值的区间(即[v1,v2])
/**
* mSpeed->0时停止转动且要考虑点击停止按钮时因惯性每次-1
*
* 设mSpeed.v1对应targetFrom
* 则有 (v1+0)*(v1+1)/2=targetFrom=>v1=(-1+Math.sqrt(1+8*targetFrom))/2(除去了负值的)
*/
float v1 = (float) ((-1 + Math.sqrt(1 + 8 * targetFrom)) / 2);
float v2 = (float) ((-1 + Math.sqrt(1 + 8 * targetEnd)) / 2);
mSpeed = v1+Math.random()*(v2-v1);
isShouldEnd = false;
}
/**
* 停止转盘
*/
public void stopDial() {
isShouldEnd = true;
mStartAngle=0;
}
/**
* 判断是转盘是否正在转
*/
public boolean isRotating() {
return mSpeed != 0;
}
public boolean isShouldEndFlag() {
return isShouldEnd;
}
}
附:
图一:盘块中文字水平偏移量的图解
图二:盘块图标中心点坐标的图解(非图标的左上角点坐标)
然后是在主界面实现点击按钮的逻辑:
public void click(View view) {
if (mDial.isRotating()) {
view.setBackgroundResource(R.drawable.start);
mDial.stopDial();
} else {
//如果点击了停止按钮,且转盘由于惯性还在旋转时,则不起作用
if(!mDial.isShouldEndFlag()) {
view.setBackgroundResource(R.drawable.stop);
mDial.startDial(1);
}
}
番外:
对于奖项概率的设置:
包装一层,将奖项的概率与指定的数的范围区间对应起来,当落在某一区间则对应某一奖项,例如总的概率是1,总的数的范围区间为[0,1000],假设iPad的中奖概率为0.1,则iPad对应的区间为[start,start+100]的连续区间,其中start按实际情况自定义,然后再生产一个[0,1000]的随机数,如果落在了iPad对应的区间,则用控制停止时的中奖项的方法(前文中的startDial(int index)方法)指定停止在iPad奖项上
源码下载:http://download.csdn.net/download/qq_22804827/9772291
(使用的AS,module基本参数:
compileSdkVersion 25;buildToolsVersion “25.0.2”;minSdkVersion 21;targetSdkVersion 25)