Android开发之儿时的回忆——拼图小游戏

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

儿时的印象——滑块拼图

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

拼图小游戏
拼图小游戏动图

抛砖引玉:

这是一个简单的小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坐标,而是指所在的行和列):


滑块可移动区域
    /**
     * 判断当前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):拼图小游戏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容