Android - 九宫格

4 篇文章 0 订阅
3 篇文章 0 订阅

前言

作为一个懂点Android的搬砖猿,作为一个有孩子的爸爸,看到孩子天天玩农药,就想着怎么减少他玩农药的时间.后来观察了他一段时间,总结出一个规律:他刚进游戏的时候会先去用钻石抽奖,看到转来转去的动画,他还特别高兴.于是乎就用android的知识,写了一个九宫格抽奖的Demo.
演示

思考

怎么样实现一个九宫格那?我想到的是Android现有的控件有哪些可以实现:GridView,RecyclerView,这些都可以实现九宫格,且我都实现了一遍.但是写出来之后,我觉得没有什么挑战性,就想着:作为一个Android搬转猿,怎么也得用自定义View实现这样的效果.于是,就去做了.
怎么样自定义一个九宫偶格那?我在打草纸上画了一个九宫格,它有一个圆角矩形作为背景,背景的上层是由九个小圆角矩形的方块组成的,每一个方块都有一个数字,中间的那个方块比较特殊,它是一个圆,点击它就会有一个颜色块在它周围顺时针转动,颇有众星拱月的感觉.
在写代码之前,先思考几个问题:

  1. 自定义View的步骤
    由于我们需要手动画一个九宫格,所以我们需要继承View,重写onDraw().
  2. 如何安排每一个小方块的位置与大小
    我们可以把view的宽高等分成9份,这样我们就计算出了每一份的大小,再计算每一份的左上角的坐标,这样就实现了9个方块的布局.
  3. 如何实现顺时针的动画
    观察我们画在纸上的九宫格,模拟着颜色块走过的轨迹,可以得出一个轨迹对应到每一个方块索引的关系数组orderArray(0,1,2,5,8,7,6,3).得到一个轨迹数组后,就可以通过实现颜色块的移动了.不明白的可以看往下的代码.
  4. 你写的代码review了吗
    自我审查代码时自己感觉不舒服的地方,就是我们代码有待改进的地方,也是我们提升自我编码能力的途径之一.所以,多做检查,发现自己的不足,再弥补自己的不足.

代码

  1. 九宫格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()
    }
}
  1. 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:----- ", )
        }
    }

总结

心有猛虎,细嗅蔷薇.现在细品这句话,觉的满符合我们学习新知识,应用新知识的历程.
在雨后初晴的清晨,猛虎静坐,鼻尖微触,这妖艳的蔷薇.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值