前言
无论是线上推广,还是线下营销,转盘游戏都是非常吸引人的一项活动,就像电视剧《赘婿》中的“拼刀刀”,达到的是万人空巷的影响力。其实,这是一种赌徒心理。每个人都希望不劳而获,这是赌徒心理的成长的肥沃土壤,不幸的是,千百万年的成长中,人已经形成了避难趋易的本性。转盘是一种傻瓜式的操作,只需要手指点一下,不过几秒钟,就可以收到反馈。而想要立即得到结果,害怕未知依然是每个人的本质。所以,转盘游戏有其滋生的土壤。
思考
怎么样实现一个转盘那?首先:转盘是一个圆,然后把圆等分成几份,每一份都是一个扇形区域,在扇形区域中画出我们先展示的内容,接着,中间可能有装饰(图片),最后,按照层级一层层画。
想必大家都对自定义View的过程了熟于心,这里就不过多介绍了。下面我们进入代码。
代码
- 自定义转盘
这里没有给出全部的代码,而是抽取了一些关键步骤,文章的最后有给出全部代码。
class LuckySpinView : View {
constructor(context: Context?) : super(context) {
initPaint()
}
// 初始化画笔。自定义View前先想好需要哪些画笔,对实现View有神奇效果
private fun initPaint() {
mArcPaint = Paint()
mArcPaint.isAntiAlias = true
mArcPaint.isDither = true
mTextPaint = TextPaint()
mTextPaint.style = Paint.Style.FILL
mTextPaint.isAntiAlias = true
mTextPaint.isDither = true
mTextPaint.color = mTextColor
mTextPaint.textSize = mTextSize
mTextPaint.typeface = ResourcesCompat.getFont(context, R.font.impact)
mTextStrokePaint = TextPaint(mTextPaint)
mTextStrokePaint.style = Paint.Style.STROKE
mTextStrokePaint.strokeWidth = TEXT_STROKE_WIDTH
mBackgroundPaint = Paint()
mBackgroundPaint.isAntiAlias = true
mBackgroundPaint.isDither = true
}
// 测量阶段。自定义View必要的阶段,用来规范我们View的展示大小
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth.coerceAtMost(measuredHeight)
mPadding = paddingLeft
if (mOutRadius == 0) mOutRadius = width / 2
if (mInnerRadius == 0) mInnerRadius = width / 2
setMeasuredDimension(mOutRadius * 2, mOutRadius * 2)
}
// 绘画阶段。自定义View必要的阶段,画布承载我们想要的效果,画笔将我们的想法变成现实
/**
* @param canvas
*/
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawArc(canvas) // 画扇形区域
drawCenterImg(canvas) //画中间的区域
drawOutBoundImg(canvas) // 画外围的装饰
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun drawCenterImg(canvas: Canvas) {
if (mCenterImageId == 0) return
if (mCenterBitmap == null) {
val drawable = resources.getDrawable(mCenterImageId, null)
val bitmap = Utils.drawableToBitmap(drawable)
mCenterBitmap = Bitmap.createScaledBitmap(
bitmap,
drawable!!.intrinsicWidth,
drawable.intrinsicHeight,
false
)
}
val bitmap = mCenterBitmap!!
canvas.drawBitmap(
bitmap, mCenterPoint.x - bitmap.width / 2.toFloat(),
mCenterPoint.y - bitmap.height / 2.toFloat(), null
)
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun drawOutBoundImg(canvas: Canvas) {
val drawable = resources.getDrawable(mBackgroundImgId, null)
drawable.bounds = Rect(
mOutRange.left.toInt() ,
mOutRange.top.toInt() ,
mOutRange.right.toInt() ,
mOutRange.bottom.toInt()
)
drawable.draw(canvas)
}
private fun drawArc(canvas: Canvas) {
var tmpAngle = mStartAngle
val sweepAngle = 360f / mRewardList.size
for (i in mRewardList.indices) {
fillArc(canvas, tmpAngle, sweepAngle, i)
drawArcBorder(canvas, tmpAngle, sweepAngle)
drawItemContentWithoutIcon(i, canvas, tmpAngle, sweepAngle)
tmpAngle += sweepAngle
}
}
private fun rotateTo(index: Int) {
HandlerUtils.runOnUiThread {
val rand = Random()
rotateTo(index, rand.nextInt() and 1, true)
}
}
// 执行旋转
fun rotateTo(index: Int, rotation: Int, startShow: Boolean) {
if (isRunning) {
return
}
val rotationAssess = if (rotation <= 0) 1 else -1
var roundOfNumber = mRoundOfNumber
val accDuration = 1500L
var decDuration = 1500L
var interpolator: TimeInterpolator
// 从上一次转盘最终的位置处旋转
if (getRotation() != 0.0f) {
setRotation(getRotation() % 360f)
interpolator = AccelerateInterpolator()
animate()
.setInterpolator(interpolator)
.setDuration(accDuration)
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
isRunning = true
}
override fun onAnimationEnd(animation: Animator) {
isRunning = false
setRotation(0f) // 加速到角度为0的水平轴, 接着再减速到目标角度
rotateTo(index, rotation, false)
}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
.rotation(360f * roundOfNumber * rotationAssess)
.start()
return
}
interpolator = DecelerateInterpolator()
// 进入转盘后的第一次玩转盘, 先加速再减速
if (startShow) {
roundOfNumber = roundOfNumber shl 1
decDuration = accDuration shl 1
interpolator = AccelerateDecelerateInterpolator()
}
val targetAngle =
360f * roundOfNumber * rotationAssess + 270f - getAngleOfIndexTarget(index) - 360f / mRewardList.size / 2
animate()
.setInterpolator(interpolator)
.setDuration(decDuration + 500L)
.setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
isRunning = true
}
override fun onAnimationEnd(animation: Animator) {
isRunning = false
setRotation(getRotation() % 360f)
if (mRotateListener != null) {
mRotateListener!!.rotateDone(index)
}
}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
.rotation(targetAngle)
.start()
}
fun setTargetIndex(index: Int) {
targetIndex = index
}
// 开始旋转
fun startLuckyWheelWithTargetIndex(index: Int) {
this.rotateTo(index)
}
fun startLuckyWheelWithRandomTarget() {
val r = Random()
this.rotateTo(r.nextInt(this.rewardListSize - 1))
}
// 取消旋转
fun cancelRotate() {
animate().cancel()
}
}
这里有全部的代码,欢迎查看,LuckySpin
总结
在实现转盘游戏的过程中,总会碰到一些拦路虎。如果避其锋芒,不敢与之对抗,那么此消彼长,它会一步步成长,直到你觉得不能再逃避想要面对它时,它已经是个庞然大物,此时便会有心无力。碰到困难,最简单也是最有效的方法,直接面对它,将它分解成你能解决的许多小块,这就是分治算法的核心:分而治之。