看到一个抽奖的效果,最近正要写个自定义的View就用这个练一下好了
不多说先上图,因为我这主要是实现了思路,所以UI做的不是很好看,后续我会补上,看是否能满足你的需求:
思路解析
1.首先需要看仔细看一下,抽奖是什么流程,拆分业务流程。
2.分析好业务流程后,开始做代码分析,如何实现分成几个步骤。
3.具体的实现步骤,要尽可能完整这样你写的时候就会很流畅。
具体实现
自定义view流程大约是几步:
-
需要绘制的静态布局都有那些要明确出来,
-
抽奖这个首先要有一个背景;
-
然后是一堆小的中奖矩形区域(区域上是文字或奖品图片等);
-
然后是有一个浮层类似的矩形模块(需要滚动在各中奖矩形上);
-
然后是一个启动抽奖的按钮(其实这个按钮应该是唯一的操作了);
-
-
上面这些东西都绘制完成后,就需要是让这个抽奖机,滚动起来了,然后产生一个中奖产品。我猜想中奖产品应该是一个固定的,就是在你还没开始抽之前,就已经确定了一个范围,因为一个抽奖活动各个奖项都是固定的。抽走一个就会少一个,相应的奖品的中奖几率就会越小。这个地方我还没有实现,目前只是随机出来一个奖品。
有了如上的分析步骤,我们写起来就不会那么复杂了,因为你已经确定要做的事情了,按步骤写就好了
由于我们的view在抽奖的时候会一直进行绘制,所以这里我选择使用SurfaceView来实现,如直播中的点赞一般也是用SurfaceView来实现。
下面开始正式进入编码
-
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); } } }
-
这个draw方法是正式开始绘制的地方,主要有以下几部分,在绘制之前先计算出各个矩形的位置。
/** * 绘制开始 */ private void draw() { //计算出抽奖块的位置 calculate(); //绘制抽奖的背景 drawBackground(mCanvas); //绘制开始按钮 drawLotteryButton(mCanvas); //绘制遮罩 drawShade(mCanvas); }
-
计算位置的代码是我自己摸索的写的,感觉应该不是很好(尴尬),主要的思路就是因为我计划绘制的是一个四个边的正方形,所以我把奖品数目分成了四份。然后就是按照顺时针的顺序挨个计算每个矩形的位置了。
因为要绘制正方形,所以如果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()); }
-
计算完成后会得到一个小的矩形列表,里面存储的是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); } }
-
现在基本上整体绘制了主要部分,现在把中心的开奖按钮绘制一下。这个地方也是需要处理文字对齐。后期会继续完善。
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); } }
-
然后在把中奖矩形上绘制一个阴影就基本完成了所有的绘制。
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
再次附上链接