2016年9月8日 星期四

Android 实战美女拼图游戏 你能坚持到第几关

Ref :  http://blog.csdn.net/lmj623565791/article/details/40595385


1、概述

继2048之后,今天给大家带来一个拼图游戏,当然了不是很传统那个缺一块的拼图,那游戏我不会玩~~所有我们换个方式玩拼图,怎么玩呢,把图片且成很多份,点击交换拼成一张完整的;这样关卡也很容易设计,3*3;4*4;5*5;6*6;...一直下去....
博客产生的原因是,没事在网上逛看到一个图片切片的辅助类,类很简单,上面说了一句,如果做拼图游戏的话可以用到,然后,我们就用到了~~
至于效果是这样的:


加了个切换动画,效果还是不错的~~其实游戏就是自定义了一个控件,下面我们开始自定义之旅~~

2、游戏的设计

首先我们分析下如何设计这款游戏:
1、我们需要一个容器,可以放这些图片的块块,为了方便,我们准备使用RelativeLayout配合addRule实现
2、每个图片的块块,我们准备使用ImageView
3、点击交换,我们准备使用传统的TranslationAnimation来实现
有了初步的设计,感觉这游戏so easy~

3、游戏布局的实现

首先,我们准备实现能够把一张图片,切成n*n份,放在指定的位置;
我们只需要设置n这个数字,然后根据布局的宽或者高其中的小值,除以n,减去一些边距就可以得到我们ImageView的宽和高了~~

构造方法:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * 设置Item的数量n*n;默认为3 
  3.      */  
  4.     private int mColumn = 3;  
  5.     /** 
  6.      * 布局的宽度 
  7.      */  
  8.     private int mWidth;  
  9.     /** 
  10.      * 布局的padding 
  11.      */  
  12.     private int mPadding;  
  13.     /** 
  14.      * 存放所有的Item 
  15.      */  
  16.     private ImageView[] mGamePintuItems;  
  17.     /** 
  18.      * Item的宽度 
  19.      */  
  20.     private int mItemWidth;  
  21.   
  22.     /** 
  23.      * Item横向与纵向的边距 
  24.      */  
  25.     private int mMargin = 3;  
  26.       
  27.     /** 
  28.      * 拼图的图片 
  29.      */  
  30.     private Bitmap mBitmap;  
  31.     /** 
  32.      * 存放切完以后的图片bean 
  33.      */  
  34.     private List<ImagePiece> mItemBitmaps;  
  35.       
  36.     private boolean once;  
  37.       
  38.     public GamePintuLayout(Context context)  
  39.     {  
  40.         this(context, null);  
  41.     }  
  42.   
  43.     public GamePintuLayout(Context context, AttributeSet attrs)  
  44.     {  
  45.         this(context, attrs, 0);  
  46.     }  
  47.   
  48.     public GamePintuLayout(Context context, AttributeSet attrs, int defStyle)  
  49.     {  
  50.         super(context, attrs, defStyle);  
  51.   
  52.         mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,  
  53.                 mMargin, getResources().getDisplayMetrics());  
  54.         // 设置Layout的内边距,四边一致,设置为四内边距中的最小值  
  55.         mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(),  
  56.                 getPaddingBottom());  
  57.     }  

构造方法里面,我们得到把设置的
margin值转化为dp;获得布局的padding值;整体是个正方形,所以我们取padding四个方向中的最小值
至于margin,作为Item之间的横向与纵向的间距,你喜欢的话可以抽取为自定义属性~~

2、onMeasure

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Override  
  2.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
  3.     {  
  4.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  5.       
  6.         // 获得游戏布局的边长  
  7.         mWidth = Math.min(getMeasuredHeight(), getMeasuredWidth());  
  8.   
  9.         if (!once)  
  10.         {  
  11.             initBitmap();  
  12.             initItem();  
  13.         }  
  14.         once = true;  
  15.         setMeasuredDimension(mWidth, mWidth);  
  16.     }  

onMeasure 
里面主要就是获得到布局的宽度,然后进行图片的准备,以及初始化我们的Item,为Item设置宽度和高度
initBitmap自然就是准备图片了
  1. private void initBitmap()  
  2.     {  
  3.         if (mBitmap == null)  
  4.             mBitmap = BitmapFactory.decodeResource(getResources(),  
  5.                     R.drawable.aa);  
  6.   
  7.         //  split
  8.         mItemBitmaps = ImageSplitter.split(mBitmap, mColumn);  
  9.   
  10.         Collections.sort(mItemBitmaps, new Comparator<ImagePiece>()  
  11.         {  
  12.             @Override  
  13.             public int compare(ImagePiece lhs, ImagePiece rhs)  
  14.             {  
  15.                 return Math.random() > 0.5 ? 1 : -1;  
  16.             }  
  17.         });  
  18.     }  



我们这里如果没有设置mBitmap就准备一张备用图片,然后调用ImageSplitter.split将图片切成n * n 返回一个List<ImagePiece>
切完以后,我们需要将顺序打乱,所以我们调用了sort方法,至于比较器,我们使用random随机比较大小~~这样我们就完成了我们的乱序操作,赞不赞~~

  1. public class ImageSplitter  
  2. {  
  3.     /** 
  4.      * 将图片切成 , piece *piece 
  5.      *  
  6.      * @param bitmap 
  7.      * @param piece 
  8.      * @return 
  9.      */  
  10.     public static List<ImagePiece> split(Bitmap bitmap, int piece)  
  11.     {  
  12.   
  13.         List<ImagePiece> pieces = new ArrayList<ImagePiece>(piece * piece);  
  14.   
  15.         int width = bitmap.getWidth();  
  16.         int height = bitmap.getHeight();  
  17.   
  18.         Log.e("TAG""bitmap Width = " + width + " , height = " + height);  
  19.         int pieceWidth = Math.min(width, height) / piece;  
  20.   
  21.         for (int i = 0; i < piece; i++)  
  22.         {  
  23.             for (int j = 0; j < piece; j++)  
  24.             {  
  25.                 ImagePiece imagePiece = new ImagePiece();  
  26.                 imagePiece.index = j + i * piece;  
  27.                 int xValue = j * pieceWidth;  
  28.                 int yValue = i * pieceWidth;  
  29.                   
  30.       //
  31.                 imagePiece.bitmap = Bitmap.createBitmap(bitmap, xValue, yValue,  
  32.                         pieceWidth, pieceWidth);  
  33.                 pieces.add(imagePiece);  
  34.             }  
  35.         }  
  36.         return pieces;  
  37.     }  
  38. }  

     重點就是   Bitmap.createBitmap  ....Returns an immutable bitmap from the specified subset of the source bitmap
    
static BitmapcreateBitmap(Bitmap source, int x, int y, int width, int height)Returns an immutable bitmap from the specified subset of the source bitmap.

  1. public class ImagePiece  
  2. {  
  3.     public int index = 0;  
  4.     public Bitmap bitmap = null;  
  5. }  

没撒说的就是一个根据宽度高度,和n,来切图保存的过程~~
ImagePiece保存的图片以及索引~~话说这两个类还是我无意中在网上发现的~~
图片到此就准备好了,现在看Item的生成已经设置宽高,即initItems

  1. private void initItem()  
  2.     {  
  3.         // 获得Item的宽度  
  4.         int childWidth = (mWidth - mPadding * 2 - mMargin * (mColumn - 1))  
  5.                 / mColumn;  
  6.         mItemWidth = childWidth;  
  7.   
  8.         mGamePintuItems = new ImageView[mColumn * mColumn];  
  9.         // 放置Item  
  10.         for (int i = 0; i < mGamePintuItems.length; i++)  
  11.         {  
  12.             ImageView item = new ImageView(getContext());  
  13.   
  14.             item.setOnClickListener(this);  
  15.   
  16.             item.setImageBitmap(mItemBitmaps.get(i).bitmap);  
  17.             mGamePintuItems[i] = item;  
  18.             item.setId(i + 1);  
  19.             item.setTag(i + "_" + mItemBitmaps.get(i).index);  
  20.   
  21.             RelativeLayout.LayoutParams lp = new LayoutParams(mItemWidth,  
  22.                     mItemWidth);  
  23.             // 设置横向边距,不是最后一列  
  24.             if ((i + 1) % mColumn != 0)  
  25.             {  
  26.                 lp.rightMargin = mMargin;  
  27.             }  
  28.             // 如果不是第一列  
  29.             if (i % mColumn != 0)  
  30.             {  
  31.                 lp.addRule(RelativeLayout.RIGHT_OF,//  
  32.                         mGamePintuItems[i - 1].getId());  
  33.             }  
  34.             // 如果不是第一行,//设置纵向边距,非最后一行  
  35.             if ((i + 1) > mColumn)  
  36.             {  
  37.                 lp.topMargin = mMargin;  
  38.                 lp.addRule(RelativeLayout.BELOW,//  
  39.                         mGamePintuItems[i - mColumn].getId());  
  40.             }  
  41.             addView(item, lp);  
  42.         }  
  43.   
  44.           
  45.     }  


可以看到我们的Item宽的计算:childWidth = (mWidth - mPadding * 2 - mMargin * (mColumn - 1) ) / mColumn;
容器的宽度,除去自己的内边距,除去Item间的间距,然后除以Item一行的个数就得到了Item的宽~~
接下来,就是遍历生成Item,根据他们的位置设置Rule,自己仔细看下注释~~
注意两点:
我们为Item设置了setOnClickListener,这个当然,因为我们的游戏就是点Item么~
还有我们为Item设置了Tag:item.setTag(i + "_" + mItemBitmaps.get(i).index);  
tag里面存放了index,也就是正确的位置;还有i,i 可以帮助我们在mItemBitmaps找到当前的Item的图片:(mItemBitmaps.get(i).bitmap)

到此,我们游戏的布局的代码就结束了~~~

然后我们在布局文件里面声明下:
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="fill_parent" >  
  5.   
  6.     <com.zhy.gamePintu.view.GamePintuLayout  
  7.         android:id="@+id/id_gameview"  
  8.         android:layout_width="fill_parent"  
  9.         android:layout_height="fill_parent"  
  10.         android:layout_centerInParent="true"  
  11.         android:padding="5dp" >  
  12.     </com.zhy.gamePintu.view.GamePintuLayout>  
  13.   
  14. </RelativeLayout>  

Activity里面记得设置这个布局~~
现在的效果是:


换了个风景图,感觉好清爽啊~~我们的切图,布局已经完成了,是不是很简单;而且感觉我们的游戏已经完成了一大半了呀~~~剩下无非就是Item的click的处理。

4、游戏的切换效果


1、初步的切换

还记得我们都给Item添加了onClick的监听么~~
现在我们需要实现,点击两个Item,他们的图片能够发生交换~
那么,我们需要两个成员变量来存储这两个Item,然后再去交换

  1. private ImageView mFirst;  
  2. private ImageView mSecond;  
  3.   
  4. @Override  
  5. public void onClick(View v)  
  6. {  
  7.     /** 
  8.      * 如果两次点击是同一个 
  9.      */  
  10.     if (mFirst == v)  
  11.     {  
  12.         mFirst.setColorFilter(null);  
  13.         mFirst = null;  
  14.         return;  
  15.     }  
  16.     //点击第一个Item  
  17.     if (mFirst == null)  
  18.     {  
  19.         mFirst = (ImageView) v;  
  20.          // 代表有點擊到
  21.         mFirst.setColorFilter(Color.parseColor("#55FF0000"));  
  22.     } else//点击第二个Item  
  23.     {  
  24.         mSecond = (ImageView) v;  
  25.         exchangeView();  
  26.     }  
  27.   
  28. }  

点击第一个,通过setColorFilter设置下选中效果,再次点击另一个,那我们就准备调用exchangeView进行交换图片了,当然这个方法我们还没写,先放着~
如果两次点击同一个,去除选中效果,我们就当什么都没发生。
接下来,我们来实现exchangeView
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * 交换两个Item的图片 
  3.      */  
  4.     private void exchangeView()  
  5.     {  
  6.                 mFirst.setColorFilter(null);  
  7.         String firstTag = (String) mFirst.getTag();  
  8.         String secondTag = (String) mSecond.getTag();  
  9.           
  10.         //得到在list中索引位置  
  11.         String[] firstImageIndex = firstTag.split("_");  
  12.         String[] secondImageIndex = secondTag.split("_");  
  13.           
  14.         mFirst.setImageBitmap(mItemBitmaps.get(Integer  
  15.                 .parseInt(secondImageIndex[0])).bitmap);  
  16.         mSecond.setImageBitmap(mItemBitmaps.get(Integer  
  17.                 .parseInt(firstImageIndex[0])).bitmap);  
  18.   
  19.         mFirst.setTag(secondTag);  
  20.         mSecond.setTag(firstTag);  
  21.           
  22.         mFirst = mSecond = null;  
  23.   
  24.     }  
应该还记得我们之前的setTag吧,忘了,返回去看看,我们还说注意来着~
通过getTag,拿到在List中是索引,然后得到bitmap进行交换设置,最后交换tag;
到此我们的交换效果写完了,我们的游戏可以完了~~效果是这样的:

可以看到我们已经可以玩了~~~至于为什么不用清爽的风景图,是因为,实在是看不出来那块对那块,还是妹子直观~
大家肯定会吐槽,我擦,动画切换呢,明明不是两个飞过去交换位置么,尼玛这算什么~~~
也是,对与程序我们要有追求,下面我们来添加动画切换效果~~

无缝的动画切换

我们先聊聊怎么添加,我准备使用TranslationAnimation,然后两个Item的top,left也很容器获取;
但是,要明白,我们实际上,Item只是setImage发生了变化,Item的位置没有变;
我们现在需要动画移动效果,比如A移动到B,没问题,移动完成以后,Item得回去吧,但是图片并没有发生变化,我们还是需要手动setImage
这样造成了一个现象,动画切换效果有了,但是最后还是会有一闪,是我们切换图片造成的;
为了避免上述现象,能够完美的做到切换效果,这里我们引入一个动画图层,专门做动画效果,有点类似ps的图层,下面看我们怎么做;

  1. /** 
  2.      * 动画运行的标志位 
  3.      */  
  4.     private boolean isAniming;  
  5.     /** 
  6.      * 动画层 
  7.      */  
  8.     private RelativeLayout mAnimLayout;  
  9.       
  10.     /** 
  11.      * 交换两个Item的图片 
  12.      */  
  13.     private void exchangeView()  
  14.     {  
  15.         mFirst.setColorFilter(null);  
  16.         setUpAnimLayout();  
  17.         // 添加FirstView  
  18.         ImageView first = new ImageView(getContext());  
  19.         first.setImageBitmap(mItemBitmaps  
  20.                 .get(getImageIndexByTag((String) mFirst.getTag())).bitmap);  
  21.         LayoutParams lp = new LayoutParams(mItemWidth, mItemWidth);  
  22.         lp.leftMargin = mFirst.getLeft() - mPadding;  
  23.         lp.topMargin = mFirst.getTop() - mPadding;  
  24.         first.setLayoutParams(lp);  
  25.         mAnimLayout.addView(first);  
  26.         // 添加SecondView  
  27.         ImageView second = new ImageView(getContext());  
  28.         second.setImageBitmap(mItemBitmaps  
  29.                 .get(getImageIndexByTag((String) mSecond.getTag())).bitmap);  
  30.         LayoutParams lp2 = new LayoutParams(mItemWidth, mItemWidth);  
  31.         lp2.leftMargin = mSecond.getLeft() - mPadding;  
  32.         lp2.topMargin = mSecond.getTop() - mPadding;  
  33.         second.setLayoutParams(lp2);  
  34.         mAnimLayout.addView(second);  
  35.   
  36.         // 设置动画  
  37.         TranslateAnimation anim = new TranslateAnimation(0, mSecond.getLeft()  
  38.                 - mFirst.getLeft(), 0, mSecond.getTop() - mFirst.getTop());  
  39.         anim.setDuration(300);  
  40.         anim.setFillAfter(true);  
  41.         first.startAnimation(anim);  
  42.   
  43.         TranslateAnimation animSecond = new TranslateAnimation(0,  
  44.                 mFirst.getLeft() - mSecond.getLeft(), 0, mFirst.getTop()  
  45.                         - mSecond.getTop());  
  46.         animSecond.setDuration(300);  
  47.         animSecond.setFillAfter(true);  
  48.         second.startAnimation(animSecond);  
  49.         // 添加动画监听  
  50.         anim.setAnimationListener(new AnimationListener()  
  51.         {  
  52.   
  53.             @Override  
  54.             public void onAnimationStart(Animation animation)  
  55.             {  
  56.                 isAniming = true;  
  57.                 mFirst.setVisibility(INVISIBLE);  
  58.                 mSecond.setVisibility(INVISIBLE);  
  59.             }  
  60.   
  61.             @Override  
  62.             public void onAnimationRepeat(Animation animation)  
  63.             {  
  64.   
  65.             }  
  66.   
  67.             @Override  
  68.             public void onAnimationEnd(Animation animation)  
  69.             {  
  70.                 String firstTag = (String) mFirst.getTag();  
  71.                 String secondTag = (String) mSecond.getTag();  
  72.   
  73.                 String[] firstParams = firstTag.split("_");  
  74.                 String[] secondParams = secondTag.split("_");  
  75.   
  76.                 mFirst.setImageBitmap(mItemBitmaps.get(Integer  
  77.                         .parseInt(secondParams[0])).bitmap);  
  78.                 mSecond.setImageBitmap(mItemBitmaps.get(Integer  
  79.                         .parseInt(firstParams[0])).bitmap);  
  80.   
  81.                 mFirst.setTag(secondTag);  
  82.                 mSecond.setTag(firstTag);  
  83.                 mFirst.setVisibility(VISIBLE);  
  84.                 mSecond.setVisibility(VISIBLE);  
  85.                 mFirst = mSecond = null;  
  86.                 mAnimLayout.removeAllViews();  
  87.                                 //checkSuccess();  
  88.                 isAniming = false;  
  89.             }  
  90.         });  
  91.   
  92.     }  
  93.   
  94.     /** 
  95.      * 创建动画层 
  96.      */  
  97.     private void setUpAnimLayout()  
  98.     {  
  99.         if (mAnimLayout == null)  
  100.         {  
  101.             mAnimLayout = new RelativeLayout(getContext());  
  102.             addView(mAnimLayout);  
  103.         }  
  104.   
  105.     }  
  106.       
  107.     private int getImageIndexByTag(String tag)  
  108.     {  
  109.         String[] split = tag.split("_");  
  110.         return Integer.parseInt(split[0]);  
  111.   
  112.     }  
开始交换时,我们创建一个动画层,然后在这一层上添加上两个一模一样的Item,把原来的Item隐藏了,然后尽情的进行动画切换,setFillAfter为true~
动画完毕,我们已经悄悄的把Item的图片交换了,直接显示出来。这样就完美的切换了:
大致过程:
1、A ,B隐藏 
2、 A副本动画移动到B的位置;B副本移动到A的位置
3、A把图片设置为B,把B副本移除,A显示,这样就完美切合了,用户感觉是B移动过去的。
4、B同上
现在我们的效果:


沒有留言:

張貼留言