V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
qiuchengjia
V2EX  ›  Android

Android 美女拼图游戏,

  •  
  •   qiuchengjia · 2016-09-12 21:21:07 +08:00 · 8479 次点击
    这是一个创建于 2997 天前的主题,其中的信息可能已经有所发展或是发生改变。

    概述

    • 游戏下载试玩

    • Github 喜欢的同学可以 Star 一下,非常感谢

    • 我的博客

    • 把图片切分很多份,点击交换拼成一张完整的;这样关卡也很容易设计, 33 ; 44 ; 55 ; 66 ;一直下去

    • 效果

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

    游戏的设计

    首先我们分析下如何设计这款游戏:

    1. 我们需要一个容器,可以放这些图片的块块,为了方便,我们准备使用 RelativeLayout 配合 addRule 实现

    2. 每个图片的块块,我们准备使用 ImageView

    3. 点击交换,我们准备使用传统的 TranslationAnimation 来实现

    有了初步的设计,感觉这游戏 so easy~

    游戏布局的实现

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

    构造方法

    /** 
         * 设置 Item 的数量 n*n ;默认为 3 
         */  
        private int mColumn = 3;  
        /** 
         * 布局的宽度 
         */  
        private int mWidth;  
        /** 
         * 布局的 padding 
         */  
        private int mPadding;  
        /** 
         * 存放所有的 Item 
         */  
        private ImageView[] mGamePintuItems;  
        /** 
         * Item 的宽度 
         */  
        private int mItemWidth;  
      
        /** 
         * Item 横向与纵向的边距 
         */  
        private int mMargin = 3;  
          
        /** 
         * 拼图的图片 
         */  
        private Bitmap mBitmap;  
        /** 
         * 存放切完以后的图片 bean 
         */  
        private List<ImagePiece> mItemBitmaps;  
          
        private boolean once;  
          
        public GamePintuLayout(Context context)  {  
            this(context, null);  
        }  
      
        public GamePintuLayout(Context context, AttributeSet attrs)  {  
            this(context, attrs, 0);  
        }  
      
      /**
         * 构造函数,用来初始化
         * @param context  the context
         * @param attrs    the attrs
         * @param defStyle the def style
         * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
         */
        public GamePintuLayout(Context context, AttributeSet attrs, int defStyle)  {  
            super(context, attrs, defStyle);  
      
       //把设置的 margin 值转换为 dp
            mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,  
                    mMargin, getResources().getDisplayMetrics());  
            // 设置 Layout 的内边距,四边一致,设置为四内边距中的最小值  
            mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(),  
                    getPaddingBottom());  
        }  
    
    • 构造方法里面,我们得到把设置的 margin 值转化为 dp ;获得布局的 padding 值;整体是个正方形,所以我们取 padding 四个方向中的最小值; 至于 margin ,作为 Item 之间的横向与纵向的间距,你喜欢的话可以抽取为自定义属性~~

    onMeasure

    /**
         * 用来设置设置自定义的 View 的宽高,
         * @param widthMeasureSpec  the width measure spec
         * @param heightMeasureSpec the height measure spec
         * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
         */
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  {  
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
          
            // 获得游戏布局的边长  
            mWidth = Math.min(getMeasuredHeight(), getMeasuredWidth());  
      
            if (!once)  {  
                initBitmap();  
                initItem();  
            }  
            once = true;  
            setMeasuredDimension(mWidth, mWidth);  
        }  
    
    • onMeasure 里面主要就是获得到布局的宽度,然后进行图片的准备,以及初始化我们的 Item ,为 Item 设置宽度和高度

    • initBitmap 自然就是准备图片了:

    /**
      * 初始化 bitmap
      * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
      */
    private void initBitmap()  {  
            if (mBitmap == null)  
                mBitmap = BitmapFactory.decodeResource(getResources(),  
                        R.drawable.aa);  
      
            mItemBitmaps = ImageSplitter.split(mBitmap, mColumn);  
      
       //对图片进行排序
            Collections.sort(mItemBitmaps, new Comparator<ImagePiece>(){  
                @Override  
                public int compare(ImagePiece lhs, ImagePiece rhs){  
                //我们使用 random 随机比较大小
                    return Math.random() > 0.5 ? 1 : -1;  
                }  
            });  
        }  
    
    • 我们这里如果没有设置 mBitmap 就准备一张备用图片,然后调用 ImageSplitter.split 将图片切成 n * n 返回一个 List<imagepiece> 切完以后,我们需要将顺序打乱,所以我们调用了 sort 方法,至于比较器,我们使用 random 随机比较大小,这样我们就完成了我们的乱序操作,赞不赞~~
    /**
     * Description: 图片切片类
     * Data : 2016/9/11-19:53
     * Blog : www.qiuchengjia.cn
     * Author: qiu
     */
    public class ImageSplitter  {  
        /** 
         * 将图片切成 , piece *piece 
         * @param bitmap 
         * @param piece 
         * @return 
         */  
        public static List<ImagePiece> split(Bitmap bitmap, int piece){  
      
            List<ImagePiece> pieces = new ArrayList<ImagePiece>(piece * piece);  
      
            int width = bitmap.getWidth();  
            int height = bitmap.getHeight();  
      
            Log.e("TAG", "bitmap Width = " + width + " , height = " + height);  
            int pieceWidth = Math.min(width, height) / piece;  
      
            for (int i = 0; i < piece; i++){  
                for (int j = 0; j < piece; j++){  
                    ImagePiece imagePiece = new ImagePiece();  
                    imagePiece.index = j + i * piece;  
                    int xValue = j * pieceWidth;  
                    int yValue = i * pieceWidth;  
                      
                    imagePiece.bitmap = Bitmap.createBitmap(bitmap, xValue, yValue,  
                            pieceWidth, pieceWidth);  
                    pieces.add(imagePiece);  
                }  
            }  
            return pieces;  
        }  
    }  
    
    /**
     * Description: 图片 bean
     * Data : 2016/9/11-19:54
     * Blog : www.qiuchengjia.cn
     * Author: qiu
     */
    public class ImagePiece  
    {  
        public int index = 0;  
        public Bitmap bitmap = null;  
    }  
    
    • 没撒说的就是一个根据宽度高度,和 n ,来切图保存的过程~~ ImagePiece 保存的图片以及索引,话说这两个类还是我无意中在网上发现的~~ 图片到此就准备好了,现在看 Item 的生成已经设置宽高,即 initItems
     /**
       * 初始化每一个 item
       * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
       */
    private void initItem()  {  
            // 获得 Item 的宽度  
            int childWidth = (mWidth - mPadding * 2 - mMargin * 
            (mColumn - 1)) / mColumn;  
            mItemWidth = childWidth;  
      
            mGamePintuItems = new ImageView[mColumn * mColumn];  
            // 放置 Item  
            for (int i = 0; i < mGamePintuItems.length; i++) {  
                ImageView item = new ImageView(getContext());  
      
                item.setOnClickListener(this);  
      
                item.setImageBitmap(mItemBitmaps.get(i).bitmap);  
                mGamePintuItems[i] = item;  
                item.setId(i + 1);  
                item.setTag(i + "_" + mItemBitmaps.get(i).index);  
      
                RelativeLayout.LayoutParams lp =
                    new LayoutParams(mItemWidth,  
                        mItemWidth);  
                // 设置横向边距,不是最后一列  
                if ((i + 1) % mColumn != 0)  {  
                    lp.rightMargin = mMargin;  
                }  
                // 如果不是第一列  
                if (i % mColumn != 0)  {  
                    lp.addRule(RelativeLayout.RIGHT_OF,//  
                            mGamePintuItems[i - 1].getId());  
                }  
                // 如果不是第一行,//设置纵向边距,非最后一行  
                if ((i + 1) > mColumn)  {  
                    lp.topMargin = mMargin;  
                    lp.addRule(RelativeLayout.BELOW,//  
                            mGamePintuItems[i - mColumn].getId());  
                }  
                addView(item, lp);  
            }  
        }  
    
    • 可以看到我们的 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 )

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

    • 然后我们在布局文件里面声明下:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    
        <game.qiu.com.beautygame.GamePintuLayout
            android:id="@+id/id_gameview"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_centerInParent="true"
            android:padding="5dp" >
        </game.qiu.com.beautygame.GamePintuLayout>
    
    </RelativeLayout>
    
    • Activity 里面记得设置这个布局~~

    • 现在的效果是:

    游戏的切换效果

    初步的切换

    • 还记得我们都给 Item 添加了 onClick 的监听么~~ 现在我们需要实现,点击两个 Item ,他们的图片能够发生交换~ 那么,我们需要两个成员变量来存储这两个 Item ,然后再去交换
    /**
      * 记录第一次点击的 ImageView
      */
    private ImageView mFirst;  
    /**
      * 记录第二次点击的 ImageView
      */
    private ImageView mSecond;  
    /**
      * 点击事件
      * @param view the view
      * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
      */  
    @Override  
    public void onClick(View v)  {  
        /** 
         * 如果两次点击是同一个 
         */  
        if (mFirst == v)  {  
            mFirst.setColorFilter(null);  
            mFirst = null;  
            return;  
        }  
        //点击第一个 Item  
        if (mFirst == null)  {  
            mFirst = (ImageView) v;  
            mFirst.setColorFilter(Color.parseColor("#55FF0000"));  
        } else//点击第二个 Item  
        {  
            mSecond = (ImageView) v;  
            exchangeView();  
        }  
      
    }  
    
    • 点击第一个,通过 setColorFilter 设置下选中效果,再次点击另一个,那我们就准备调用 exchangeView 进行交换图片了,当然这个方法我们还没写,先放着~ 如果两次点击同一个,去除选中效果,我们就当什么都没发生

    • 接下来,我们来实现 exchangeView :

    /**
      * 交换两个 Item 图片 
      * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
      */
     private void exchangeView()  {  
                    
            mFirst.setColorFilter(null);  
            String firstTag = (String) mFirst.getTag();  
            String secondTag = (String) mSecond.getTag();  
              
            //得到在 list 中索引位置  
            String[] firstImageIndex = firstTag.split("_");  
            String[] secondImageIndex = secondTag.split("_");  
              
            mFirst.setImageBitmap(mItemBitmaps.get(Integer  
                    .parseInt(secondImageIndex[0])).bitmap);  
            mSecond.setImageBitmap(mItemBitmaps.get(Integer  
                    .parseInt(firstImageIndex[0])).bitmap);  
      
            mFirst.setTag(secondTag);  
            mSecond.setTag(firstTag);  
              
            mFirst = mSecond = null;  
      
     }  
    
    • 应该还记得我们之前的 setTag 吧,忘了,返回去看看,我们还说注意来着~ 通过 getTag ,拿到在 List 中是索引,然后得到 bitmap 进行交换设置,最后交换 tag ; 到此我们的交换效果写完了,我们的游戏可以完了~~效果是这样的:

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

    无缝的动画切换

    • 我们先聊聊怎么添加,我准备使用 TranslationAnimation ,然后两个 Item 的 top , left 也很容器获取; 但是,要明白,我们实际上, Item 只是 setImage 发生了变化, Item 的位置没有变; 我们现在需要动画移动效果,比如 A 移动到 B ,没问题,移动完成以后, Item 得回去吧,但是图片并没有发生变化,我们还是需要手动 setImage 这样造成了一个现象,动画切换效果有了,但是最后还是会有一闪,是我们切换图片造成的; 为了避免上述现象,能够完美的做到切换效果,这里我们引入一个动画图层,专门做动画效果,有点类似 ps 的图层,下面看我们怎么做;
    /** 
      * 动画运行的标志位 
      */  
    private boolean isAniming;  
    /** 
      * 动画层 
      */  
    private RelativeLayout mAnimLayout;  
          
    /**
      * 交换两个 Item 图片
      * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
      */
    private void exchangeView(){  
            mFirst.setColorFilter(null);  
            setUpAnimLayout();  
            // 添加 FirstView  
            ImageView first = new ImageView(getContext());  
            first.setImageBitmap(mItemBitmaps  
                    .get(getImageIndexByTag((String) mFirst.getTag())).bitmap);  
            LayoutParams lp = new LayoutParams(mItemWidth, mItemWidth);  
            lp.leftMargin = mFirst.getLeft() - mPadding;  
            lp.topMargin = mFirst.getTop() - mPadding;  
            first.setLayoutParams(lp);  
            mAnimLayout.addView(first);  
            // 添加 SecondView  
            ImageView second = new ImageView(getContext());  
            second.setImageBitmap(mItemBitmaps  
                    .get(getImageIndexByTag((String) mSecond.getTag())).bitmap);  
            LayoutParams lp2 = new LayoutParams(mItemWidth, mItemWidth);  
            lp2.leftMargin = mSecond.getLeft() - mPadding;  
            lp2.topMargin = mSecond.getTop() - mPadding;  
            second.setLayoutParams(lp2);  
            mAnimLayout.addView(second);  
      
            // 设置动画  
            TranslateAnimation anim = new TranslateAnimation(0, mSecond.getLeft()  
                    - mFirst.getLeft(), 0, mSecond.getTop() - mFirst.getTop());  
            anim.setDuration(300);  
            anim.setFillAfter(true);  
            first.startAnimation(anim);  
      
            TranslateAnimation animSecond = new TranslateAnimation(0,  
                    mFirst.getLeft() - mSecond.getLeft(), 0, mFirst.getTop()  
                            - mSecond.getTop());  
            animSecond.setDuration(300);  
            animSecond.setFillAfter(true);  
            second.startAnimation(animSecond);  
            // 添加动画监听  
            anim.setAnimationListener(new AnimationListener(){  
      
                @Override  
                public void onAnimationStart(Animation animation){  
                    isAniming = true;  
                    mFirst.setVisibility(INVISIBLE);  
                    mSecond.setVisibility(INVISIBLE);  
                }  
      
                @Override  
                public void onAnimationRepeat(Animation animation){  
      
                }  
      
                @Override  
                public void onAnimationEnd(Animation animation){  
                    String firstTag = (String) mFirst.getTag();  
                    String secondTag = (String) mSecond.getTag();  
      
                    String[] firstParams = firstTag.split("_");  
                    String[] secondParams = secondTag.split("_");  
      
                    mFirst.setImageBitmap(mItemBitmaps.get(Integer  
                            .parseInt(secondParams[0])).bitmap);  
                    mSecond.setImageBitmap(mItemBitmaps.get(Integer  
                            .parseInt(firstParams[0])).bitmap);  
      
                    mFirst.setTag(secondTag);  
                    mSecond.setTag(firstTag);  
                    mFirst.setVisibility(VISIBLE);  
                    mSecond.setVisibility(VISIBLE);  
                    mFirst = mSecond = null;  
                    mAnimLayout.removeAllViews();  
                                    //checkSuccess();  
                    isAniming = false;  
                }  
            });  
      
        }  
      
        /** 
         * 创建动画层 
         */  
        private void setUpAnimLayout(){  
            if (mAnimLayout == null){  
                mAnimLayout = new RelativeLayout(getContext());  
                addView(mAnimLayout);  
            }  
      
        }  
          
        private int getImageIndexByTag(String tag){  
            String[] split = tag.split("_");  
            return Integer.parseInt(split[0]);  
      
        }  
    
    • 开始交换时,我们创建一个动画层,然后在这一层上添加上两个一模一样的 Item ,把原来的 Item 隐藏了,然后尽情的进行动画切换, setFillAfter 为 true~ 动画完毕,我们已经悄悄的把 Item 的图片交换了,直接显示出来。这样就完美的切换了:

    大致过程:

    1. A , B 隐藏

    2. A 副本动画移动到 B 的位置; B 副本移动到 A 的位置

    3. A 把图片设置为 B ,把 B 副本移除, A 显示,这样就完美切合了,用户感觉是 B 移动过去的

    4. B 同上

    • 现在我们的效果:

    • 现在效果满意了把~~为了防止用户狂点,在 onClick 里面添加一句:
    @Override  
        public void onClick(View v)  
        {  
            // 如果正在执行动画,则屏蔽  
            if (isAniming)  
                return;  
    
    • 到此我们的动画的切换,已经完美结束了~~ 切换时,我们是不是应该判断是否成功了~~

    游戏胜利的判断

    • 我们在切换完成,进行 checkSuccess();的判断;好在我们把图片的正确的顺序存在 tag 里面~~
    /**
      * 用来判断游戏是否成功
      * @author qiu  博客: www.qiuchengjia.cn 时间: 2016-09-12
      */
    private void checkSuccess(){  
            boolean isSuccess = true;  
            for (int i = 0; i < mGamePintuItems.length; i++){  
                ImageView first = mGamePintuItems[i];  
                Log.e("TAG", getIndexByTag((String) first.getTag()) + "");  
                if (getIndexByTag((String) first.getTag()) != i){  
                    isSuccess = false;  
                }  
            }  
      
            if (isSuccess){  
                Toast.makeText(getContext(), "Success , Level Up !",  
                        Toast.LENGTH_LONG).show();  
                // nextLevel();  
            }  
        }  
      
        /** 
         * 获得图片的真正索引 
         * @param tag 
         * @return 
         */  
        private int getIndexByTag(String tag){  
            String[] split = tag.split("_");  
            return Integer.parseInt(split[1]);  
        }  
    
    • 很简单,遍历所有的 Item ,根据 Tag 拿到真正的索引和当然顺序比较,完全一致则胜利~~胜利以后进入下一关

    • 至于下一关的代码:

    public void nextLevel(){  
            this.removeAllViews();  
            mAnimLayout = null;  
            mColumn++;  
            initBitmap();  
            initItem();  
        }  
    

    总结

    • ok ,到此我们的游戏结束了,我来带大家闯个关:

    源码下载

    参考资料

    11 条回复    2016-09-13 23:14:09 +08:00
    zixianlei
        1
    zixianlei  
       2016-09-12 23:53:43 +08:00
    这个可以啊。哈哈
    qiuchengjia
        2
    qiuchengjia  
    OP
       2016-09-13 00:08:51 +08:00 via Android
    @zixianlei 对啊,仔细看看啦,其实挺简单的
    Override
        3
    Override  
       2016-09-13 10:00:51 +08:00
    @param 有人 @你
    param
        4
    param  
       2016-09-13 10:02:01 +08:00
    围观
    qiuchengjia
        5
    qiuchengjia  
    OP
       2016-09-13 12:17:14 +08:00
    @param 可以自己实现一下,还不错
    qiuchengjia
        6
    qiuchengjia  
    OP
       2016-09-13 12:17:31 +08:00
    @Override 可以自己实现一下,还不错
    gino86
        7
    gino86  
       2016-09-13 14:28:54 +08:00
    代码太多没有看,觉得单纯就是交换了两张图片的布局位置
    xmi
        8
    xmi  
       2016-09-13 22:57:15 +08:00
    原谅我看成戒码人生了,以为 LZ 要戒了。。。
    xmi
        9
    xmi  
       2016-09-13 23:04:32 +08:00
    🌟 & 🍴
    qiuchengjia
        10
    qiuchengjia  
    OP
       2016-09-13 23:13:15 +08:00 via Android
    @xmi 哈哈哈哈,没事
    qiuchengjia
        11
    qiuchengjia  
    OP
       2016-09-13 23:14:09 +08:00 via Android
    @gino86 本来就很简单,这里面其实有一个图层专门用来交换图片位置
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3260 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:24 · PVG 20:24 · LAX 04:24 · JFK 07:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.