前言
最近在学Kotlin,Kotlin的语法简直是出神入化,都不想回到Java了,果然站在巨人的肩膀上就是不一样,佩服JetBrains的脑回路,所以为了尽快掌握,做了几个小游戏,以下以拼图为例,效果如下:
拼图游戏比较简单,我们一步步来分析下如何实现。
实现过程
图片掏洞
首先我们得有一张图片Bitmap引用,那是肯定的,但是我们要进行缩放,比如手机屏幕尺寸是1080*1920
,图片的通过Bitmap.width获取到的是2150,那么我们要进行缩放,把他缩放到宽为1080大小,计算公式为1080/2150,结果是0.502325581395349,通过Matrix.setScale指定就行。
注意的是高的缩放值也要和宽一样,不然就挤压了,最后缩放后,假如宽度是1080,高是2004,我们还要从这个尺寸中掏一块1080*1080
的图片出来,毕竟拼图是一个正方形。
那么这个值如何计算,其实就是高-宽后剩余的高度/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*3
、4*4
,在申明一个N*N
的二维数组,每个数组中的值就是对应位置的图片(Bitmap),还要保存位置、图片位于View的left、top,这个位置就是第几个块,比如在3*3
的宫格里,2,1
的位置第3行第2个,所以值是8。这个值用于判断是不是拼图完成,在后面会详细说。
所以就要有个类来保存这几个信息。
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的值)。
如果向上滑动,那么布局此时就是这样的,只是对二维数组中的postion、bitmap进行交换,其他数据不变。
当然为了不让他滑动的生硬,加个过度动画,此时保存的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()
}
复制代码
判断完成
比如最后移动成了这个样子,此时在向左滑动即可拼图完成,那么该如何判断呢?
这时候二维数组保存的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;
}
复制代码
完整代码
当然还有些细节没提到,可在以下代码啊中查看,比如在长按后,显示原图,
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))
}
}
复制代码