Kotlin jigsaw puzzle, it's embarrassing if you can't play it, [Android interview question]

So there should be a class to store these 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
    }
} 

To split a picture, you specify the top, left, width and height through Bitmap.createBitmap. For example, in a View with a size of 1080, the size of each block is 1080 / 3 = 360. For example, the picture at position 1 and 1 should be deducted through the following parameters.

Bitmap.createBitmap(targetPicture, 1*360, 1*360, 360, 360) 

So let's have a loop and divide a whole graph into N small blocks.

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)
 } 

We know that the last square of the puzzle is a blank for moving, so let's set the last square of this grid as 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;
} 

Draw grid

Now we have a two-dimensional array and store the corresponding values. The rest is drawn in onDraw. Because left and top have been saved in each item, we don't need to calculate. We can take them out directly for use.

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())
         }
     }
 } 

Move picture

This is a complicated step in the game, but if you think about the logic carefully, it is still relatively simple.

There are two ways to slide your fingers, because it should be a personal habit. For example, slide your fingers upward. One way is to move the white position upward and the other way is to move the white position downward. My way is to move down, which doesn't matter.

The first step is to recognize gestures, which can be completed with the help of GestureDetector. We just rewrite the onFling method, and don't worry about anything else.

Gesture recognition is very simple, that is, to compare the X and y when the finger is pressed with the X and y when the finger is lifted. First, judge which sliding distance is larger. More importantly, you can judge whether it is sliding left and right or up and down. For example, when sliding left and right, take out the sliding distance of X and Y respectively for absolute value calculation. If x is greater than y, it indicates that it is sliding left and right, On the contrary, it slides up and down.

After knowing the left and right, you have to judge whether it is left or right. This is a better judgment. You only need to judge who is big and who is small. Slide to the left, and the raised x must be smaller than the pressed X.

Here is the logic 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;
 } 

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

If you slide upward, the layout is like this at this time. Only the position and bitmap in the two-dimensional array are exchanged, and other data remains unchanged.

Of course, in order to prevent him from sliding rigidly, add an excessive animation. At this time, the saved left and top will play a role. For example, sliding to 2.1 at position 1.1 in the above figure actually means that the top transitions from 360 to 720. Then the top at position 8 transitions from 720 to 360, and continuously call invalidate() to redraw.

Take the example of moving left and right( In fact, the movement is the two positions of the movement). After the movement is completed, the values of the two positions are exchanged in the animation onAnimationEnd. Because the movement belongs to the movement, the values in the two-dimensional array will eventually change. Finally, it is judged whether the puzzle is completed or not.

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()
} 

Judgment complete

For example, when the final movement is like this, the puzzle can be completed by sliding to the left, so how to judge?

At this time, the position saved by the two-dimensional array plays a role. We only need to judge whether the order is 123456789.

If there is no judgment in the two-dimensional array, then in this way, there is a set with 1-9 arrays in the set. How to judge whether the order is 123456789?

There are many methods, such as converting to a string and comparing it with "123456789". Another method is poor, like the following, because if it is sequential, each adjacent difference 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;
} 

Complete code

Of course, some details are not mentioned. You can check them in the following code. For example, after long pressing, the original image will be displayed,

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;

    //Two dimensional array to store icon blocks
    private val pictureBlock2dMap = Array(tableSize) { Array<PictureBlock?>(tableSize) { null } }


    //Gesture monitoring
    private var gestureDetector: GestureDetector = GestureDetector(context, this);

    //Start
    private var isStart: Boolean = false;

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

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

    //Picture size
    private var gridItemSize = 0;
    private var slideAnimatorDuration: Long = 150;
    private var showSourceBitmap = false;
    //Moving steps
    private var step: Int = 0;

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

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

    //Split picture
    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())
            }
        }

    }

    //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 picture width is greater 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() {
        var list = mutableListOf<Int>();
        for (i in pictureBlock2dMap.indices) {
            for (j in pictureBlock2dMap[i].indices) {
Picture, 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) {
 

Tags: Android Design Pattern kotlin

Posted on Mon, 06 Sep 2021 22:28:23 -0400 by keiron77