會寫這篇文章完全是由于巧合,前幾天路過天橋下的路邊攤發(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坐標泼返,而是指所在的行和列):
/**
* 判斷當前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):拼圖小游戲