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;}@Overridepublic 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;//切割原图为拼图碎片装入GridLayoutfor (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() {@Overridepublic 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() {@Overridepublic void onAnimationStart(Animation animation) {isAnimated = true;}@Overridepublic 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();}}@Overridepublic 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
