阅读 947

Kotlin版拼图游戏,做出来不会玩就很尴尬

前言

最近在学Kotlin,Kotlin的语法简直是出神入化,都不想回到Java了,果然站在巨人的肩膀上就是不一样,佩服JetBrains的脑回路,所以为了尽快掌握,做了几个小游戏,以下以拼图为例,效果如下:

录屏_选择区域_20210515090641.gif

拼图游戏比较简单,我们一步步来分析下如何实现。

实现过程

图片掏洞

首先我们得有一张图片Bitmap引用,那是肯定的,但是我们要进行缩放,比如手机屏幕尺寸是1080*1920,图片的通过Bitmap.width获取到的是2150,那么我们要进行缩放,把他缩放到宽为1080大小,计算公式为1080/2150,结果是0.502325581395349,通过Matrix.setScale指定就行。

注意的是高的缩放值也要和宽一样,不然就挤压了,最后缩放后,假如宽度是1080,高是2004,我们还要从这个尺寸中掏一块1080*1080的图片出来,毕竟拼图是一个正方形。

image.png

那么这个值如何计算,其实就是高-宽后剩余的高度/2,这个值会在创建新的Bitmap时指定top的参数,其实就是偏移多少px去裁剪。

代码如下:

fun Bitmap.getCenterBitmap(): Bitmap {
    //如果图片宽度大于View宽度
    var min = min(this.height, this.width)
    if (min >= measuredWidth) {
        val matrix = Matrix()
        val sx: Float = measuredWidth / min.toFloat()
        matrix.setScale(sx, sx)
        return Bitmap.createBitmap(
            this, 0, (this.height * sx - measuredHeight / 2).toInt(),
            this.width,
            this.width,
            matrix,
            true
        )
    }
    return this;
}
复制代码

分割成块

然后就是分割成N个块,这个N就是宫格的大小,比如3*34*4,在申明一个N*N的二维数组,每个数组中的值就是对应位置的图片(Bitmap),还要保存位置、图片位于View的left、top,这个位置就是第几个块,比如在3*3的宫格里,2,1的位置第3行第2个,所以值是8。这个值用于判断是不是拼图完成,在后面会详细说。

image.png

所以就要有个类来保存这几个信息。

inner class PictureBlock {
    var bitmap: Bitmap;
    var postion: Int = 0
    var left = 0;
    var top = 0;
    constructor(bitmap: Bitmap, postion: Int, left: Int, top: Int) {
        this.bitmap = bitmap
        this.postion = postion
        this.left = left
        this.top = top
    }
}
复制代码

分割图片就是通过Bitmap.createBitmap指定top、left、width、height。比如在1080大小的View中,每个块大小是1080/3=360,那么比如1,1位置的图片,因该通过以下参数去扣出来。

Bitmap.createBitmap(targetPicture, 1*360, 1*360, 360, 360)
复制代码

所以来一个循环,把一整张图分割成N个小块。

private val pictureBlock2dMap = Array(tableSize) { Array<PictureBlock?>(tableSize) { null } }
 
var top = 0;
var left = 0;
var postion = 0;
for (i in pictureBlock2dMap.indices) {
    for (j in pictureBlock2dMap[i].indices) {
        postion++;
        left = j * gridItemSize;
        top = i * gridItemSize;
        pictureBlock2dMap[i][j] =
            PictureBlock(
                createBitmap(left, top, gridItemSize),
                postion,
                left,
                top
            )
    }
}

 private fun createBitmap(left: Int, top: Int, size: Int): Bitmap {
     return Bitmap.createBitmap(targetPicture, left, top, size, size)
 }
复制代码

我们知道拼图的最后一个方格是个空白,用来移动,所以我们吧这个宫格的最后一个设置为纯色或者透明的Bitmap。

pictureBlock2dMap[tableSize - 1][tableSize - 1]!!.bitmap = createSolidColorBitmap(width)

private fun createSolidColorBitmap(size: Int): Bitmap {
    var bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
    bitmap.eraseColor(Color.TRANSPARENT)
    return bitmap;
}
复制代码

绘制宫格

现在有了二维数组,并且也存放了对应的值,剩下就是在onDraw中绘制了,因为每个item中已经保存了left、top,我们无需计算,直接拿出来用就可以了。

override fun onDraw(canvas: Canvas) {
     var left: Int = 0;
     var top: Int = 0;
     for (i in pictureBlock2dMap.indices) {
         for (j in pictureBlock2dMap[i].indices) {
             var item = pictureBlock2dMap[i][j]!!;
             left = item.left;
             top = item.top;
             var bitmap = pictureBlock2dMap[i][j]!!.bitmap;
             var pictureRect = Rect(0, 0, bitmap.width, bitmap.height);
             var rect = Rect(left, top + offsetTop, gridItemSize + left, gridItemSize + top + offsetTop);
             canvas.drawBitmap(bitmap, pictureRect, rect, Paint())
         }
     }
 }
复制代码

移动图片

这是这个游戏中比较复杂的一步,但仔细想逻辑的话,还是比较简单。

在手指滑动时候有两种做法,这因该是个人习惯的关系,比如手指向上滑动,一种做法是白色位置向上移动,二是白色位置向下移动,而我的做法是向下,这个无关紧要。

首先第一步就是要识别手势,可以借助GestureDetector来完成,我们只重写onFling方法就行,其他不用管。

手势识别非常简单,就是拿到手指按下时候的x、y,和抬起时候的x、y去比较,首先判断哪个滑动的距离比较大,更具这个可以判断出是左右滑动还是上下滑动,比如左右滑动的时候,分别取出x、y的滑动距离进行绝对值运算,如果x大于y,那么就说明是左右滑动,反之是上下滑动。

知道了左右之后,还得判断到底是左还是右,这个更好判断,只需要判断谁大谁小就行,向左滑动,抬起的x肯定比按下的x小。

下面是逻辑代码:

 override fun onFling(
     e1: MotionEvent,
     e2: MotionEvent,
     velocityX: Float,
     velocityY: Float
 ): Boolean {
     var moveXDistance = Math.abs(e1.x - e2.x);
     var moveYDistance = Math.abs(e1.y - e2.y);
     if (moveXDistance > moveYDistance) {
         doMoveLeftRight(e1.x < e2.x)
         return true;
     }
     doMoveTopBottom(e1.y < e2.y)
     return true;
 }
复制代码

然后就是移动二维数组中的Bitmap、和位置,比如此时的布局是这样的(白色方块的位置始终是N*N的值)。

image.png

如果向上滑动,那么布局此时就是这样的,只是对二维数组中的postion、bitmap进行交换,其他数据不变。

image.png

当然为了不让他滑动的生硬,加个过度动画,此时保存的left、top就体现出作用了,比如上图中的1.1位置,滑动到2.1,其实就是top从360过度到720,那么原来8位置的top就是从720过度到360,并且不断调用invalidate()重绘制。

拿左右移动举例。(其实移动是移动的两个位置),移动完成后在动画onAnimationEnd中在交换两个位置的值,因为移动归移动,二维数组中的值终究要变,最后在判断是不是拼图完成。

private fun doMoveLeftRight(direction: Boolean) {
    if ((moveBlockPoint.y == 0 && direction) || (moveBlockPoint.y == tableSize - 1 && !direction)) {
        return;
    }
    step++
    var value = if (direction) 1 else {
        -1
    }
    var start = moveBlockPoint.y * gridItemSize;
    var end = (moveBlockPoint.y - (value)) * gridItemSize
    startAnimator(
        start, end, Point(moveBlockPoint.x, moveBlockPoint.y),
        Point(moveBlockPoint.x, moveBlockPoint.y - (value)),
        true
    )
    moveBlockPoint.y = moveBlockPoint.y - (value);
}

private fun startAnimator(
    start: Int,
    end: Int,
    srcPoint: Point,
    dstPoint: Point,
    type: Boolean
) {
    val handler = object : AnimatorListener {
        override fun onAnimationRepeat(animation: Animator?) {
        }
        override fun onAnimationEnd(animation: Animator?) {
            pictureBlock2dMap[dstPoint.x][dstPoint.y] =
                pictureBlock2dMap[srcPoint.x][srcPoint.y].also {
                    pictureBlock2dMap[srcPoint.x][srcPoint.y] =
                        pictureBlock2dMap[dstPoint.x][dstPoint.y]!!;
                }
            invalidate()
            isFinish()
        }
        override fun onAnimationCancel(animation: Animator?) {
        }
        override fun onAnimationStart(animation: Animator?) {
        }
    }
    var animatorSet = AnimatorSet()
    animatorSet.addListener(handler)
    animatorSet.playTogether(ValueAnimator.ofFloat(start.toFloat(), end.toFloat()).apply {
        duration = slideAnimatorDuration
        interpolator=itemMovInterpolator
        addUpdateListener { animation ->
            var value = animation.animatedValue as Float
            if (type) {
                pictureBlock2dMap[srcPoint.x][srcPoint.y]!!.left = value.toInt();
            } else {
                pictureBlock2dMap[srcPoint.x][srcPoint.y]!!.top = value.toInt();
            }
            invalidate()
        }
    }, ValueAnimator.ofFloat(end.toFloat(), start.toFloat()).apply {
        duration = slideAnimatorDuration
        interpolator=itemMovInterpolator
        addUpdateListener { animation ->
            var value = animation.animatedValue as Float
            if (type) {
                pictureBlock2dMap[dstPoint.x][dstPoint.y]!!.left = value.toInt();
            } else {
                pictureBlock2dMap[dstPoint.x][dstPoint.y]!!.top = value.toInt();
            }
            invalidate()
        }
    });
    animatorSet.start()
}

复制代码

判断完成

比如最后移动成了这个样子,此时在向左滑动即可拼图完成,那么该如何判断呢?

image.png

这时候二维数组保存的postion就体现出作用了,我们只需要判断顺序是不是123456789就可以了。

如果二维数组中不会判断,那么这样来,现在有一个集合,集合中是1-9的数组,那么如何判断顺序是不是123456789呢?

办法有很多,比如转成字符串,和"123456789"比较,还有一种办法是比较差,就像下面这样,因为如果是顺序的话,每相邻的两个差必定是1。

private fun List<Int>.isOrder(): Boolean {
    for (i in 1 until this.size) {
        if (this[i] - this[i - 1] != 1) {
            return false
        }
    }
    return true;
}
复制代码

完整代码

当然还有些细节没提到,可在以下代码啊中查看,比如在长按后,显示原图,

录屏_选择区域_20210515105356.gif

package com.example.kotlindemo

import android.animation.Animator
import android.animation.Animator.AnimatorListener
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.os.Handler
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.animation.*
import android.view.animation.Interpolator
import android.widget.Toast
import kotlin.math.min
import kotlin.random.Random


class JigsawView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), GestureDetector.OnGestureListener {

    private var TAG = "TAG";

    //表格大小
    private var tableSize = 3;

    //二维数组,存放图标块
    private val pictureBlock2dMap = Array(tableSize) { Array<PictureBlock?>(tableSize) { null } }


    //手势监听
    private var gestureDetector: GestureDetector = GestureDetector(context, this);

    //是否开始
    private var isStart: Boolean = false;

    //空白点坐标
    private var moveBlockPoint: Point = Point(-1, -1);

    //top偏移
    private var offsetTop: Int = 0;

    //图片大小
    private var gridItemSize = 0;
    private var slideAnimatorDuration: Long = 150;
    private var showSourceBitmap = false;
    //移动步数
    private var step: Int = 0;

    private var itemMovInterpolator:Interpolator=OvershootInterpolator()
    //目标Bitmap
    private lateinit var targetPicture: Bitmap;

    fun setPicture(bitmap: Bitmap) {
        post {
            targetPicture = bitmap.getCenterBitmap();
            parsePicture();
            step = 0;
        }
    }

    //分割图片
    private fun parsePicture() {
        var top = 0;
        var left = 0;
        var postion = 0;
        for (i in pictureBlock2dMap.indices) {
            for (j in pictureBlock2dMap[i].indices) {
                postion++;
                left = j * gridItemSize;
                top = i * gridItemSize;
                pictureBlock2dMap[i][j] =
                    PictureBlock(
                        createBitmap(left, top, gridItemSize),
                        postion,
                        left,
                        top
                    )
            }
        }
        pictureBlock2dMap[tableSize - 1][tableSize - 1]!!.bitmap = createSolidColorBitmap(width)
        isStart = true;
        randomPostion();
        invalidate()

    }

    private fun randomPostion() {
        for (i in 1..pictureBlock2dMap.size * pictureBlock2dMap.size) {
            var srcIndex = Random.nextInt(0, pictureBlock2dMap.size);
            var dstIndex = Random.nextInt(0, pictureBlock2dMap.size);
            var srcIndex1 = Random.nextInt(0, pictureBlock2dMap.size);
            var dstIndex2 = Random.nextInt(0, pictureBlock2dMap.size);
            pictureBlock2dMap[srcIndex][dstIndex]!!.swap(pictureBlock2dMap[srcIndex1][dstIndex2]!!);
        }

        for (i in pictureBlock2dMap.indices) {
            for (j in pictureBlock2dMap[i].indices) {
                var item = pictureBlock2dMap[i][j]!!;
                if (item.postion == tableSize * tableSize) {
                    moveBlockPoint.set(i, j)
                    return
                }
            }
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        offsetTop = (h - w) / 2;
        gridItemSize = w / tableSize;
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var min = min(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(min, min)
    }

    override fun onDraw(canvas: Canvas) {
        if (!isStart) {
            return
        }
        if (showSourceBitmap) {
            var pictureRect = Rect(0, 0, targetPicture.width, targetPicture.height);
            var rect = Rect(0, 0, measuredWidth, measuredHeight);
            canvas.drawBitmap(targetPicture, pictureRect, rect, Paint())
            return
        }
        var left: Int = 0;
        var top: Int = 0;
        for (i in pictureBlock2dMap.indices) {
            for (j in pictureBlock2dMap[i].indices) {
                var item = pictureBlock2dMap[i][j]!!;
                left = item.left;
                top = item.top;
                var bitmap = pictureBlock2dMap[i][j]!!.bitmap;
                var pictureRect = Rect(0, 0, bitmap.width, bitmap.height);
                var rect = Rect(left, top + offsetTop, gridItemSize + left, gridItemSize + top + offsetTop);
                canvas.drawBitmap(bitmap, pictureRect, rect, Paint())
            }
        }

    }

    //交换内容
    private fun PictureBlock.swap(target: PictureBlock) {
        target.postion = this.postion.also {
            this.postion = target.postion;
        }
        target.bitmap = this.bitmap.also {
            this.bitmap = target.bitmap;
        }
    }

    fun Bitmap.getCenterBitmap(): Bitmap {
        //如果图片宽度大于View宽度
        var min = min(this.height, this.width)
        if (min >= measuredWidth) {
            val matrix = Matrix()
            val sx: Float = measuredWidth / min.toFloat()
            matrix.setScale(sx, sx)
            return Bitmap.createBitmap(
                this, 0, (this.height * sx - measuredHeight / 2).toInt(),
                this.width,
                this.width,
                matrix,
                true
            )
        }
        return this;
    }

    fun setTarget(targetPicture: Bitmap) {
        this.targetPicture = targetPicture;
    }


    private fun createSolidColorBitmap(size: Int): Bitmap {
        var bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
        bitmap.eraseColor(Color.TRANSPARENT)
        return bitmap;
    }

    private fun createBitmap(left: Int, top: Int, size: Int): Bitmap {
        return Bitmap.createBitmap(targetPicture, left, top, size, size)
    }


    private fun List<Int>.isOrder(): Boolean {
        for (i in 1 until this.size) {
            if (this[i] - this[i - 1] != 1) {
                return false
            }
        }
        return true;
    }

    private fun isFinish() {
        var list = mutableListOf<Int>();
        for (i in pictureBlock2dMap.indices) {
            for (j in pictureBlock2dMap[i].indices) {
                var item = pictureBlock2dMap[i][j]!!;
                list.add(item.postion)
            }
        }
        if (list.isOrder()) {
            finish()
        }
    }

    private fun finish() {
        Toast.makeText(context, "", Toast.LENGTH_SHORT).show()
    }

    private fun startAnimator(
        start: Int,
        end: Int,
        srcPoint: Point,
        dstPoint: Point,
        type: Boolean
    ) {
        val handler = object : AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                pictureBlock2dMap[dstPoint.x][dstPoint.y] =
                    pictureBlock2dMap[srcPoint.x][srcPoint.y].also {
                        pictureBlock2dMap[srcPoint.x][srcPoint.y] =
                            pictureBlock2dMap[dstPoint.x][dstPoint.y]!!;
                    }
                invalidate()
                isFinish()

            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {
            }

        }
        var animatorSet = AnimatorSet()
        animatorSet.addListener(handler)
        animatorSet.playTogether(ValueAnimator.ofFloat(start.toFloat(), end.toFloat()).apply {
            duration = slideAnimatorDuration
            interpolator=itemMovInterpolator
            addUpdateListener { animation ->
                var value = animation.animatedValue as Float
                if (type) {
                    pictureBlock2dMap[srcPoint.x][srcPoint.y]!!.left = value.toInt();
                } else {
                    pictureBlock2dMap[srcPoint.x][srcPoint.y]!!.top = value.toInt();

                }
                invalidate()
            }
        }, ValueAnimator.ofFloat(end.toFloat(), start.toFloat()).apply {
            duration = slideAnimatorDuration
            interpolator=itemMovInterpolator
            addUpdateListener { animation ->
                var value = animation.animatedValue as Float
                if (type) {
                    pictureBlock2dMap[dstPoint.x][dstPoint.y]!!.left = value.toInt();
                } else {
                    pictureBlock2dMap[dstPoint.x][dstPoint.y]!!.top = value.toInt();

                }
                invalidate()
            }
        });
        animatorSet.start()

    }


    private fun doMoveTopBottom(direction: Boolean) {
        if ((moveBlockPoint.x == 0 && direction) || (moveBlockPoint.x == tableSize - 1 && !direction)) {
            return;
        }
        step++;
        var value = if (direction) 1 else {
            -1
        }

        var start = moveBlockPoint.x * gridItemSize;
        var end = (moveBlockPoint.x - (value)) * gridItemSize

        startAnimator(
            start, end, Point(moveBlockPoint.x, moveBlockPoint.y),
            Point(moveBlockPoint.x - (value), moveBlockPoint.y),
            false
        )
        moveBlockPoint.x = moveBlockPoint.x - (value);

    }

    private fun doMoveLeftRight(direction: Boolean) {
        if ((moveBlockPoint.y == 0 && direction) || (moveBlockPoint.y == tableSize - 1 && !direction)) {
            return;
        }
        step++
        var value = if (direction) 1 else {
            -1
        }

        var start = moveBlockPoint.y * gridItemSize;
        var end = (moveBlockPoint.y - (value)) * gridItemSize

        startAnimator(
            start, end, Point(moveBlockPoint.x, moveBlockPoint.y),
            Point(moveBlockPoint.x, moveBlockPoint.y - (value)),
            true
        )


        moveBlockPoint.y = moveBlockPoint.y - (value);

    }

    /**
     * 图片块
     */
    inner class PictureBlock {
        var bitmap: Bitmap;
        var postion: Int = 0
        var left = 0;
        var top = 0;

        constructor(bitmap: Bitmap, postion: Int, left: Int, top: Int) {
            this.bitmap = bitmap
            this.postion = postion
            this.left = left
            this.top = top
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        Log.i(TAG, "onTouchEvent: ")
        if (event.action == MotionEvent.ACTION_UP) {
            Log.i(TAG, "onDown: ACTION_UP")
            showSourceBitmap = false;
            invalidate()
        }
        return gestureDetector.onTouchEvent(event);
    }

    override fun onShowPress(e: MotionEvent?) {
        Log.i(TAG, "onShowPress: ")
    }

    override fun onSingleTapUp(e: MotionEvent?): Boolean {
        Log.i(TAG, "onSingleTapUp: ")
        return true;

    }

    override fun onDown(e: MotionEvent): Boolean {
        Log.i(TAG, "onDown: ")

        return true;
    }

    override fun onFling(
        e1: MotionEvent,
        e2: MotionEvent,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        var moveXDistance = Math.abs(e1.x - e2.x);
        var moveYDistance = Math.abs(e1.y - e2.y);
        if (moveXDistance > moveYDistance) {
            doMoveLeftRight(e1.x < e2.x)
            return true;
        }
        doMoveTopBottom(e1.y < e2.y)
        return true;
    }

    override fun onScroll(
        e1: MotionEvent,
        e2: MotionEvent,
        distanceX: Float,
        distanceY: Float
    ): Boolean {
        return true;
    }

    override fun onLongPress(e: MotionEvent) {
        showSourceBitmap = true;
        invalidate()
        Handler().postDelayed({
            showSourceBitmap = false;
            invalidate()
        }, 5000)
    }
}
复制代码

使用方法

上面的JigsawView类不依赖于其他类,导入后即可运行。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <RelativeLayout

        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <com.example.kotlindemo.JigsawView

            android:id="@+id/jigsawView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true">

        </com.example.kotlindemo.JigsawView>
    </RelativeLayout>
</layout>
复制代码
class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding;
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);

        binding.jigsawView.setPicture(BitmapFactory.decodeResource(resources, R.drawable.back))
    }
}
复制代码
文章分类
Android
文章标签