Android實現(xiàn)拼圖小游戲功能
效果圖:

拋磚引玉:
這是一個簡單的小Demo,還可以有更多的擴展,比如我們可以動態(tài)的從手機相冊中選取圖片作為拼圖底圖,可以動態(tài)的設(shè)置拼圖難易度(滑塊個數(shù))等等,看完這篇文章,請大家盡情發(fā)揮想象力吧~
實現(xiàn)思路:
簡單的過一下思路,首先我們需要一張圖作為拼圖背景,然后根據(jù)一定的比例把它分成n個拼圖滑塊并隨機打亂位置,指定其中一個滑塊為空白塊,當(dāng)用戶點擊這個空白塊相鄰(上下左右)的拼圖滑塊時,交換它們位置,每次交換位置后去判斷是否完成了拼圖,大概思路是這樣子,下面我們來看代碼實現(xià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),當(dāng)前顯示的圖像(bitmap),當(dāng)前的位置點(currentX、currentY),我們在移動滑塊的時候,需要不斷的去交換顯示的圖像和當(dāng)前位置點,而原始位置點是用來判斷游戲是否結(jié)束的一個標(biāo)志,當(dāng)所有的原始位置點與所有的當(dāng)前位置點相等時,就代表游戲結(jié)束。
拼圖底圖的實現(xiàn):
既然要拼圖,那肯定需要有圖片了,有些朋友可能會想是不是需要準(zhǔn)備n張小圖片?其實是不用的,如果都這樣去準(zhǔn)備的話,要做一個拼圖闖關(guān)的游戲得預(yù)置多少圖片資源啊,包體積還不直接上天了,這里我們采用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設(shè)置一個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;//切割原圖為拼圖碎片裝入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);}}});//綁定數(shù)據(jù)imageView.setTag(new Jigsaw(i, j, bitmap));//添加到拼圖布局mImageViewArray[i][j] = imageView;mGridLayout.addView(imageView);}}//設(shè)置拼圖空碎片ImageView imageView = (ImageView) mGridLayout.getChildAt(mGridLayout.getChildCount() - 1);imageView.setImageBitmap(null);????????mEmptyImageView?=?imageView;}
拼圖滑塊的移動事件:
上面代碼我們?yōu)镮mageView設(shè)置了點擊事件,這邊就是用來判斷當(dāng)前點擊的ImageView是否是可以移動的,判斷的依據(jù):當(dāng)前點擊ImageView是否在空白塊相鄰(上下左右)的位置,而這個位置信息可以通過ImageView里的Tag得到,參考圖如下(這里的R,C不是指XY坐標(biāo),而是指所在的行和列):

/*** 判斷當(dāng)前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)造出對應(yīng)的移動的動畫
2、動畫結(jié)束后,需要處理對應(yīng)的數(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() {@Overridepublic void onAnimationStart(Animation animation) {isAnimated = true;}@Overridepublic 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();}}@Overridepublic void onAnimationRepeat(Animation animation) {}});imageView.startAnimation(translateAnimation);}}}
這里我們重點看一下數(shù)據(jù)的交換,我們都知道Android補間動畫只是給我們視覺上的改變,本質(zhì)上View的位置是沒有移動的,我們先通過setFillAfter讓其做完動畫保持在原處(視覺效果),在動畫執(zhí)行完畢的時候,我們進行ImageView數(shù)據(jù)的交換,這邊要特別注意的是,其實我們并沒有去交換View的位置,本質(zhì)上我們只是交換了Bitmap讓ImageView更改顯示和currentX、currentY的值,原來的View在哪,它還是在哪,當(dāng)數(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é)束:
我們之前在拼圖滑塊實體類中預(yù)置了這幾個屬性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ù)坐標(biāo)的起始點判斷具體方向,有了對應(yīng)的移動方向,我們就可以來處理拼圖滑塊的移動了,這次是逆向思維,根據(jù)手勢方向判斷空白塊相鄰(上下左右)有沒有拼圖塊,如果有,把對應(yīng)的滑塊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é)束了,很簡單的一個小游戲,很美好的一份童年回憶~
源碼地址:
https://github.com/Lichenwei-Dev/JigsawView
