Android实现拼图小游戏功能

龙旋

共 9578字,需浏览 20分钟

 · 2022-03-18

效果图:



抛砖引玉:


这是一个简单的小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

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报