掌握SurfaceView,实现抽奖大转盘

博客转移到个人站点:http://www.wangchengmeng.club/2018/02/01/%E6%8E%8C%E6%8F%A1SurfaceView%EF%BC%8C%E5%AE%9E%E7%8E%B0%E6%8A%BD%E5%A5%96%E5%A4%A7%E8%BD%AC%E7%9B%98/

欢迎吐槽

使用surfaceView自定义抽奖大转盘
话不多说,先上效果图

这里写图片描述

完整代码地址欢迎start

实现思路以及过程

1、首先了解SurfaceView的基本用法,它跟一般的View不太一样,采用的双缓存机制,可以在子线程中绘制View,不会因为绘制耗时而失去流畅性,这也是选择使用SurfaceView去自定义这个抽奖大转盘的原因,毕竟绘制这个转盘的盘块,奖项的图片和文字以及转动都是靠绘制出来的,是一个比较耗时的绘制过程。

2、使用SurfaceView的一般模板样式

一般会用到的成员变量

private SurfaceHolder mSurfaceHolder;
private Canvas        mCanvas;

初始化常亮

public SurfaceViewTemplate(Context context,AttributeSet attrs) {
    super(context, attrs);
    //初始化
    mSurfaceHolder = getHolder();
    mSurfaceHolder.addCallback(this);
    //设置可获得焦点
    setFocusable(true);
    setFocusableInTouchMode(true);
    //这是常亮
    setKeepScreenOn(true);
}

给SurfaceView添加callback实现其中三个方法

 @Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
    //surface创建的时候
    mThread = new Thread(this);
    //创建的时候就开启线程
    isRunning = true;
    mThread.start();
}

@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
    //变化的时候
}

@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    //销毁的时候  关闭线程
    isRunning = false;
}

在子线程中定义一个死循环不断的进行绘制

 @Override
public void run() {
    //在子线程中不断的绘制
    while (isRunning) {
        draw();
    }
}

private void draw() {
    try {
        mCanvas = mSurfaceHolder.lockCanvas();
        if (null != mCanvas) {
   //避免执行到这里的时候程序已经退出 surfaceView已经销毁那么获取到canvas为null
        }
    } catch (Exception e) {
        //异常可以不必处理
    } finally {
        //一定要释放canvas避免泄露
        mSurfaceHolder.unlockCanvasAndPost(mCanvas);
    }
}

3、了解了SurfaceView的基本用法之后,接下来实现抽奖转盘

首先测量整个View的范围,设置成正方形

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //直接控制Span为正方形
        int width = Math.min(getMeasuredWidth(), getMeasuredHeight());
        mPadding = getPaddingLeft();
        //直径
        mRadius = width - mPadding * 2;
        //设置中心点
        mCenter = width / 2;
        //设置成正方形
        setMeasuredDimension(width, width);
    }

在SurfaceView创建的时候初始化画笔矩形范围等,见代码

public void surfaceCreated(SurfaceHolder surfaceHolder) {
        //初始化绘制Span的画笔
        mSpanPaint = new Paint();
        mSpanPaint.setAntiAlias(true);
        mSpanPaint.setDither(true);
        //初始化绘制文本的画笔
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setColor(0Xffa58453);
        //绘制圆环的画笔
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(0xffdfc89c);
        //初始化Span的范围
        mRectRange = new RectF(mPadding, mPadding, mPadding + mRadius, mPadding + mRadius);
        mRectCircleRange = new RectF(mPadding * 3 / 2, mPadding * 3 / 2, getMeasuredWidth() - mPadding * 3 / 2, getMeasuredWidth() - mPadding * 3 / 2);
        //初始化bitmap
        mImgIconBitmap = new Bitmap[mSpanCount];
        //将奖项的icon存储为Bitmap
        for (int i = 0; i < mSpanCount; i++) {
            mImgIconBitmap[i] = BitmapFactory.decodeResource(getResources(), mPrizeIcon[i]);
        }

        //surface创建的时候
        mThread = new Thread(this);
        //创建的时候就开启线程
        isRunning = true;
        mThread.start();
    }

接下来就是在开启的子线程中进行绘制

 @Override
    public void run() {
        //在子线程中不断的绘制
        while (isRunning) {
            //保证绘制不低于50毫秒  优化性能
            long start = SystemClock.currentThreadTimeMillis();
            draw();
            long end = SystemClock.currentThreadTimeMillis();
            if ((end - start) < 50) {
                //休眠到50毫秒
                SystemClock.sleep(50 - (end - start));
            }
        }
    }

重点就在draw()方法中了下面就实现draw方法:

注意:避免mCanvas带来的内存泄漏

 try {
        mCanvas = mSurfaceHolder.lockCanvas();
        if (null != mCanvas) {
            //避免执行到这里的时候程序已经退出 surfaceView已经销毁那么获取到canvas为null
            //绘制背景
            drawBg();
            //绘制圆环
            mCanvas.drawCircle(mCenter, mCenter, mRadius / 2 + mPadding / 20, mCirclePaint);
            drawSpan();
        }
    } catch (Exception e) {
        //异常可以不必处理
    } finally {
        //一定要释放canvas避免泄露
        mSurfaceHolder.unlockCanvasAndPost(mCanvas);
    }

画背景:

//绘制背景
    private void drawBg() {
        //背景设置为白色
        mCanvas.drawColor(0xffffffff);
        mCanvas.drawBitmap(mSpanBackground, null, new RectF(mPadding / 2, mPadding / 2, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2), mSpanPaint);
    }

参数解释:

    mSpanBackground背景图片
    new RectF(mPadding / 2, mPadding / 2, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2)
    //限制背景在一个矩形范围之类 

绘制内圆环

mCanvas.drawCircle(mCenter, mCenter, mRadius / 2 + mPadding / 20, mCirclePaint); 

绘制中间八个盘块

//定义一个变量临时记录开始转动的角度
float tempAngle = mStartSpanAngle;
//每个盘块所占的角度 CIRCLE_ANGLE = 360
float sweepAngle = CIRCLE_ANGLE / mSpanCount;
//循环绘制八个板块
   for (int i = 0; i < mSpanCount; i++) {
       //设置每个盘块画笔的颜色
        mSpanPaint.setColor(mSpanColor[i]);
        //绘制扇形盘块,第四个参数为true就是扇形否则就是弧形
        mCanvas.drawArc(mRectCircleRange, tempAngle, sweepAngle, true, mSpanPaint);
        //绘制文字
        drawText(tempAngle, sweepAngle, mPrizeName[i]);
        //绘制奖项Icon
        drawPrizeIcon(tempAngle, mImgIconBitmap[i]);
            //改变角度
            tempAngle += sweepAngle;
        }

绘制文字

文字绘制成圆环形状,根据path绘制文字
private void drawText(float tempAngle, float sweepAngle, String text) {
    //绘制有弧度的文字 根据path绘制文字的路径
    Path path = new Path();
    path.addArc(mRectRange, tempAngle, sweepAngle);
    //让文字水平居中 那绘制文字的起点位子就是  弧度的一半 - 文字的一半
    float textWidth = mTextPaint.measureText(text);
    float hOval = (float) ((mRadius * Math.PI / mSpanCount / 2) - (textWidth / 2));

    float vOval = mRadius / 15;//竖直偏移量可以自定义

    mCanvas.drawTextOnPath(text, path, hOval, vOval, mTextPaint); //第三个四个参数是竖直和水平偏移量
}

绘制盘块中的奖品icon图片

private void drawPrizeIcon(float tempAngle, Bitmap bitmap) {
    //图片的大小设置成直径的1/8
    int iconWidth = mRadius / 20;
    //根据角度计算icon中心点
    //角度计算 1度 == Math.PI / 180
    double angle = (tempAngle + CIRCLE_ANGLE / mSpanCount / 2) * Math.PI / 180;
    //根据三角函数,计算中心点(x,y)
    int x = (int) (mCenter + mRadius / 4 * Math.cos(angle));
    int y = (int) (mCenter + mRadius / 4 * Math.sin(angle));
    //定义一个矩形 限制icon位置
    RectF rectF = new RectF(x - iconWidth, y - iconWidth, x + iconWidth, y + iconWidth);
    mCanvas.drawBitmap(bitmap, null, rectF, null);
}

大致的绘制基本完成,重点就是通过改变开始转动的角度让转盘转动起来。

 mStartSpanAngle += mSpeed;//mSpeed的数值控制转动的速度
     //声明的一个结束标志
    if (isSpanEnd) {
        mSpeed -= 1;
    }
    if (mSpeed <= 0) {
        //停止旋转了
        mSpeed = 0;
        isSpanEnd = false;
        //定义一个回调,监控转盘停止转动
      mSpanRollListener.onSpanRollListener(mSpeed);
    }

定义一个方法, 启动转盘’

//抽奖转盘重点就在这里,根据自己传入的index控制抽到的奖品
public void luckyStart(int index) {
    //根据index控制停留的位置  angle 是每个奖品所占的角度范围
    float angle = CIRCLE_ANGLE / mSpanCount;
    //计算指针停留在某个index下的角度范围HALF_CIRCLE_ANGLE=180度
    float from = HALF_CIRCLE_ANGLE - (index - 1) * angle;
    float end = from + angle;

    //设置需要停下来的时候转动的距离  保证每次不停留的某个index下的同一个位置
    float targetFrom = 4 * CIRCLE_ANGLE + from;
    float targetEnd = 4 * CIRCLE_ANGLE + end;//最终停下来的位置在from-end之间,4 * CIRCLE_ANGLE 自定义要多转几圈

    //计算要停留下来的时候速度的范围,这里注意:涉及到等差数列的公式,因为涉及到让转盘停止转动是使mSpeed-=1;所以它是从 vFrom--0等差递减的一个过程,所以可以算出来vFrom,同理计算出vEnd
    float vFrom = (float) ((Math.sqrt(1 + 8 * targetFrom) - 1) / 2);
    float vEnd = (float) ((Math.sqrt(1 + 8 * targetEnd) - 1) / 2);
    //在点击开始转动的时候 传递进来的index值就已经决定停留在那一项上面了
    mSpeed = vFrom + Math.random() * (vEnd - vFrom);
    isSpanEnd = false;
}

停止转动

 public void luckStop() {
    //在停止转盘的时候强制吧开始角度赋值为0  因为控制停留指定位置的角度计算是根据开始角度为0计算的
    mStartSpanAngle = 0;
    isSpanEnd = true;
}

具体实现牵涉到一些数学知识,可能讲述不太清楚,不过上代码就比较好了,直接看代码会更加清晰,代码中注释很详细,防止以后自己再回头看的时候忘记。欢迎访问github地址,查看完整代码,可以根据自己的需求去修改,顺便学习一下自定义view了解一下SurfaceView的用法。

地址:完整代码地址欢迎start,如有发现问题请多多指点互相学习交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值