自定义SurfaceView实现抽奖转盘实战篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shayubuhuifei/article/details/84107114

看到一个抽奖的效果,最近正要写个自定义的View就用这个练一下好了

不多说先上图,因为我这主要是实现了思路,所以UI做的不是很好看,后续我会补上,看是否能满足你的需求:

在这里插入图片描述

项目git地址

思路解析

1.首先需要看仔细看一下,抽奖是什么流程,拆分业务流程。

2.分析好业务流程后,开始做代码分析,如何实现分成几个步骤。

3.具体的实现步骤,要尽可能完整这样你写的时候就会很流畅。

具体实现

自定义view流程大约是几步:

  • 需要绘制的静态布局都有那些要明确出来,

    1. 抽奖这个首先要有一个背景;

    2. 然后是一堆小的中奖矩形区域(区域上是文字或奖品图片等);

    3. 然后是有一个浮层类似的矩形模块(需要滚动在各中奖矩形上);

    4. 然后是一个启动抽奖的按钮(其实这个按钮应该是唯一的操作了);

  • 上面这些东西都绘制完成后,就需要是让这个抽奖机,滚动起来了,然后产生一个中奖产品。我猜想中奖产品应该是一个固定的,就是在你还没开始抽之前,就已经确定了一个范围,因为一个抽奖活动各个奖项都是固定的。抽走一个就会少一个,相应的奖品的中奖几率就会越小。这个地方我还没有实现,目前只是随机出来一个奖品。

有了如上的分析步骤,我们写起来就不会那么复杂了,因为你已经确定要做的事情了,按步骤写就好了

由于我们的view在抽奖的时候会一直进行绘制,所以这里我选择使用SurfaceView来实现,如直播中的点赞一般也是用SurfaceView来实现。

下面开始正式进入编码

  1. SurfaceView常规使用,由于支持在子线程中绘制,所以初始代码如下:

     @Override
        public void surfaceCreated(SurfaceHolder holder) {
            LogUtil.d("surfaceCreated--调用surfaceCreated");
            isDrawing = true;
            drawThread = new Thread(this);
            drawThread.start();
        }
    
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            LogUtil.d("surfaceChanged--调用surfaceChanged");
        }
    
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            LogUtil.d("surfaceDestroyed--调用surfaceDestroyed");
            currentCount = 0;
            if (mRunningAnimator != null) {
                mRunningAnimator.cancel();
                mRunningAnimator.removeAllListeners();
            }
            isDrawing = false;
            mRectList.clear();
            drawThread = null;
    
        }
    
    
      @Override
        public void run() {
            while (isDrawing) {
                try {
                    //降低绘制的频率
                    Thread.sleep(10);
                    mCanvas = mHolder.lockCanvas();
                    draw();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    LogUtil.d("run_finally--unlockCanvasAndPost:");
                    mHolder.unlockCanvasAndPost(mCanvas);
                }
            }
    
        }
    
  2. 这个draw方法是正式开始绘制的地方,主要有以下几部分,在绘制之前先计算出各个矩形的位置。

     /**
         * 绘制开始
         */
        private void draw() {
            //计算出抽奖块的位置
            calculate();
            //绘制抽奖的背景
            drawBackground(mCanvas);
            //绘制开始按钮
            drawLotteryButton(mCanvas);
            //绘制遮罩
            drawShade(mCanvas);
        }
    
  3. 计算位置的代码是我自己摸索的写的,感觉应该不是很好(尴尬),主要的思路就是因为我计划绘制的是一个四个边的正方形,所以我把奖品数目分成了四份。然后就是按照顺时针的顺序挨个计算每个矩形的位置了。

    因为要绘制正方形,所以如果SurfaceView不是正方形的话,就要不能填充完全了,按照较小的边进行计算。

    /**
         * 计算需要多少个奖品块,奖品平均分配到4个边上
         */
        private void calculate() {
      
            if (mCanvas.getWidth() < mCanvas.getHeight()) {
                everyWidth = mCanvas.getWidth() / (rowCount + 1);
            } else {
                everyWidth = mCanvas.getHeight() / (rowCount + 1);
            }
            realityWidth = everyWidth * (rowCount + 1);
    
            int left = -everyWidth;
            int top = 0;
            int right = 0;
            int bottom = everyWidth;
            for (int i = 0; i < rowCount; i++) {
                left += everyWidth;
                right += everyWidth;
                Rect rect = new Rect(left, top, right, bottom);
                mRectList.add(rect);
            }
    //        LogUtil.d("calculate1--mRectList长度:" + mRectList.size());
    
            left = rowCount * everyWidth;
            top = -everyWidth;
            right = (rowCount + 1) * everyWidth;
            bottom = 0;
            for (int i = 0; i < rowCount; i++) {
                top += everyWidth;
                bottom += everyWidth;
                Rect rect = new Rect(left, top, right, bottom);
                mRectList.add(rect);
    //            LogUtil.d("calculate2--top:" + rect.top + "bottom:" + rect.bottom);
    
            }
    //        LogUtil.d("calculate2--mRectList长度:" + mRectList.size());
    
            left = (rowCount + 1) * everyWidth;
            top = rowCount * everyWidth;
            right = (rowCount + 2) * everyWidth;
            bottom = (rowCount + 1) * everyWidth;
            for (int i = 0; i < rowCount; i++) {
                left -= everyWidth;
                right -= everyWidth;
                Rect rect = new Rect(left, top, right, bottom);
                mRectList.add(rect);
    //            LogUtil.d("calculate3--left:" + rect.left + "right:" + rect.right);
    
            }
    //        LogUtil.d("calculate3--mRectList长度:" + mRectList.size());
    
            left = 0;
            top = (rowCount + 1) * everyWidth;
            right = everyWidth;
            bottom = (rowCount + 2) * everyWidth;
            for (int i = 0; i < rowCount; i++) {
                top -= everyWidth;
                bottom -= everyWidth;
                Rect rect = new Rect(left, top, right, bottom);
                mRectList.add(rect);
    //            LogUtil.d("calculate4--top:" + rect.top + "bottom:" + rect.bottom);
    
            }
    //        LogUtil.d("calculate4--mRectList长度:" + mRectList.size());
    
        }
    
  4. 计算完成后会得到一个小的矩形列表,里面存储的是Rect用来记录每个矩形的位置。下面开始绘制矩形,先绘制整个背景矩形,再绘制小的矩形,然后在把文字绘制到小矩形上,这里计算文字的位置比较麻烦,很不容易对齐。

     canvas.drawRect(new Rect(0, 0, mCanvas.getWidth(), canvas.getHeight()), mPaint);
            for (int i = 0; i < mRectList.size(); i++) {
    //            LogUtil.d("开始绘制第:" + i);
                Rect rectF1 = mRectList.get(i);
                canvas.drawRect(rectF1, mPaint);
                canvas.drawRect(rectF1, mBorderPaint);
                //计算文字的位置
                if (i < awardCount) {
                    Point point = calculateTextLocation(rectF1, awardList.get(i));
                    mCanvas.drawText(awardList.get(i), point.x, point.y, mTextPaint);
                } else {
                    Point point = calculateTextLocation(rectF1, awardList.get(i - awardCount));
                    mCanvas.drawText(awardList.get(i - awardCount), point.x, point.y, mTextPaint);
    
                }
            }
    
  5. 现在基本上整体绘制了主要部分,现在把中心的开奖按钮绘制一下。这个地方也是需要处理文字对齐。后期会继续完善。

     private void drawLotteryButton(Canvas canvas) {
            mButtonRegion = new Region(realityWidth / 2 - radius / 2, realityWidth / 2 - radius / 2, realityWidth / 2 + radius / 2, realityWidth / 2 + radius / 2);
            canvas.drawCircle(realityWidth / 2, realityWidth / 2, radius, mButtonPaint);
            if (lotteryState == IS_LOTTERYING) {
                Point point = calculateTextLocation(mButtonRegion.getBounds(), "STOP");
                canvas.drawText("STOP", point.x, point.y, mTextPaint);
            } else {
                Point point = calculateTextLocation(mButtonRegion.getBounds(), "GO");
                canvas.drawText("GO", point.x, point.y, mTextPaint);
    
            }
        }
    
  6. 然后在把中奖矩形上绘制一个阴影就基本完成了所有的绘制。

     private void drawShade(Canvas mCanvas) {
            LogUtil.d("开始绘制阴影图" + currentCount);
            if (mRectList.size() > currentCount) {
                mCanvas.drawRect(mRectList.get(currentCount), mShadePaint);
            }
            if (mRectList.size() == rowCount * 4) {
                isDrawing = false;
            }
        }
    

以上的步骤完成后,基本上一个不会动的抽奖自定义控件已经出来了。

下面思考如何让这个动起来?

思路:

我想小的中奖矩形的位置都有了,就按照已有的位置,在上面在绘制一层不就好了么?

有了想法了,就可以开始去实践一下,看是否可行。

尝试一

通过一个不停增加变化的数字,来绘制阴影,因为我想只绘制阴影部分不影响已经绘制好的其他部分,尝试后发现SurfaceView会一直闪烁。

尝试二

如果只绘制阴影不行的话,我就只能把整个画布都绘制一次,然后每次绘制阴影的位置不同,这种方式倒是实现了大概的抽奖效果,但是感觉比较消耗内存,因为你要绘制一整张画布。(目前我还没找到其他的方法)

阴影也能动起来了,就差一个点击事件了,这个是通过实现touch事件来处理,因为我们知道按钮的坐标范围,我们只要判断点击的位置在这个坐标范围内就响应事件即可。

具体实现如下,这里面有一个逻辑是通过状态来控制按钮是开始摇奖,还是结束摇奖。:

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mButtonRegion.contains(x, y)) {
                    LogUtil.d("onTouchEvent-X:" + x + "Y:" + y);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mButtonRegion.contains(x, y)) {
                    LogUtil.d("onTouchEvent-X:" + x + "Y:" + y);
                    if (isEnable) {
                        if (lotteryState == IS_DEFAULT) {
                            startLottery();
                        } else if(lotteryState == IS_LOTTERYING) {
                            stopLottery();
                        }
                    }
                }
                break;
            default:
                break;
        }
        return true;

    }

开奖的动画我也贴出来吧,属性动画的知识,通过改变currentCount来确定阴影的绘制位置。

/**
     * 让阴影滚动起来
     *
     * @param
     */
    private void startLottery() {
        lotteryState = IS_LOTTERYING;
        drawLotteryButton(mCanvas);
        if (currentCount > mRectList.size()) {
            return;
        }
        if (mRunningAnimator != null) {
            currentCount = 0;
            mRunningAnimator.cancel();
        }
//        int timeResult = testRandom3() * 1000;
        //由于属性动画中,当达到最终值会立刻跳到下一次循环,所以需要补1
        mRunningAnimator = ObjectAnimator.ofInt(this, "currentCount", 0, 1);
        mRunningAnimator.setRepeatMode(ValueAnimator.RESTART);
        mRunningAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mRunningAnimator.setDuration(3000);
        mRunningAnimator.setInterpolator(new LinearInterpolator());
        mRunningAnimator.start();

    }

上面基本完成了这个还不太完整的抽奖自定义View了,但是还有许多小的细节没有实现完全。

todo的内容

1.开奖动画加入;

2.指定开奖的奖品,不能说使用随机开奖。

今天添加了开奖动画和指定到某一个奖品

思路分析:

我们要实现上面的需求,首先要处理两个问题:

1.我们点击stop的时候,currentCount需要回到初始位置,因为我门计划播放开奖动画是从0开始变化,如果不把currentCount重置为初始位置,会出现跳跃。

2.指定奖品结果,需要我们播放最后一圈动画的时候加上这个结果数值,让选中的奖品刚好走到指定位置。

解决方案:

轮盘现在还旋转,先取消第一个播放动画。然后我们播放一个临时动画,把移动到初始值(选中模块移动到初始位置)。然后在正式播放我们的开奖动画。一个逐渐变慢的动画,最后停在指定位置。

代码比较简单,这里我就不再贴出,有需要的可以去看一下git。

This ALL
再次附上链接

猜你喜欢

转载自blog.csdn.net/shayubuhuifei/article/details/84107114
今日推荐