android自定义游戏闯关图,Android开发之儿时的回忆——拼图小游戏

会写这篇文章完全是由于巧合,前几天路过天桥下的路边摊发现一个很熟悉的“老朋友”,想必大家小时候也玩过这种滑块拼图吧。

817199138267

儿时的印象——滑块拼图

哈哈,暴露年龄的东西,刚开始觉得很惊喜,没想到这么多年过去了,它依旧健在,或许还有其它方式可以让它存留的更久一些,所以萌发了想写这个滑块拼图的小游戏的念头,花了2个晚上的时间把它实现了,来看一下实现的效果图:

817199138267

拼图小游戏

817199138267

拼图小游戏动图

抛砖引玉:

这是一个简单的小Demo,还可以有更多的扩展,比如我们可以动态的从手机相册中选取图片作为拼图底图,可以动态的设置拼图难易度(滑块个数)等等,看完这篇文章,请大家尽情发挥想象力吧~

实现思路:

简单的过一下思路,首先我们需要一张图作为拼图背景,然后根据一定的比例把它分成n个拼图滑块并随机打乱位置,指定其中一个滑块为空白块,当用户点击这个空白块相邻(上下左右)的拼图滑块时,交换它们位置,每次交换位置后去判断是否完成了拼图,大概思路是这样子,下面我们来看代码实现。

拼图滑块实体类:

package jigsaw.lcw.com.jigsaw;

import android.graphics.Bitmap;

/**

* 拼图实体类

* Create by: chenWei.li

* Date: 2018/1/2

* Time: 下午10:10

* Email: lichenwei.me@foxmail.com

*/

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坐标,而是指所在的行和列):

817199138267

滑块可移动区域

/**

* 判断当前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);

}

}

好了,到这里文章就结束了,很简单的一个小游戏,很美好的一份童年回忆~

源码下载:

这里附上源码地址(欢迎Star,欢迎Fork):拼图小游戏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值