Android自定义控件:做一个拼图游戏
2017-07-17 10:23
549 查看
一些简单的游戏可以用自定义控件实现,如拼图游戏。先上效果图:
2、把一张大图切割成多个小图。
3、图片压缩。
4、属性动画。
5、DialogFragment的使用。
继承View:继承View的自定义控件可以叫自绘控件,需要用到paint、canvas等类来进行绘制。例如:http://www.jianshu.com/p/ac33e61a1476
继承ViewGroup:继承ViewGroup的自定义控件可以叫组合控件,有多个控件组合而成的自定义控件,例如这个拼图游戏,就是由多个ImageView和一个ViewGroup组合而成。
**View有对应的四个方法:**getLeft() 、getTop()、 getRight()、 getBottom() 。来分别获取left,top,right,bottom的值。这些值代表子view的边界和父容器边界的距离。
图片来自:http://blog.csdn.net/u013872857/article/details/53750682
从图片中可以看出:
如果一个View获取到它自身的left、top、right、bottom四个参数,就可以通过view的layout方法来确定该view在父容器的位置。
这些参数在ViewGroup中的使用:
拼图游戏中计算出了每个ImageView的left、top、right、bottom。计算的原因是:ViewGroup通过AddView(子view)之后子View默认都是显示在左上角。通过计算之后才能使子view根据不同的left、top、right、bottom展示在不同的位置。
每个ImageView在onLayout方法中的展示:
这里通过 imageView.layout()方法将图片展示在父容器不同的位置。
图片切割的方法:
通过Bitmap.createBitmap的方法将图片分割成多份。建立一个javaBean用来保存bitmap和index(index保存图片的下标,用于检查是否完成拼图)。在拼图游戏的普通模式(普通模式效果图)中有一张空白的图,这里用一张透明的.9 图代替。
QQ的列表中的气泡拖拽效果,就用到类似的概念:
如果这个气泡是在某个ViewGroup中,那么拖动的时候是不可能拖出这个ViewGroup的,因为气泡是这个ViewGroup的子类,它不可能展示在ViewGroup之外,更不要说整个屏幕都能拖动了。因此这里可能用到动画层的概念:在点击气泡时,隐藏点击的气泡,并添加一个透明的全屏的ViewGroup覆盖在整个布局上,然后在原来的气泡位置添加一个相似的气泡,然后就可以做到在这个ViewGroup上面滑动了。(注:这是我YY出来的结果,可能QQ并不是这样实现的)
这个会遇到一个问题:调用了
解决该问题的代码:
3.5.2 实现小图滑动或者交换
拼图时,无论是普通模式或者交换模式,无非都是两个图片的交换效果。
当点击图片时,隐藏点击的图片并记录,然后生成动画层,在动画层上生成大小位置一样的图片,然后在动画层上实现图片交换的效果。
动画完成之后可以在onAnimationEnd中隐藏动画层,移除掉动画层中的ImageView,将记录好的两个ImageView一些属性的交换,比如说bitmap的交换,index的交换。
注意:效果看起来像两个ImageView互换了位置,实际上只是bitmap相互替换了。普通模式中的空白图片默认就是记录好的一张图片。
DialogFragment在android 3.0时被引入。是一种特殊的Fragment,用于在Activity的内容之上展示一个模态的对话框。典型的用于:展示警告框,输入框,确认框等等。
3.6.2 使用的好处
使用DialogFragment来管理对话框,当旋转屏幕和按下后退键时可以更好的管理其声明周期,它和Fragment有着基本一致的声明周期。且DialogFragment也允许开发者把Dialog作为内嵌的组件进行重用,类似Fragment(可以在大屏幕和小屏幕显示出不同的效果)。上面会通过例子展示这些好处~
以上的文字来自博客:http://blog.csdn.net/lmj623565791/article/details/37815413/
基本用法都在上面的博客了,不详细讲解用法了。
图片选择和游戏成功时用到DialogFragment。
属性动画:
http://blog.csdn.net/lmj623565791/article/details/38067475
http://www.jianshu.com/p/ecba05115d80
http://www.jianshu.com/p/2412d00a0ce4
DialogFragment:
http://blog.csdn.net/lmj623565791/article/details/37815413/
ViewGroup:
http://blog.csdn.net/lmj623565791/article/details/38339817/
源码地址:https://github.com/AxeChen/PuzzleGame
1、游戏的大概思路
游戏的基本思路:将一个大图切割成多份小图,然后将小图的顺序打乱,整齐排列在一个ViewGroup中,通过点击小图互换位置将图片拼合为原来的大图。2、技术要点
1、继承ViewGroup的自定义控件以及onLayout方法的使用。2、把一张大图切割成多个小图。
3、图片压缩。
4、属性动画。
5、DialogFragment的使用。
3、技术点分析
3.1、继承ViewGroup实现自定义View
在实现一个自定义View时,需要判断继承View还是ViewGroup。继承View:继承View的自定义控件可以叫自绘控件,需要用到paint、canvas等类来进行绘制。例如:http://www.jianshu.com/p/ac33e61a1476
继承ViewGroup:继承ViewGroup的自定义控件可以叫组合控件,有多个控件组合而成的自定义控件,例如这个拼图游戏,就是由多个ImageView和一个ViewGroup组合而成。
3.2、非常重要的onLayout方法
继承ViewGroup,onLayout方法必须实现的。这个方法非常重要,是控制子控件在父容器中位置的关键方法。@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
**View有对应的四个方法:**getLeft() 、getTop()、 getRight()、 getBottom() 。来分别获取left,top,right,bottom的值。这些值代表子view的边界和父容器边界的距离。
图片来自:http://blog.csdn.net/u013872857/article/details/53750682
从图片中可以看出:
left = view.getLeft(); top = view.getTop(); right = view.getLeft()+view的宽度 botto = view.getTop()+view的高度
如果一个View获取到它自身的left、top、right、bottom四个参数,就可以通过view的layout方法来确定该view在父容器的位置。
这些参数在ViewGroup中的使用:
protected void onLayout(boolean changed, int l, int t, int r, int b) { view.layout(left,top,right, bottom); }
拼图游戏中计算出了每个ImageView的left、top、right、bottom。计算的原因是:ViewGroup通过AddView(子view)之后子View默认都是显示在左上角。通过计算之后才能使子view根据不同的left、top、right、bottom展示在不同的位置。
private void initBitmapsWidth() { int line = 0; int left = 0; int top = 0; int right = 0; int bottom = 0; for (int i = 0; i < mImagePieces.size(); i++) { /// ... 省略若干代码 if (i != 0 && i % mCount == 0) { line++; } if (i % mCount == 0) { left = i % mCount * mItemWidth; } else { left = i % mCount * mItemWidth + (i % mCount) * mMargin; } top = mItemWidth * line + line * mMargin; right = left + mItemWidth; bottom = top + mItemWidth; imageView.setRight(right); imageView.setLeft(left); imageView.setBottom(bottom); imageView.setTop(top); imageView.setId(i); imageView.setOnClickListener(this); mImagePieces.get(i).setImageView(imageView); addView(imageView); } }
每个ImageView在onLayout方法中的展示:
这里通过 imageView.layout()方法将图片展示在父容器不同的位置。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { if (getChildAt(i) instanceof ImageView) { ImageView imageView = (ImageView) getChildAt(i); imageView.layout(imageView.getLeft(), imageView.getTop(), imageView.getRight(), imageView.getBottom()); } else { //针对动画层的layout if (getChildAt(i) instanceof RelativeLayout) { RelativeLayout relativeLayout = (RelativeLayout) getChildAt(i); relativeLayout.layout(0, 0, mViewWidth, mViewWidth); } } } }
3.3 将一张大图切割成多个小图
这里的拼图游戏并不是自己找来很多的图片,而是用一张大图片切割成多个小图片。这也比较好理解,随着难度等级提高,每一行显示的图片要增加,如果每个小图是单独的图片,那么这会非常麻烦。图片切割的方法:
/** * 传入一个bitmap 返回 一个picec集合 * * @param bitmap * @param count * @return */ public static List<ImagePiece> splitImage(Context context, Bitmap bitmap, int count, String gameMode) { List<ImagePiece> imagePieces = new ArrayList<>(); int width = bitmap.getWidth(); int height = bitmap.getHeight(); int picWidth = Math.min(width, height) / count; for (int i = 0; i < count; i++) { for (int j = 0; j < count; j++) { ImagePiece imagePiece = new ImagePiece(); imagePiece.setIndex(j + i * count); //为createBitmap 切割图片获取xy int x = j * picWidth; int y = i * picWidth; if (gameMode.equals(PuzzleLayout.GAME_MODE_NORMAL)) { if (i == count - 1 && j == count - 1) { imagePiece.setType(ImagePiece.TYPE_EMPTY); Bitmap emptyBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.empty); imagePiece.setBitmap(emptyBitmap); } else { imagePiece.setBitmap(Bitmap.createBitmap(bitmap, x, y, picWidth, picWidth)); } } else { imagePiece.setBitmap(Bitmap.createBitmap(bitmap, x, y, picWidth, picWidth)); } imagePieces.add(imagePiece); } } return imagePieces; }
通过Bitmap.createBitmap的方法将图片分割成多份。建立一个javaBean用来保存bitmap和index(index保存图片的下标,用于检查是否完成拼图)。在拼图游戏的普通模式(普通模式效果图)中有一张空白的图,这里用一张透明的.9 图代替。
3.4 图片的压缩
为了防止图片过大导致OOM,这里用了压缩图片的方法:/** * 读取图片,按照缩放比保持长宽比例返回bitmap对象 * <p> * * @param scale 缩放比例(1到10, 为2时,长和宽均缩放至原来的2分之1,为3时缩放至3分之1,以此类推) * @return Bitmap */ public synchronized static Bitmap readBitmap(Context context, int res, int scale) { try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = false; options.inSampleSize = scale; options.inPurgeable = true; options.inInputShareable = true; options.inPreferredConfig = Bitmap.Config.RGB_565; return BitmapFactory.decodeResource(context.getResources(), res, options); } catch (Exception e) { return null; } }
3.5 属性动画
3.5.1 动画层的概念QQ的列表中的气泡拖拽效果,就用到类似的概念:
如果这个气泡是在某个ViewGroup中,那么拖动的时候是不可能拖出这个ViewGroup的,因为气泡是这个ViewGroup的子类,它不可能展示在ViewGroup之外,更不要说整个屏幕都能拖动了。因此这里可能用到动画层的概念:在点击气泡时,隐藏点击的气泡,并添加一个透明的全屏的ViewGroup覆盖在整个布局上,然后在原来的气泡位置添加一个相似的气泡,然后就可以做到在这个ViewGroup上面滑动了。(注:这是我YY出来的结果,可能QQ并不是这样实现的)
/** * 构造动画层 用于点击之后的动画 * 为什么要做动画层? 要保证动画在整个view上面执行。 */ private void setUpAnimLayout() { if (mAnimLayout == null) { mAnimLayout = new RelativeLayout(getContext()); } if (!isAddAnimatorLayout) { isAddAnimatorLayout = true; addView(mAnimLayout); } }
这个会遇到一个问题:调用了
addView(mAnimLayout);这段代码之后发现,动画层不显示。这个问题可能需要去看源码,不过我还是暂时找到了一个解决方案(暂时也不知道原因):addview之后要重新给子view设置宽高。http://www.cnblogs.com/renjiemei1225/p/6215671.html
解决该问题的代码:
@Override protected void onMeasure(int widthMeasureSpec, dfdd int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(mViewWidth, mViewWidth); for (int i = 0; i < getChildCount(); i++) { if (getChildAt(i) instanceof RelativeLayout) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); } } }
3.5.2 实现小图滑动或者交换
拼图时,无论是普通模式或者交换模式,无非都是两个图片的交换效果。
当点击图片时,隐藏点击的图片并记录,然后生成动画层,在动画层上生成大小位置一样的图片,然后在动画层上实现图片交换的效果。
动画完成之后可以在onAnimationEnd中隐藏动画层,移除掉动画层中的ImageView,将记录好的两个ImageView一些属性的交换,比如说bitmap的交换,index的交换。
注意:效果看起来像两个ImageView互换了位置,实际上只是bitmap相互替换了。普通模式中的空白图片默认就是记录好的一张图片。
/** * @param imageView 点击时记录下的ImageView * @return */ private ImageView addAnimationImageView(ImageView imageView) { ImageView getImage = new ImageView(getContext()); RelativeLayout.LayoutParams firstParams = new RelativeLayout.LayoutParams(mItemWidth, mItemWidth); firstParams.leftMargin = imageView.getLeft() - mPadding; firstParams.topMargin = imageView.getTop() - mPadding; Bitmap firstBitmap = mImagePieces.get(imageView.getId()).getBitmap(); getImage.setImageBitmap(firstBitmap); getImage.setLayoutParams(firstParams); mAnimLayout.addView(getImage); return getImage; } private ImageView addAnimationImageView(ImageView imageView) { ImageView getImage = new ImageView(getContext()); RelativeLayout.LayoutParams firstParams = new RelativeLayout.LayoutParams(mItemWidth, mItemWidth); firstParams.leftMargin = imageView.getLeft() - mPadding; firstParams.topMargin = imageView.getTop() - mPadding; Bitmap firstBitmap = mImagePieces.get(imageView.getId()).getBitmap(); getImage.setImageBitmap(firstBitmap); getImage.setLayoutParams(firstParams); mAnimLayout.addView(getImage); return getImage; } /** * 添加动画层,并且添加平移的动画 */ private void exChangeView() { //添加动画层 setUpAnimLayout(); //添加第一个图片 ImageView first = addAnimationImageView(mFirst); //添加另一个图片 ImageView second = addAnimationImageView(mSecond); ObjectAnimator secondXAnimator = ObjectAnimator.ofFloat(second, "TranslationX", 0f, -(mSecond.getLeft() - mFirst.getLeft())); ObjectAnimator secondYAnimator = ObjectAnimator.ofFloat(second, "TranslationY", 0f, -(mSecond.getTop() - mFirst.getTop())); ObjectAnimator firstXAnimator = ObjectAnimator.ofFloat(first, "TranslationX", 0f, mSecond.getLeft() - mFirst.getLeft()); ObjectAnimator firstYAnimator = ObjectAnimator.ofFloat(first, "TranslationY", 0f, mSecond.getTop() - mFirst.getTop()); AnimatorSet secondAnimator = new AnimatorSet(); secondAnimator.play(secondXAnimator).with(secondYAnimator).with(firstXAnimator).with(firstYAnimator); secondAnimator.setDuration(300); secondAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { ImagePiece firstPiece = mImagePieces.get(mFirst.getId()); ImagePiece secondPiece = mImagePieces.get(mSecond.getId()); int firstType = firstPiece.getType(); int secondType = secondPiece.getType(); Bitmap firstBitmap = mImagePieces.get(mFirst.getId()).getBitmap(); Bitmap secondBitmap = mImagePieces.get(mSecond.getId()).getBitmap(); int fristIndex = firstPiece.getIndex(); int secondeIndex = secondPiece.getIndex(); if (mFirst != null) { mFirst.setColorFilter(null); mFirst.setVisibility(VISIBLE); mFirst.setImageBitmap(secondBitmap); firstPiece.setBitmap(secondBitmap); firstPiece.setIndex(secondeIndex); } if (mSecond != null) { mSecond.setVisibility(VISIBLE); mSecond.setImageBitmap(firstBitmap); secondPiece.setBitmap(firstBitmap); secondPiece.setIndex(fristIndex); } if (mGameMode.equals(GAME_MODE_NORMAL)) { firstPiece.setType(secondType); secondPiece.setType(firstType); } mAnimLayout.removeAllViews(); mAnimLayout.setVisibility(GONE); mFirst = null; mSecond = null; isAnimation = false; invalidate(); if (checkSuccess()) { Toast.makeText(getContext(), "成功!", Toast.LENGTH_SHORT).show(); if (mSuccessListener != null) { mSuccessListener.success(); } } } @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); isAnimation = true; mAnimLayout.setVisibility(VISIBLE); mFirst.setVisibility(INVISIBLE); mSecond.setVisibility(INVISIBLE); } }); secondAnimator.start(); }
3.6 DialogFragment的使用
3.6.1 基本概念DialogFragment在android 3.0时被引入。是一种特殊的Fragment,用于在Activity的内容之上展示一个模态的对话框。典型的用于:展示警告框,输入框,确认框等等。
3.6.2 使用的好处
使用DialogFragment来管理对话框,当旋转屏幕和按下后退键时可以更好的管理其声明周期,它和Fragment有着基本一致的声明周期。且DialogFragment也允许开发者把Dialog作为内嵌的组件进行重用,类似Fragment(可以在大屏幕和小屏幕显示出不同的效果)。上面会通过例子展示这些好处~
以上的文字来自博客:http://blog.csdn.net/lmj623565791/article/details/37815413/
基本用法都在上面的博客了,不详细讲解用法了。
图片选择和游戏成功时用到DialogFragment。
3.7 一些公开的API
提供一些公共的方法便于改变游戏的模式、难度、图片等。每次改变都应该重置一些必要参数。/** * 重置游戏 */ public void reset() { mItemWidth = (mViewWidth - mPadding * 2 - mMargin * (mCount - 1)) / mCount; if (mImagePieces != null) { mImagePieces.clear(); } isAddAnimatorLayout = false; mBitmap = null; removeAllViews(); initBitmaps(); initBitmapsWidth(); } /** * 添加count 最多每行7个 */ public boolean addCount() { mCount++; if (mCount > 7) { mCount--; return false; } reset(); return true; } /** * 改变图片 */ public void changeRes(int res) { this.res = res; reset(); } /** * 减少count 最少每行三个,否则普通模式无法游戏 */ public boolean reduceCount() { mCount--; if (mCount < 3) { mCount++; return false; } reset(); return true; }
3.8 其他
一些推荐的网址:属性动画:
http://blog.csdn.net/lmj623565791/article/details/38067475
http://www.jianshu.com/p/ecba05115d80
http://www.jianshu.com/p/2412d00a0ce4
DialogFragment:
http://blog.csdn.net/lmj623565791/article/details/37815413/
ViewGroup:
http://blog.csdn.net/lmj623565791/article/details/38339817/
源码地址:https://github.com/AxeChen/PuzzleGame
相关文章推荐
- 为Unity开发的android手机游戏添加一个社会化分享功能
- Android开发自定义控件实现一个圆形进度条【带数值和动画】
- 用lGame(3.0)框架在android平台上开发一个经典游戏Tetris (将会有LGame与Android的对比呦)第二篇
- (转)【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- Android 自定义控件-Canvas和Paint绘图详解-手把手带你绘制一个时钟.
- android开发专题系列-一个简单的游戏的设计
- android自定义控件(一):写一个圆形菜单的Layout
- 发现一个牛人写的android游戏教程——libgdx引擎
- VerticalBannerView 是一个 android 平台下的自定义控件,通常用来展示广告,类似淘宝头条
- LGame(Android及J2SE游戏引擎)入门示例——如何构建一个游戏
- 16—【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能
- 一个简单的美女拼图游戏
- Android自定义控件实现一个带文本与数字的圆形进度条
- 一个简单的入门的android游戏demo
- LGame(Android及J2SE游戏引擎)入门示例——如何构建一个游戏
- 安卓Android自定义控件:200行代码实现一个简约时钟
- 初学Android,写了一个小时候的游戏《狼吃羊》
- 【Android游戏开发之四】基础的Android 游戏框架(一个游戏角色在屏幕行走的demo)
- Android开发自定义控件实现一个球赛胜负数统计条
- [置顶] Android用SurfaceView写一个简单有趣的游戏--《数字组合》之一