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

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

android surfaceView 旋转90度 微软surface屏幕旋转_i++

项目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);
            }
        }

    }
  1. 这个draw方法是正式开始绘制的地方,主要有以下几部分,在绘制之前先计算出各个矩形的位置。
/**
     * 绘制开始
     */
    private void draw() {
        //计算出抽奖块的位置
        calculate();
        //绘制抽奖的背景
        drawBackground(mCanvas);
        //绘制开始按钮
        drawLotteryButton(mCanvas);
        //绘制遮罩
        drawShade(mCanvas);
    }
  1. 计算位置的代码是我自己摸索的写的,感觉应该不是很好(尴尬),主要的思路就是因为我计划绘制的是一个四个边的正方形,所以我把奖品数目分成了四份。然后就是按照顺时针的顺序挨个计算每个矩形的位置了。
    因为要绘制正方形,所以如果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());

    }
  1. 计算完成后会得到一个小的矩形列表,里面存储的是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);

            }
        }
  1. 现在基本上整体绘制了主要部分,现在把中心的开奖按钮绘制一下。这个地方也是需要处理文字对齐。后期会继续完善。
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);

        }
    }
  1. 然后在把中奖矩形上绘制一个阴影就基本完成了所有的绘制。
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
再次附上链接