Android实现拼图小游戏功能
效果图:
抛砖引玉:
这是一个简单的小Demo,还可以有更多的扩展,比如我们可以动态的从手机相册中选取图片作为拼图底图,可以动态的设置拼图难易度(滑块个数)等等,看完这篇文章,请大家尽情发挥想象力吧~
实现思路:
简单的过一下思路,首先我们需要一张图作为拼图背景,然后根据一定的比例把它分成n个拼图滑块并随机打乱位置,指定其中一个滑块为空白块,当用户点击这个空白块相邻(上下左右)的拼图滑块时,交换它们位置,每次交换位置后去判断是否完成了拼图,大概思路是这样子,下面我们来看代码实现。
拼图滑块实体类:
package jigsaw.lcw.com.jigsaw;
import android.graphics.Bitmap;
/**
* 拼图实体类
*/
public class Jigsaw {
private int originalX;
private int originalY;
private Bitmap bitmap;
private int currentX;
private int currentY;
public Jigsaw(int originalX, int originalY, Bitmap bitmap) {
this.originalX = originalX;
this.originalY = originalY;
this.bitmap = bitmap;
this.currentX = originalX;
this.currentY = originalY;
}
public int getOriginalX() {
return originalX;
}
public void setOriginalX(int originalX) {
this.originalX = originalX;
}
public int getOriginalY() {
return originalY;
}
public void setOriginalY(int originalY) {
this.originalY = originalY;
}
public Bitmap getBitmap() {
return bitmap;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
public int getCurrentX() {
return currentX;
}
public void setCurrentX(int currentX) {
this.currentX = currentX;
}
public int getCurrentY() {
return currentY;
}
public void setCurrentY(int currentY) {
this.currentY = currentY;
}
@Override
public String toString() {
return "Jigsaw{" +
"originalX=" + originalX +
", originalY=" + originalY +
", currentX=" + currentX +
", currentY=" + currentY +
'}';
}
}
首先我们需要一个滑块的实体类,这个类用来记录拼图滑块的原始位置点(originalX、originalY),当前显示的图像(bitmap),当前的位置点(currentX、currentY),我们在移动滑块的时候,需要不断的去交换显示的图像和当前位置点,而原始位置点是用来判断游戏是否结束的一个标志,当所有的原始位置点与所有的当前位置点相等时,就代表游戏结束。
拼图底图的实现:
既然要拼图,那肯定需要有图片了,有些朋友可能会想是不是需要准备n张小图片?其实是不用的,如果都这样去准备的话,要做一个拼图闯关的游戏得预置多少图片资源啊,包体积还不直接上天了,这里我们采用GridLayout来做,将一张图片动态切割成n个小图填充至ImageView,然后加入到GridLayout布局中。
/**
* 获取拼图(大图)
*
* @return
*/
public Bitmap getJigsaw(Context context) {
//加载Bitmap原图,并获取宽高
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.img);
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
//按屏幕宽铺满显示,算出缩放比例
int screenWidth = getScreenWidth(context);
float scale = 1.0f;
if (screenWidth < bitmapWidth) {
scale = screenWidth * 1.0f / bitmapWidth;
}
bitmap = Bitmap.createScaledBitmap(bitmap, screenWidth, (int) (bitmapHeight * scale), false);
return bitmap;
}
首先我们需要对资源图片进行一定比例的压缩,我们让图片充满屏幕宽度,算出一定的缩放比例,然后压缩图片的高,这里有个createScaledBitmap方法,我们来看下底层源码:
/**
* Creates a new bitmap, scaled from an existing bitmap, when possible. If the
* specified width and height are the same as the current width and height of
* the source bitmap, the source bitmap is returned and no new bitmap is
* created.
*
* @param src The source bitmap.
* @param dstWidth The new bitmap's desired width.
* @param dstHeight The new bitmap's desired height.
* @param filter true if the source should be filtered.
* @return The new scaled bitmap or the source bitmap if no scaling is required.
* @throws IllegalArgumentException if width is <= 0, or height is <= 0
*/
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
boolean filter) {
Matrix m = new Matrix();
final int width = src.getWidth();
final int height = src.getHeight();
if (width != dstWidth || height != dstHeight) {
final float sx = dstWidth / (float) width;
final float sy = dstHeight / (float) height;
m.setScale(sx, sy);
}
return Bitmap.createBitmap(src, 0, 0, width, height, m, filter);
}
其实它的原理就是根据我们传入的压缩宽高值,通过矩阵Matrix对图片进行缩放。
再来就是切割小块拼图滑块了,我们把图片分成3行5列,根据算出的宽高去创建3*5个小的Bitmap并装载入ImageView,加入到GridLayout布局中,然后为每个ImageView设置一个Tag,这个Tag的信息就是我们之前创建的实体类数据,并制定最后一个ImageView为空白块。
/**
* 初始化拼图碎片
* @param jigsawBitmap
*/
private void initJigsaw(Bitmap jigsawBitmap) {
mGridLayout = findViewById(R.id.gl_layout);
int itemWidth = jigsawBitmap.getWidth() / 5;
int itemHeight = jigsawBitmap.getHeight() / 3;
//切割原图为拼图碎片装入GridLayout
for (int i = 0; i < mJigsawArray.length; i++) {
for (int j = 0; j < mJigsawArray[0].length; j++) {
Bitmap bitmap = Bitmap.createBitmap(jigsawBitmap, j * itemWidth, i * itemHeight, itemWidth, itemHeight);
ImageView imageView = new ImageView(this);
imageView.setImageBitmap(bitmap);
imageView.setPadding(2, 2, 2, 2);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//判断是否可移动
boolean isNearBy = JigsawHelper.getInstance().isNearByEmptyView((ImageView) v, mEmptyImageView);
if (isNearBy) {
//处理移动
handleClickItem((ImageView) v, true);
}
}
});
//绑定数据
imageView.setTag(new Jigsaw(i, j, bitmap));
//添加到拼图布局
mImageViewArray[i][j] = imageView;
mGridLayout.addView(imageView);
}
}
//设置拼图空碎片
ImageView imageView = (ImageView) mGridLayout.getChildAt(mGridLayout.getChildCount() - 1);
imageView.setImageBitmap(null);
mEmptyImageView = imageView;
}
拼图滑块的移动事件:
上面代码我们为ImageView设置了点击事件,这边就是用来判断当前点击的ImageView是否是可以移动的,判断的依据:当前点击ImageView是否在空白块相邻(上下左右)的位置,而这个位置信息可以通过ImageView里的Tag得到,参考图如下(这里的R,C不是指XY坐标,而是指所在的行和列):
/**
* 判断当前view是否在可移动范围内(在空白View的上下左右)
*
* @param imageView
* @param emptyImageView
* @return
*/
public boolean isNearByEmptyView(ImageView imageView, ImageView emptyImageView) {
Jigsaw emptyJigsaw = (Jigsaw) imageView.getTag();
Jigsaw jigsaw = (Jigsaw) emptyImageView.getTag();
if (emptyJigsaw != null && jigsaw != null) {
//点击拼图在空拼图的左边
if (jigsaw.getOriginalX() == emptyJigsaw.getOriginalX() && jigsaw.getOriginalY() + 1 == emptyJigsaw.getOriginalY()) {
return true;
}
//点击拼图在空拼图的右边
if (jigsaw.getOriginalX() == emptyJigsaw.getOriginalX() && jigsaw.getOriginalY() - 1 == emptyJigsaw.getOriginalY()) {
return true;
}
//点击拼图在空拼图的上边
if (jigsaw.getOriginalY() == emptyJigsaw.getOriginalY() && jigsaw.getOriginalX() + 1 == emptyJigsaw.getOriginalX()) {
return true;
}
//点击拼图在空拼图的下边
if (jigsaw.getOriginalY() == emptyJigsaw.getOriginalY() && jigsaw.getOriginalX() - 1 == emptyJigsaw.getOriginalX()) {
return true;
}
}
return false;
}
然后我们看一下移动拼图滑块的代码,这里其实做了这么几件事情:
1、根据点击ImageView位置去构造出对应的移动的动画
2、动画结束后,需要处理对应的数据交换
3、动画结束后,需要去判断是否完成了拼图(下文会提,这里先不管)
/**
* 处理点击拼图的移动事件
*
* @param imageView
*/
private void handleClickItem(final ImageView imageView) {
if (!isAnimated) {
TranslateAnimation translateAnimation = null;
if (imageView.getX() < mEmptyImageView.getX()) {
//左往右
translateAnimation = new TranslateAnimation(0, imageView.getWidth(), 0, 0);
}
if (imageView.getX() > mEmptyImageView.getX()) {
//右往左
translateAnimation = new TranslateAnimation(0, -imageView.getWidth(), 0, 0);
}
if (imageView.getY() > mEmptyImageView.getY()) {
//下往上
translateAnimation = new TranslateAnimation(0, 0, 0, -imageView.getHeight());
}
if (imageView.getY() < mEmptyImageView.getY()) {
//上往下
translateAnimation = new TranslateAnimation(0, 0, 0, imageView.getHeight());
}
if (translateAnimation != null) {
translateAnimation.setDuration(80);
translateAnimation.setFillAfter(true);
translateAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
isAnimated = true;
}
@Override
public void onAnimationEnd(Animation animation) {
//清除动画
isAnimated = false;
imageView.clearAnimation();
//交换拼图数据
changeJigsawData(imageView);
//判断游戏是否结束
boolean isFinish = JigsawHelper.getInstance().isFinishGame(mImageViewArray, mEmptyImageView);
if (isFinish) {
Toast.makeText(MainActivity.this, "拼图成功,游戏结束!", Toast.LENGTH_LONG).show();
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
imageView.startAnimation(translateAnimation);
}
}
}
这里我们重点看一下数据的交换,我们都知道Android补间动画只是给我们视觉上的改变,本质上View的位置是没有移动的,我们先通过setFillAfter让其做完动画保持在原处(视觉效果),在动画执行完毕的时候,我们进行ImageView数据的交换,这边要特别注意的是,其实我们并没有去交换View的位置,本质上我们只是交换了Bitmap让ImageView更改显示和currentX、currentY的值,原来的View在哪,它还是在哪,当数据交换完成后,记得更改空白块的引用。
/**
* 交换拼图数据
*
* @param imageView
*/
public void changeJigsawData(ImageView imageView) {
Jigsaw emptyJigsaw = (Jigsaw) mEmptyImageView.getTag();
Jigsaw jigsaw = (Jigsaw) imageView.getTag();
//更新imageView的显示内容
mEmptyImageView.setImageBitmap(jigsaw.getBitmap());
imageView.setImageBitmap(null);
//交换数据
emptyJigsaw.setCurrentX(jigsaw.getCurrentX());
emptyJigsaw.setCurrentY(jigsaw.getCurrentY());
emptyJigsaw.setBitmap(jigsaw.getBitmap());
//更新空拼图引用
mEmptyImageView = imageView;
}
判断游戏结束:
我们之前在拼图滑块实体类中预置了这几个属性originalX、originalY(代表最开始的位置),currentX、currentY(经过一系列移动后的位置),因为滑块的移动只是视觉效果,本质上是没有改变View位置的,只是交换了数据,所以我们最后可以根据originalX、currentX和originalY、currentY是否相等来判断(空白块除外):
/**
* 判断游戏是否结束
*
* @param imageViewArray
* @return
*/
public boolean isFinishGame(ImageView[][] imageViewArray, ImageView emptyImageView) {
int rightNum = 0;//记录匹配拼图数
for (int i = 0; i < imageViewArray.length; i++) {
for (int j = 0; j < imageViewArray[0].length; j++) {
if (imageViewArray[i][j] != emptyImageView) {
Jigsaw jigsaw = (Jigsaw) imageViewArray[i][j].getTag();
if (jigsaw != null) {
if (jigsaw.getOriginalX() == jigsaw.getCurrentX() && jigsaw.getOriginalY() == jigsaw.getCurrentY()) {
rightNum++;
}
}
}
}
}
if (rightNum == (imageViewArray.length * imageViewArray[0].length) - 1) {
return true;
}
return false;
}
手势交互:
刚才我们已经实现了点击的交互事件,可以更炫酷点,我们把手势交互也补上,用手指的滑动来带动拼图滑块的移动,我们来看下核心代码:
/**
* 判断手指移动的方向,
*
* @param startEvent
* @param endEvent
* @return
*/
public int getGestureDirection(MotionEvent startEvent, MotionEvent endEvent) {
float startX = startEvent.getX();
float startY = startEvent.getY();
float endX = endEvent.getX();
float endY = endEvent.getY();
//根据滑动距离判断是横向滑动还是纵向滑动
int gestureDirection = Math.abs(startX - endX) > Math.abs(startY - endY) ? LEFT_OR_RIGHT : UP_OR_DOWN;
//具体判断滑动方向
switch (gestureDirection) {
case LEFT_OR_RIGHT:
if (startEvent.getX() < endEvent.getX()) {
//手指向右移动
return RIGHT;
} else {
//手指向左移动
return LEFT;
}
case UP_OR_DOWN:
if (startEvent.getY() < endEvent.getY()) {
//手指向下移动
return DOWN;
} else {
//手指向上移动
return UP;
}
}
return NONE;
}
首先我们根据手指的移动距离先判断是左右滑动还是上下滑动,然后再根据坐标的起始点判断具体方向,有了对应的移动方向,我们就可以来处理拼图滑块的移动了,这次是逆向思维,根据手势方向判断空白块相邻(上下左右)有没有拼图块,如果有,把对应的滑块ImageView取出,交给上文提到的点击滑块移动代码处理:
/**
* 处理手势移动拼图
*
* @param gestureDirection
* @param animation 是否带有动画
*/
private void handleFlingGesture(int gestureDirection, boolean animation) {
ImageView imageView = null;
Jigsaw emptyJigsaw = (Jigsaw) mEmptyImageView.getTag();
switch (gestureDirection) {
case GestureHelper.LEFT:
if (emptyJigsaw.getOriginalY() + 1 <= mGridLayout.getColumnCount() - 1) {
imageView = mImageViewArray[emptyJigsaw.getOriginalX()][emptyJigsaw.getOriginalY() + 1];
}
break;
case GestureHelper.RIGHT:
if (emptyJigsaw.getOriginalY() - 1 >= 0) {
imageView = mImageViewArray[emptyJigsaw.getOriginalX()][emptyJigsaw.getOriginalY() - 1];
}
break;
case GestureHelper.UP:
if (emptyJigsaw.getOriginalX() + 1 <= mGridLayout.getRowCount() - 1) {
imageView = mImageViewArray[emptyJigsaw.getOriginalX() + 1][emptyJigsaw.getOriginalY()];
}
break;
case GestureHelper.DOWN:
if (emptyJigsaw.getOriginalX() - 1 >= 0) {
imageView = mImageViewArray[emptyJigsaw.getOriginalX() - 1][emptyJigsaw.getOriginalY()];
}
break;
default:
break;
}
if (imageView != null) {
handleClickItem(imageView, animation);
}
}
游戏的初始化:
关于游戏的初始化,其实很简单,我们可以构造给随机次数,让游戏开始的时候随机方向,随机次数的滑动即可:
/**
* 游戏初始化,随机打乱顺序
*/
private void randomJigsaw() {
for (int i = 0; i < 100; i++) {
int gestureDirection = (int) ((Math.random() * 4) + 1);
handleFlingGesture(gestureDirection, false);
}
}
好了,到这里文章就结束了,很简单的一个小游戏,很美好的一份童年回忆~
源码地址:
https://github.com/Lichenwei-Dev/JigsawView