一些简单的游戏可以用自定义控件实现,如拼图游戏。先上效果图:
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 splitImage(Context context, Bitmap bitmap, int count, String gameMode) {
List 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对象
*
*
* @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的子View,它不可能展示在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, 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 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
原创不易,如果大佬对这篇文章感兴趣,还望点个赞,给点鼓励!