看到一个抽奖的效果,最近正要写个自定义的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
再次附上链接