前言
作为一个懂点Android的搬砖猿,作为一个有孩子的爸爸,看到孩子天天玩农药,就想着怎么减少他玩农药的时间.后来观察了他一段时间,总结出一个规律:他刚进游戏的时候会先去用钻石抽奖,看到转来转去的动画,他还特别高兴.于是乎就用android的知识,写了一个九宫格抽奖的Demo.
思考
怎么样实现一个九宫格那?我想到的是Android现有的控件有哪些可以实现:GridView,RecyclerView,这些都可以实现九宫格,且我都实现了一遍.但是写出来之后,我觉得没有什么挑战性,就想着:作为一个Android搬转猿,怎么也得用自定义View实现这样的效果.于是,就去做了.
怎么样自定义一个九宫偶格那?我在打草纸上画了一个九宫格,它有一个圆角矩形作为背景,背景的上层是由九个小圆角矩形的方块组成的,每一个方块都有一个数字,中间的那个方块比较特殊,它是一个圆,点击它就会有一个颜色块在它周围顺时针转动,颇有众星拱月的感觉.
在写代码之前,先思考几个问题:
- 自定义View的步骤
由于我们需要手动画一个九宫格,所以我们需要继承View,重写onDraw(). - 如何安排每一个小方块的位置与大小
我们可以把view的宽高等分成9份,这样我们就计算出了每一份的大小,再计算每一份的左上角的坐标,这样就实现了9个方块的布局. - 如何实现顺时针的动画
观察我们画在纸上的九宫格,模拟着颜色块走过的轨迹,可以得出一个轨迹对应到每一个方块索引的关系数组orderArray(0,1,2,5,8,7,6,3).得到一个轨迹数组后,就可以通过实现颜色块的移动了.不明白的可以看往下的代码. - 你写的代码review了吗
自我审查代码时自己感觉不舒服的地方,就是我们代码有待改进的地方,也是我们提升自我编码能力的途径之一.所以,多做检查,发现自己的不足,再弥补自己的不足.
代码
- 九宫格View
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import java.lang.IllegalArgumentException
data class ItemInfo(val reward: Int)
class NineSpinView : View {
private val mTextPaint: Paint = Paint()
private val mBgPaint: Paint = Paint()
private val mHitPaint: Paint = Paint()
private var mBackgroundColor = 0
private var mTextSize = 0F
private var mTextColor = 0
private var mRowAndColumn = 0
private var mRadius = 0F
private var mCenterIndex = 0
private var mHitColor = 0
private var mItemImg: Drawable? = null
private var mCenterImg: Drawable? = null
private var mItemSize = 0f
private var mBitmapCache: HashMap<Int, Bitmap?>? = null
private var centerRectF: RectF? = null
private var mCompleteListener: (() -> Unit)? = null
private var mCenterListener: (() -> Unit)? = null
private val orderArray = intArrayOf(0, 1, 2, 5, 8, 7, 6, 3)
private var hitIndex = -1
private var repeatCount = 0
private var targetIndex = -1
private var isSpinning = false
private var isFinish = false
private var dataList:ArrayList<ItemInfo>? = null
companion object {
const val TAG = "NineSpinView"
const val MAX_REPEAT_COUNT = 3
const val DEFAULT_TEXT_SIZE = 31F
const val DEFAULT_RADIUS = 0F
const val DEFAULT_ROW_COLUMN = 3
const val DEFAULT_TEXT_COLOR = Color.BLACK
const val DEFAULT_HIT_COLOR = Color.YELLOW
}
constructor(context: Context?) : super(context)
constructor(context: Context?, attributes: AttributeSet?) : super(context, attributes) {
val typeArray = context?.resources?.obtainAttributes(attributes, R.styleable.NineSpinView)
if (typeArray != null) {
mBackgroundColor =
typeArray.getColor(R.styleable.NineSpinView_spin_backgroundColor, 0)
mTextSize =
typeArray.getDimension(
R.styleable.NineSpinView_spin_textSize,
DEFAULT_TEXT_SIZE
)
mTextColor =
typeArray.getColor(
R.styleable.NineSpinView_spin_textColor,
DEFAULT_TEXT_COLOR
)
mHitColor =
typeArray.getColor(
R.styleable.NineSpinView_spin_hit_color,
DEFAULT_HIT_COLOR
)
mRowAndColumn =
typeArray.getInt(
R.styleable.NineSpinView_spin_rowAndColumn,
DEFAULT_ROW_COLUMN
)
mRadius = typeArray.getDimension(
R.styleable.NineSpinView_spin_radius,
DEFAULT_RADIUS
)
mItemImg = typeArray.getDrawable(
R.styleable.NineSpinView_spin_item_img
)
mCenterImg = typeArray.getDrawable(
R.styleable.NineSpinView_spin_center_img
)
typeArray.recycle()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mItemSize = (width / mRowAndColumn).toFloat()
mCenterIndex = mRowAndColumn * mRowAndColumn / 2
if (mTextSize > mItemSize) {
mTextSize /= 3
} else if (mTextSize > mItemSize / 2) {
mTextSize = mTextSize * 2 / 3
}
initBitmapCache()
initPaint()
}
private fun initBitmapCache() {
mBitmapCache = HashMap()
val centerImg = BitmapFactory.decodeResource(resources, R.drawable.spin_nine_center)
val itemImg = ImageUtils.drawable2Bitmap(mItemImg!!)
val width = itemImg.width
val height = itemImg.height
val matrix = Matrix()
mRadius *= (mItemSize * 0.98f / width.toFloat())
matrix.setScale(
mItemSize * 0.98f / width.toFloat(),
mItemSize * 0.98f / height.toFloat()
)
val scaleItemImg = Bitmap.createBitmap(itemImg, 0, 0, width, height, matrix, true)
val scaleCenterImg =
Bitmap.createBitmap(centerImg, 0, 0, centerImg.width, centerImg.height, matrix, true)
(0 until mRowAndColumn * mRowAndColumn).forEach { index ->
val newBitmap = if (index == mCenterIndex) {
scaleCenterImg
} else {
scaleItemImg
}
mBitmapCache?.set(index, newBitmap)
}
}
private fun initPaint() {
mTextPaint.isAntiAlias = true
mTextPaint.style = Paint.Style.FILL
mTextPaint.color = mTextColor
mTextPaint.textSize = mTextSize
mTextPaint.textAlign = Paint.Align.CENTER
mTextPaint.isDither = true
mBgPaint.isAntiAlias = true
mBgPaint.color = mBackgroundColor
mBgPaint.style = Paint.Style.FILL
mHitPaint.isAntiAlias = true
mHitPaint.color = mHitColor
mHitPaint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// 背景层
drawBackground(canvas)
// 内容层
drawContent(canvas)
}
private fun drawBackground(canvas: Canvas?) {
canvas?.drawRoundRect(
0f,
0f,
width.toFloat(),
height.toFloat(),
mRadius,
mRadius,
mBgPaint
)
}
private fun drawContent(canvas: Canvas?) {
val size = mItemSize
var row = 0
(0 until mRowAndColumn * mRowAndColumn).forEach { i ->
val column = i % mRowAndColumn
if (column == 0 && i > 0) {
row++
}
val left = column * size + 3
val top = row * size + 3
drawItem(left, top, canvas, i, i)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_UP) {
val touchX = event.x
val touchY = event.y
centerRectF?.let { centerRectF ->
if (touchX > centerRectF.left
&& touchX < centerRectF.right
&& touchY > centerRectF.top &&
touchY < centerRectF.bottom
) {
if (isSpinning) {
return@let
}
isSpinning = true
reset()
this.mCenterListener?.invoke()
}
}
}
return true
}
private fun drawItem(left: Float, top: Float, canvas: Canvas?, imgId: Int, index: Int) {
val newBitmap: Bitmap? = mBitmapCache?.get(imgId)
if (index == mCenterIndex) {
val bitmap: Bitmap = newBitmap!!
if (centerRectF == null) {
centerRectF = RectF(left, top, left + mItemSize, top + mItemSize)
}
val imgLeft = left + mItemSize / 2 - (newBitmap.width.div(2))
val imgTop = top + mItemSize / 2 - (newBitmap.height.div(2))
canvas?.drawBitmap(bitmap, imgLeft, imgTop, null)
} else {
val bitmap: Bitmap = newBitmap!!
canvas?.drawBitmap(bitmap, left, top, null)
if (index == hitIndex) {
canvas?.drawRoundRect(
left,
top,
left + mItemSize,
top + mItemSize,
mRadius,
mRadius,
mHitPaint
)
}
val nLeft = left + mItemSize / 2
val nTop = top + mItemSize * 1.2f / 2
if (dataList != null && dataList!!.size <= mRowAndColumn*mRowAndColumn) {
val reward = dataList!![index].reward
canvas?.drawText("x $reward", nLeft, nTop, mTextPaint)
} else {
canvas?.drawText("x $index", nLeft, nTop, mTextPaint)
}
}
}
fun startSpinWithTargetReward(reward: Int) {
dataList?.forEachIndexed{index, itemInfo ->
if (reward == itemInfo.reward) {
this.targetIndex = index
return@forEachIndexed
}
}
spinNineAnim()
}
private fun spinNineAnim() {
orderArray.forEachIndexed { index, i ->
handler.postDelayed({
if (isFinish) {
return@postDelayed
}
hitIndex = i
if (targetIndex == i && repeatCount == MAX_REPEAT_COUNT) {
isSpinning = false
isFinish = true
this.mCompleteListener?.invoke()
return@postDelayed
}
invalidate()
if (i == orderArray.last() && repeatCount++ < MAX_REPEAT_COUNT) {
spinNineAnim()
}
}, ((index + 1) * 100).toLong())
}
}
fun setData(data:ArrayList<ItemInfo>) {
if (data.size <= 0 && data.size > mRowAndColumn * mRowAndColumn) throw IllegalArgumentException(
"data not use"
)
this.dataList = data
}
fun setOnCenterClickListener(clickListener: () -> Unit) {
this.mCenterListener = clickListener
}
fun setCompleteListener(complete: () -> Unit) {
this.mCompleteListener = complete
}
private fun reset() {
hitIndex = -1i
targetIndex = -1
repeatCount = 0
isFinish = false
invalidate()
}
}
- j简单使用
<NineSpinView
android:id="@+id/view_nie_spin"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_margin="50dp"
android:background="#00ffffff"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spin_hit_color="#a0fff000"
app:spin_item_img="@drawable/spin_nine_item_bg"
app:spin_radius="13dp"
app:spin_textColor="@color/purple_200"
app:spin_textSize="30sp" />
private fun initNineSpin() {
val nineSpinView = findViewById<NineSpinView>(R.id.view_nie_spin)
val dataList = ArrayList<ItemInfo>(9)
(5..13).forEach {
dataList.add(ItemInfo(it))
}
nineSpinView.setData(dataList)
nineSpinView.setOnCenterClickListener {
val targetIndex = (10..13).random()
nineSpinView.startSpinWithTargetReward(targetIndex)
}
nineSpinView.setCompleteListener {
Log.e("TAG", "spin complete:----- ", )
}
}
总结
心有猛虎,细嗅蔷薇.现在细品这句话,觉的满符合我们学习新知识,应用新知识的历程.
在雨后初晴的清晨,猛虎静坐,鼻尖微触,这妖艳的蔷薇.