我们知道view的绘制一般是在主线程进行的,这就导致了如果view的绘制很复杂,主线程就会一直被view的绘制占用而导致卡死。因此surfaceView的出现很好的解决了这个问题。Surfaceview允许在子线程中进行view的绘制,因此可以使用它进行复杂的绘制,主线程还可以去处理其他的事情。这也是surfaceview最棒的优点。
SurfaceView中有一个显示的区域对象Surface,SurfaceView负责控制Surface显示的位置,Surface显示内容,那总得有一个对象来控制显示的内容的吧?没错,我们可以通过SurfaceView类中的getHolder()方法得到该SurfaceView中Surface的控制。我们要通过SurfaceViewHolder类中的addCallback()方法添加Surface的回调,这个SurfaceViewHolder.Callback回调接口里面有三个方法:
/**
* 当view创建的时候进行回调
* 一般在该方法中开启绘制view的子线程
* 该方法在ui线程执行
* @param holder 该SurfaceView的SurfaceViewHolder对象
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
/**
* 该方法在ui线程执行
* 当SurfaceView的尺寸发生变化的时候进行回调(比如竖屏切换到横屏的时候)
* @param holder SurfaceView的SurfaceViewHolder对象
* @param format 格式
* @param width 宽度
* @param height 高度
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
/**
* 该方法中ui线程执行
* 当SurfaceView被销毁的时候执行
* @param holder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
下面大家首先看一下这个SurfaceViewTemplate模板
package test.com.lukypan;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
*surfaceView的模板
* Created by wangpeiyu on 2016/11/21.
*/
public abstract class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
protected SurfaceHolder mSurfaceHolder;
//surfaceView的画布,进行view的绘制
protected Canvas mCanvas;
//绘制的子线程
protected Thread mThread;
//子线程运行的Runnable
protected Runnable mRunnable;
//决定线程运行和结束
boolean isStart = false;
public SurfaceViewTemplate(Context context) {
this(context, null);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
super(context, attrs);
initNecessaryVariables();
initVariables();//调用子类的初始化变量方法
}
//初始化必要的变量
private void initNecessaryVariables() {
mRunnable = this;
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
setKeepScreenOn(true);
mThread = new Thread(mRunnable);
}
/**
* 当view创建的时候进行回调
* 一般在该方法中开启绘制view的子线程
* 该方法在ui线程执行
* @param holder 该SurfaceView的SurfaceViewHolder对象
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
isStart = true;
mThread.start();
create();
}
/**
* 该方法在ui线程执行
* 当SurfaceView的尺寸发生变化的时候进行回调(比如竖屏切换到横屏的时候)
* @param holder SurfaceView的SurfaceViewHolder对象
* @param format 格式
* @param width 宽度
* @param height 高度
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
change();
}
/**
* 该方法中ui线程执行
* 当SurfaceView被销毁的时候执行
* @param holder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
isStart = false;
destroy();
}
/**
* 进行view的绘制
*/
@Override
public void run() {
while (isStart) {
try {
draw();
} catch (Exception e) {
} finally {
if (mCanvas != null) {
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
protected abstract void draw();
protected abstract void create();
protected abstract void change();
protected abstract void destroy();
protected abstract void initVariables();
}
通过这个类我们可以看到有5个成员变量。mSurfaceHolder指向该SurfaceView中Surface的控制器,mCanvas为当前SurfaceView的画布。既然我们知道SurfaceView的绘制是在子线程中执行的,所以我们肯定要有一个线程对象mThread,mRunnable对象在这里没有显示出太大的作用,但是考虑到可以动态的指定其它的Runnable对象,所以还是保留吧。最后一个mStart对象为决定线程的开始开始结束。我们将这个模板类定义为抽象的,将各种实SurfaceView的子类一定有的部分都写在了模板类中,并将一些不同的实现定义成了抽象的方法,这样基于这个模板类实现的子类都必须实现这些方法。这些方法有
protected abstract void draw();
protected abstract void create();
protected abstract void change();
protected abstract void destroy();
protected abstract void initVariables();
下面是基于这个SurfaceViewTemplate模板类而开发的一个抽奖转盘。这个抽奖转盘的实现参考了张鸿祥老师的博客和慕课视频,但是代码的实现还是不一样的,大家可以参考一下。
package test.com.lukypan;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
/**
* Created by wangpeiyu on 2016/11/21.
*/
public class MyLuckyPan extends SurfaceViewTemplate {
private int mCount=6;
private int mBitmapIds[];
private Bitmap mBtp[];
private String mStr[];
private int mColor[];
private Bitmap mBtpBg;
private RectF mRectf;
private float mPadding;
private int mCenter;
private float mUnitOffset;
private float mStartAngle;
private Paint mPaintPan;
private Paint mPaintText;
private int mTxtSize;
private float mDiameter;
private float mSpeed;
private boolean isClickEnd;
public MyLuckyPan(Context context) {
this(context,null);
}
public MyLuckyPan(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 绘制,模板类的抽象类
*/
@Override
protected void draw() {
long startTime=System.currentTimeMillis();
myDraw();
long endTime=System.currentTimeMillis();
if(endTime-startTime<100)
{
try {
mThread.sleep(100-(endTime-startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(isClickEnd)
{
mSpeed-=0.5;
}
if(mSpeed<0)
{
mSpeed = 0;
isClickEnd=false;
}
mStartAngle+=mSpeed;
}
/**
* 当前SurfaceView 的绘制
*/
private void myDraw() {
mCanvas = mSurfaceHolder.lockCanvas();
mCanvas.drawColor(Color.WHITE);
mCanvas.drawBitmap(mBtpBg, null, mRectf, null);
drawArc();
drawBgPrize();
drawText();
}
/**
* 绘制抽奖转盘上的文本
*/
private void drawText() {
Path path= new Path();
float divider=mDiameter/16;
RectF panRectf=new RectF(mPadding+divider,mPadding+divider,
getMeasuredWidth()-mPadding-divider,getMeasuredHeight()-mPadding-divider);
for(int i=0;i<mCount;i++)
{
path.reset();
path.addArc(panRectf,mStartAngle+mUnitOffset*i,mUnitOffset);
Rect textRect=new Rect();
mPaintText.getTextBounds(mStr[i],0,mStr[i].length(),textRect);
float textHeight=textRect.height();
float textWidth = textRect.width();
float angle= (float) (mUnitOffset*Math.PI/180);
float radiu=(mDiameter-divider*2)/2;
float angleLendht=angle*radiu;
mCanvas.drawTextOnPath(mStr[i],path,angleLendht/2-textWidth/2,textHeight+10,mPaintText);
}
}
/**
* 绘制抽奖转盘上的图片
*/
private void drawBgPrize() {
double unit=Math.PI/180;
float radiu=mDiameter/4;
float lenght=mDiameter/12;
RectF rectF=new RectF();
float x = 0;
float y=0;
for(int i=0;i<mCount;i++){
x = (float) (mCenter+radiu*Math.cos((mStartAngle+i*mUnitOffset+mUnitOffset/2)*unit));
y= (float) (mCenter+radiu*Math.sin((mStartAngle+i*mUnitOffset+mUnitOffset/2)*unit));
rectF.set(x-lenght,y-lenght,x+lenght,y+lenght);
mCanvas.drawBitmap(mBtp[i],null,rectF,null);
}
}
/**
* 绘制不同颜色的扇形区域
*/
private void drawArc() {
float divider=mDiameter/16;
RectF panRectf=new RectF(mPadding+divider,mPadding+divider,
getMeasuredWidth()-mPadding-divider,getMeasuredHeight()-mPadding-divider);
for (int i = 0; i < mCount; i++) {
mPaintPan.setColor(mColor[i]);
mCanvas.drawArc(panRectf, mStartAngle + i * mUnitOffset,mUnitOffset,true, mPaintPan);
}
}
/**
* 判断是否在转动
* @return
*/
public boolean isRolling(){
return mSpeed!=0;
}
/**
* 判断是否已经点击了停止转动
* @return
*/
public boolean isClickEnd(){
return isClickEnd;
}
/**
* 开始转动
*/
public void start(){
mSpeed=30;
isStart=true;
}
/**
* 停止转动
*/
public void setClickEnd(){
isClickEnd = true;
//决定最后只能转移到什么位置
stopInEnd();
}
/**
* 这个方法是当用户点击停止转动后,判断转盘最后定在那个位置
* 如果停止的位置不是谢谢惠顾的位置,则应该对当前的位置进行一个
* 最小的微调,以致让这个转盘最后定在谢谢惠顾的扇形中
*/
private void stopInEnd() {
double result = mStartAngle+60*(60-1)*0.5/2;
double offset = result%360;//偏移了多少
if((offset>0&&offset<30)||(offset>150&&offset<210)||(offset>330&&offset<360))
{
/**
* 表明最后停止的位置是在谢谢惠顾范围内,不用执行微调操作。
*/
return;
}
/**
* 对于这里的微调,大家首先画图便可以很好的理解
*/
if(offset<90)
{
mStartAngle= (float) (mStartAngle-(30*Math.random()+offset-30));
}else if(offset<150){
mStartAngle= (float) (mStartAngle+60*Math.random()+150-offset);
}else if(offset<270)
{
mStartAngle= (float) (mStartAngle-(60*Math.random()+offset-210));
}else{
mStartAngle= (float) (mStartAngle+30*Math.random()+330-offset);
}
}
@Override
protected void create() {
}
@Override
protected void change() {
initVariables();
}
@Override
protected void destroy() {
}
/**
* 初始化变量
* 因为该方法是在模板类中调用的,所以所有的变量都应该在这个方法中初始化
* 不能在声明变量的时候就直接初始化
*/
@Override
protected void initVariables() {
mCount=6;
mBitmapIds = new int[]{R.drawable.ipad, R.drawable.thanks, R.drawable.iphone,
R.drawable.meinv, R.drawable.thanks, R.drawable.meizi};
mStr= new String[]{"ipad", "谢谢惠顾", "iphone", "妹子", "谢谢惠顾", "衣服"};
mBtpBg = BitmapFactory.decodeResource(getResources(), R.drawable.bg2);
mColor = new int[]{Color.parseColor("#ffff0000"), Color.parseColor("#ff00ff00"),
Color.parseColor("#ffff0000"), Color.parseColor("#ff00ff00"),
Color.parseColor("#ffff0000"), Color.parseColor("#ff00ff00")};
mStartAngle = 0;
mSpeed=0;
mBtp=new Bitmap[mCount];
for(int i=0;i<mCount;i++)
{
mBtp[i]=BitmapFactory.decodeResource(getResources(),mBitmapIds[i]);
}
mUnitOffset = (float) (360 / mCount);
mPaintPan = new Paint();
mPaintPan.setAntiAlias(true);
mPaintPan.setDither(true);
mPaintText = new Paint();
mPaintText.setColor(Color.BLACK);
mPaintText.setAntiAlias(true);
mPaintText.setDither(true);
mPaintText.setStrokeCap(Paint.Cap.ROUND);
mPaintText.setStrokeJoin(Paint.Join.ROUND);
mTxtSize= (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,20,getResources().getDisplayMetrics());
mPaintText.setTextSize(mTxtSize);
}
/**
* 测量当前SurfaceView 的绘制区域
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int desireLenght = Math.min(getMeasuredHeight(), getMeasuredWidth());
setMeasuredDimension(desireLenght, desireLenght);
mPadding = getPaddingLeft();
mCenter = desireLenght / 2;
mDiameter = desireLenght - mPadding * 2;
mRectf = new RectF(mPadding, mPadding, desireLenght - mPadding, desireLenght - mPadding);
}
}
这个是MainActivity的类:
package test.com.lukypan;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
MyLuckyPan luckyPan;
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
luckyPan = (MyLuckyPan) findViewById(R.id.id_luckpan);
imageView= (ImageView) findViewById(R.id.id_img);
imageView.setImageResource(R.drawable.start);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!luckyPan.isRolling())
{
luckyPan.start();
imageView.setImageResource(R.drawable.stop);
}else{
if(!luckyPan.isClickEnd())
{
luckyPan.setClickEnd();
imageView.setImageResource(R.drawable.start);
}else{
//已经点击了
Toast.makeText(MainActivity.this,"等待本次抽奖结束哦",Toast.LENGTH_SHORT).show();
}
}
}
});
}
}
大家复制这个三个类到自己的项目中,只需要修改图片的id便可以了,也就是修改在MyLuckyPan类中的initVariables方法中的mBitmapIds图片id的数组。这样就可以实现一个转盘了,大家试试吧。