SurfaceView的介绍和使用

我们知道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的数组。这样就可以实现一个转盘了,大家试试吧。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值