preface

I have been learning Kotlin recently, and the syntax of Kotlin is so amazing that I don’t even want to go back to Java. As expected, STANDING on the shoulders of giants is just different. I admire JetBrains’ brain circuit, so IN order to master it as soon as possible, I made several small games.

Jigsaw puzzle is relatively simple, let’s analyze step by step how to achieve.

The implementation process

Pictures cut hole

First we need to have a Bitmap reference, that’s for sure, but we need to scale it. For example, the screen size of the phone is 1080*1920, and the bitmap. width of the image is 2150, so we need to scale it to 1080, calculate the formula 1080/2150, The result is 0.502325581395349, specified by matrix.setScale.

Note that the height of the scale must be the same as the width, otherwise it will be squeezed. Finally, if the width is 1080 and the height is 2004, we will also need to extract a 1080*1080 image from this size, after all, the puzzle is a square.

This value will specify the top parameter when creating the new Bitmap, which is the offset by how many pixels to crop.

The code is as follows:

fun Bitmap.getCenterBitmap(): Bitmap {
    // If the image width is larger than the View width
    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;
}
Copy the code

Split into pieces

The N is the size of the cell, such as 3*3, 4*4, declare an N*N two-dimensional array, the value in each array is the corresponding position of the picture (Bitmap), and save the position, the picture is located in the View left, top, this position is the number of blocks, such as in 3*3 cell, 2,1, row 3, row 2, so the value is 8. This value is used to determine whether the puzzle is complete, as discussed later.

So you have to have a class to hold this information.

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
    }
}
Copy the code

Bitmap.createBitmap specifies top, left, width, height. For example, in the View of 1080 size, each block size is 1080/3=360, then for example, the image of position 1,1 should be extracted with the following parameters.

Bitmap.createBitmap(targetPicture, 1*360.1*360.360.360)
Copy the code

So let’s go through a loop and divide the whole picture into N pieces.

privateval 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)
 }
Copy the code

We know that the last square of the puzzle is a blank space for movement, so let’s set the last square of the grid to a solid color or transparent 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;
}
Copy the code

Draw GongGe

Now that we have the two-dimensional array and store the corresponding values, the only thing left to do is to draw in onDraw, because each item has left and top stored, we don’t need to calculate them, we can just pull them out and use them.

override fun onDraw(canvas: Canvas) {
     var left: Int = 0;
     var top: Int = 0;
     for (i in pictureBlock2dMap.indices) {
         for (j in pictureBlock2dMap[i].indices) {
             varitem = pictureBlock2dMap[i][j]!! ; left = item.left; top = item.top;varbitmap = 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())
         }
     }
 }
Copy the code

Moving pictures

This is one of the more complicated steps in the game, but when you think about the logic, it’s relatively simple.

There are two methods of finger sliding, which is a personal habit, such as finger sliding up, one method is to move up the white position, the other is to move down the white position, and my method is to move down, this does not matter.

The first step is to recognize the gesture. This can be done using the GestureDetector. We just override the onFling method and leave the rest alone.

Gesture recognition is very simple, is to get the fingers when press the x, y, and lift when x and y to compare, first determine which sliding distance is larger, more about this can determine is slip and slide up and down, such as sliding around, pulled out sliding distance of absolute value of x, y, operations, if x is greater than y, So it’s left or right, and up or down.

So once you know left and right, you have to figure out whether it’s left or right, and that’s a better way to figure out who’s big and who’s small, so if you swipe left, the x that you lift up is definitely smaller than the x that you press down.

Here is the logical code:

 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;
 }
Copy the code

Then move the Bitmap, and position in the two-dimensional array, such that the layout is like this (the position of the white square is always N*N).

If you swipe up, the layout looks like this, just swapping postion and bitmap in the 2-D array.

Of course, in order to avoid the rigidity of sliding, excessive animation is added, at this time, the left and top are saved to reflect the effect. For example, in the figure 1.1, slide to 2.1, in fact, the top is changed from 360 to 720, so the top of the original 8 position is changed from 720 to 360. And constantly call invalidate() to redraw.

Take moving left to right. (In fact, the movement is the two positions of the movement), after the completion of the movement in the animation onAnimationEnd, the value of the two positions should be exchanged, because the movement is the movement, the value in the two-dimensional array will eventually change, and finally judge whether the puzzle is completed.

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() }Copy the code

Determine to complete

Let’s say you end up like this, and you swipe left to finish the puzzle, so how do you tell?

In this case, postion saved in 2d array is in effect, we just need to determine whether the order is 123456789.

If you can’t find 123456789 in a two-dimensional array, then you have a set of arrays 1 through 9. How do you find 123456789 in order?

There are many ways to do this, such as converting it to a string and comparing it to “123456789”. There is also a way to compare it badly, such as the following, because if it is sequential, every two adjacent differences must be 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;
}
Copy the code

The complete code

Of course, there are some details that are not mentioned, which can be seen in the following code, such as the original image after a long press,

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";

    // Table size
    private var tableSize = 3;

    // The icon block is stored in a two-dimensional array
    privateval pictureBlock2dMap = Array(tableSize) { Array<PictureBlock? >(tableSize) {null}}// gesture listener
    private var gestureDetector: GestureDetector = GestureDetector(context, this);

    // Whether to start
    private var isStart: Boolean = false;

    // Empty point coordinates
    private var moveBlockPoint: Point = Point(-1, -1);

    / / top offset
    private var offsetTop: Int = 0;

    // Image size
    private var gridItemSize = 0;
    private var slideAnimatorDuration: Long = 150;
    private var showSourceBitmap = false;
    // Move the number of steps
    private var step: Int = 0;

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

    fun setPicture(bitmap: Bitmap) {
        post {
            targetPicture = bitmap.getCenterBitmap();
            parsePicture();
            step = 0; }}// Split the image
    private fun parsePicture(a) {
        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(a) {
        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) {
                varitem = 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) {
                varitem = pictureBlock2dMap[i][j]!! ; left = item.left; top = item.top;varbitmap = 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())
            }
        }

    }

    // Exchange content
    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 {
        // If the image width is larger than the View width
        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(a) {
        var list = mutableListOf<Int>();
        for (i in pictureBlock2dMap.indices) {
            for (j in pictureBlock2dMap[i].indices) {
                varitem = pictureBlock2dMap[i][j]!! ; list.add(item.postion) } }if (list.isOrder()) {
            finish()
        }
    }

    private fun finish(a) {
        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);

    }

    /** ** block */
    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)}}Copy the code

Method of use

The JigsawView class above does not depend on other classes and can be imported and run.

<? 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>
Copy the code
class MainActivity : AppCompatActivity(a){
    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))
    }
}
Copy the code