慕客網視頻傳送門: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)