Android開發(fā)之兒時的回憶——拼圖小游戲

會寫這篇文章完全是由于巧合,前幾天路過天橋下的路邊攤發(fā)現(xiàn)一個很熟悉的“老朋友”雾狈,想必大家小時候也玩過這種滑塊拼圖吧廓潜。

兒時的印象——滑塊拼圖

哈哈,暴露年齡的東西箍邮,剛開始覺得很驚喜茉帅,沒想到這么多年過去了,它依舊健在锭弊,或許還有其它方式可以讓它存留的更久一些,所以萌發(fā)了想寫這個滑塊拼圖的小游戲的念頭擂错,花了2個晚上的時間把它實現(xiàn)了味滞,來看一下實現(xiàn)的效果圖:

拼圖小游戲
拼圖小游戲動圖

拋磚引玉:

這是一個簡單的小Demo,還可以有更多的擴展钮呀,比如我們可以動態(tài)的從手機相冊中選取圖片作為拼圖底圖剑鞍,可以動態(tài)的設置拼圖難易度(滑塊個數(shù))等等,看完這篇文章爽醋,請大家盡情發(fā)揮想象力吧~

實現(xiàn)思路:

簡單的過一下思路蚁署,首先我們需要一張圖作為拼圖背景,然后根據(jù)一定的比例把它分成n個拼圖滑塊并隨機打亂位置蚂四,指定其中一個滑塊為空白塊光戈,當用戶點擊這個空白塊相鄰(上下左右)的拼圖滑塊時,交換它們位置遂赠,每次交換位置后去判斷是否完成了拼圖久妆,大概思路是這樣子,下面我們來看代碼實現(xià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)邻吞,我們在移動滑塊的時候蜀踏,需要不斷的去交換顯示的圖像和當前位置點埂伦,而原始位置點是用來判斷游戲是否結(jié)束的一個標志丘跌,當所有的原始位置點與所有的當前位置點相等時眶掌,就代表游戲結(jié)束健盒。

拼圖底圖的實現(xiàn):

既然要拼圖绒瘦,那肯定需要有圖片了,有些朋友可能會想是不是需要準備n張小圖片扣癣?其實是不用的惰帽,如果都這樣去準備的話,要做一個拼圖闖關(guān)的游戲得預置多少圖片資源啊父虑,包體積還不直接上天了该酗,這里我們采用GridLayout來做,將一張圖片動態(tài)切割成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);
    }

其實它的原理就是根據(jù)我們傳入的壓縮寬高值括细,通過矩陣Matrix對圖片進行縮放伪很。

再來就是切割小塊拼圖滑塊了,我們把圖片分成3行5列匾七,根據(jù)算出的寬高去創(chuàng)建3*5個小的Bitmap并裝載入ImageView,加入到GridLayout布局中改基,然后為每個ImageView設置一個Tag,這個Tag的信息就是我們之前創(chuàng)建的實體類數(shù)據(jù)饰恕,并制定最后一個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);
                        }
                    }
                });
                //綁定數(shù)據(jù)
                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;

    }

拼圖滑塊的移動事件:

上面代碼我們?yōu)镮mageView設置了點擊事件踊餐,這邊就是用來判斷當前點擊的ImageView是否是可以移動的,判斷的依據(jù):當前點擊ImageView是否在空白塊相鄰(上下左右)的位置失乾,而這個位置信息可以通過ImageView里的Tag得到,參考圖如下(這里的R,C不是指XY坐標泼返,而是指所在的行和列):


滑塊可移動區(qū)域
    /**
     * 判斷當前view是否在可移動范圍內(nèi)(在空白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、根據(jù)點擊ImageView位置去構(gòu)造出對應的移動的動畫
2吧恃、動畫結(jié)束后虾啦,需要處理對應的數(shù)據(jù)交換
3、動畫結(jié)束后痕寓,需要去判斷是否完成了拼圖(下文會提傲醉,這里先不管)

   /**
     * 處理點擊拼圖的移動事件
     *
     * @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();
                        //交換拼圖數(shù)據(jù)
                        changeJigsawData(imageView);
                        //判斷游戲是否結(jié)束
                        boolean isFinish = JigsawHelper.getInstance().isFinishGame(mImageViewArray, mEmptyImageView);
                        if (isFinish) {
                            Toast.makeText(MainActivity.this, "拼圖成功,游戲結(jié)束呻率!", Toast.LENGTH_LONG).show();
                        }
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {

                    }
                });

                imageView.startAnimation(translateAnimation);
            }
        }
    }

這里我們重點看一下數(shù)據(jù)的交換硬毕,我們都知道Android補間動畫只是給我們視覺上的改變,本質(zhì)上View的位置是沒有移動的礼仗,我們先通過setFillAfter讓其做完動畫保持在原處(視覺效果)吐咳,在動畫執(zhí)行完畢的時候,我們進行ImageView數(shù)據(jù)的交換元践,這邊要特別注意的是韭脊,其實我們并沒有去交換View的位置,本質(zhì)上我們只是交換了Bitmap讓ImageView更改顯示和currentX单旁、currentY的值沪羔,原來的View在哪,它還是在哪象浑,當數(shù)據(jù)交換完成后蔫饰,記得更改空白塊的引用。

   /**
     * 交換拼圖數(shù)據(jù)
     *
     * @param imageView
     */
    public void changeJigsawData(ImageView imageView) {
        Jigsaw emptyJigsaw = (Jigsaw) mEmptyImageView.getTag();
        Jigsaw jigsaw = (Jigsaw) imageView.getTag();

        //更新imageView的顯示內(nèi)容
        mEmptyImageView.setImageBitmap(jigsaw.getBitmap());
        imageView.setImageBitmap(null);
        //交換數(shù)據(jù)
        emptyJigsaw.setCurrentX(jigsaw.getCurrentX());
        emptyJigsaw.setCurrentY(jigsaw.getCurrentY());
        emptyJigsaw.setBitmap(jigsaw.getBitmap());

        //更新空拼圖引用
        mEmptyImageView = imageView;
    }

判斷游戲結(jié)束:

我們之前在拼圖滑塊實體類中預置了這幾個屬性originalX愉豺、originalY(代表最開始的位置)篓吁,currentX、currentY(經(jīng)過一系列移動后的位置)蚪拦,因為滑塊的移動只是視覺效果越除,本質(zhì)上是沒有改變View位置的,只是交換了數(shù)據(jù)外盯,所以我們最后可以根據(jù)originalX、currentX和originalY翼雀、currentY是否相等來判斷(空白塊除外):

   /**
     * 判斷游戲是否結(jié)束
     *
     * @param imageViewArray
     * @return
     */
    public boolean isFinishGame(ImageView[][] imageViewArray, ImageView emptyImageView) {

        int rightNum = 0;//記錄匹配拼圖數(shù)

        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;
    }

手勢交互:

剛才我們已經(jīng)實現(xiàn)了點擊的交互事件饱苟,可以更炫酷點,我們把手勢交互也補上狼渊,用手指的滑動來帶動拼圖滑塊的移動箱熬,我們來看下核心代碼:

    /**
     * 判斷手指移動的方向类垦,
     *
     * @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();
        //根據(jù)滑動距離判斷是橫向滑動還是縱向滑動
        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;
    }

首先我們根據(jù)手指的移動距離先判斷是左右滑動還是上下滑動,然后再根據(jù)坐標的起始點判斷具體方向城须,有了對應的移動方向,我們就可以來處理拼圖滑塊的移動了糕伐,這次是逆向思維砰琢,根據(jù)手勢方向判斷空白塊相鄰(上下左右)有沒有拼圖塊,如果有良瞧,把對應的滑塊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);
        }
    }

游戲的初始化:

關(guān)于游戲的初始化,其實很簡單褥蚯,我們可以構(gòu)造給隨機次數(shù)挚冤,讓游戲開始的時候隨機方向,隨機次數(shù)的滑動即可:

   /**
     * 游戲初始化赞庶,隨機打亂順序
     */
    private void randomJigsaw() {
        for (int i = 0; i < 100; i++) {
            int gestureDirection = (int) ((Math.random() * 4) + 1);
            handleFlingGesture(gestureDirection, false);
        }
    }

好了训挡,到這里文章就結(jié)束了,很簡單的一個小游戲歧强,很美好的一份童年回憶~

源碼下載:

這里附上源碼地址(歡迎Star澜薄,歡迎Fork):拼圖小游戲

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市誊锭,隨后出現(xiàn)的幾起案子表悬,更是在濱河造成了極大的恐慌,老刑警劉巖丧靡,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蟆沫,死亡現(xiàn)場離奇詭異,居然都是意外死亡温治,警方通過查閱死者的電腦和手機饭庞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熬荆,“玉大人舟山,你說我怎么就攤上這事÷笨遥” “怎么了累盗?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長突琳。 經(jīng)常有香客問我若债,道長,這世上最難降的妖魔是什么拆融? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任蠢琳,我火速辦了婚禮啊终,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘傲须。我一直安慰自己蓝牲,他們只是感情好,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布泰讽。 她就那樣靜靜地躺著例衍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪菇绵。 梳的紋絲不亂的頭發(fā)上肄渗,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音咬最,去河邊找鬼翎嫡。 笑死,一個胖子當著我的面吹牛永乌,可吹牛的內(nèi)容都是我干的惑申。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼翅雏,長吁一口氣:“原來是場噩夢啊……” “哼圈驼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起望几,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤绩脆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后橄抹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體靴迫,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年楼誓,在試婚紗的時候發(fā)現(xiàn)自己被綠了玉锌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡疟羹,死狀恐怖主守,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情榄融,我是刑警寧澤参淫,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站愧杯,受9級特大地震影響黄刚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜民效,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一憔维、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧畏邢,春花似錦业扒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至臂寝,卻和暖如春章鲤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咆贬。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工败徊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掏缎。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓皱蹦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親眷蜈。 傳聞我的和親對象是個殘疾皇子沪哺,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容