视频地址: Android美女拼图小游戏
实现功能:
(1)多个难度
第一关3*3。
(2)倒计时
(3)图片切分
(4)图片位置变换
第一步:
在src下创建工具包com.imooc.game.utils,创建两个工具类分别为ImagePiece.java和ImageSplitterUtil.java:
package com.imooc.game.utils;
import android.graphics.Bitmap;
/**
* 用来存储每一张图片。图片碎片
* */
public class ImagePiece
{
private int index ; // 当前第几块
private Bitmap bitmap ; // 指向当前图片
public ImagePiece()
{
}
public ImagePiece(int index, Bitmap bitmap)
{
this.index = index;
this.bitmap = bitmap;
}
public int getIndex()
{
return index;
}
public void setIndex(int index)
{
this.index = index;
}
public Bitmap getBitmap()
{
return bitmap;
}
public void setBitmap(Bitmap bitmap)
{
this.bitmap = bitmap;
}
/*
* 这个是每个类里可以重写的。
* */
@Override
public String toString()
{
return "ImagePiece [index=" + index + ", bitmap=" + bitmap + "]";
}
}
package com.imooc.game.utils;
import java.util.ArrayList;
import java.util.List;
import android.graphics.Bitmap;
public class ImageSplitterUtil
{
/**
* 传入bitmap,切成piece*piece块,返回的是这个List列表。
* @param bitmap
* @param piece
* @return List<ImagePiece>
*/
public static List<ImagePiece> splitImage(Bitmap bitmap, int piece)
{
List<ImagePiece> imagePieces = new ArrayList<ImagePiece>();
// 获取图片宽高
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 图片应该是方的,这是每一块的宽度。
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();
// i表示行,j表示列,得到的数字就是1,2,3,4,5,6,7。。。
imagePiece.setIndex(j + i * piece);
int x = j * pieceWidth;
int y = i * pieceWidth;
// 第二三个参数是顶点坐标,第四五个参数是宽高。
imagePiece.setBitmap(Bitmap.createBitmap(bitmap, x, y,
pieceWidth, pieceWidth));
imagePieces.add(imagePiece);
}
}
return imagePieces;
}
}
package com.imooc.game.pintu.view;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.imooc.game.pintu.R;
import com.imooc.game.utils.ImagePiece;
import com.imooc.game.utils.ImageSplitterUtil;
/*
* 用RelativeLayout比较容易控制位置。
* **/
public class GamePintuLayout extends RelativeLayout implements OnClickListener
{
// 默认3*3的碎片
private int mColumn = 3;
// 容器的内边距
private int mPadding;
// 每张小图之间的距离,横纵。dp
private int mMargin = 3;
// 每一张小图位置上的那个view
private ImageView[] mGamePintuItems;
// 每一张小图的宽度。
private int mItemWidth;
// 游戏的图片。
private Bitmap mBitmap;
// 存放所有的图片碎片的list。
private List<ImagePiece> mItemBitmaps;
// 操作一次的标识。
private boolean once;
// 游戏面板的宽度,容器的宽度
private int mWidth;
private boolean isGameSuccess;
private boolean isGameOver;
// 设置接口通知MainActivity进行回调。这三个函数是在MainActivity中进行实现的。这里提供的只是一个接口。
public interface GamePintuListener
{
void nextLevel(int nextLevel);
void timechanged(int currentTime);
void gameover();
}
// 接口成员变量。
public GamePintuListener mListener;
/**
* 设置接口回调
*
* @param mListener
*/
public void setOnGamePintuListener(GamePintuListener mListener)
{
this.mListener = mListener;
}
private int mLevel = 1;
private static final int TIME_CHANGED = 0x110;
private static final int NEXT_LEVEL = 0x111;
/*
* 进行UI操作。
* */
private Handler mHandler = new Handler()
{
public void handleMessage(android.os.Message msg)
{
switch (msg.what)
{
case TIME_CHANGED:
// 如果游戏成功、失败、停止,停止计时。
if (isGameSuccess || isGameOver || isPause)
return;
if (mListener != null)
{
mListener.timechanged(mTime);
}
if (mTime == 0)
{
isGameOver = true;
mListener.gameover();
return;
}
mTime--;
// 每一秒发送一次减少。
mHandler.sendEmptyMessageDelayed(TIME_CHANGED, 1000);
break;
case NEXT_LEVEL:
mLevel = mLevel + 1;
// 这个是判断用户是否选择进行下一关。
if (mListener != null)
{
mListener.nextLevel(mLevel);
} else
{
nextLevel();
}
break;
}
};
};
private boolean isTimeEnabled = false;
private int mTime;
/**
* 设置是否开启时间
*
* @param isTimeEnabled
*/
public void setTimeEnabled(boolean isTimeEnabled)
{
this.isTimeEnabled = isTimeEnabled;
}
/*
* 第一个构造方法调用第二个构造方法。
* */
public GamePintuLayout(Context context)
{
this(context, null);
}
/*
* 第二个构造方法调用第三个构造方法。
* */
public GamePintuLayout(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
/*
* 第三个构造方法进行初始化。
* */
public GamePintuLayout(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
init();
}
private void init()
{
/*
* 可以用TypedValue进行单位的转换,将dp转为px,或者将sp转为px
* 这样也就把我们3dp转化为3px的值。
* 在布局当中尽可能的所有的字体使用sp,所有的merage使用dp,一般不使用px,因为不同的屏幕上分辨率是不同的。
* */
mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
3, getResources().getDisplayMetrics());
// 保证上下左右边距相同的,取最小值
mPadding = min(getPaddingLeft(), getPaddingRight(), getPaddingTop(),
getPaddingBottom());
}
/*
* 设置当前布局的大小
* */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 取容器宽高的最小值。
mWidth = Math.min(getMeasuredHeight(), getMeasuredWidth());
// 如果once没有发生过。
if (!once)
{
// 进行切图以及排序。
initBitmap();
// 设置ImageView(Item)的宽高等属性。
initItem();
// 判断是否开启时间。
checkTimeEnable();
once = true;
}
// 重置它占据的位置是一个正方形。
setMeasuredDimension(mWidth, mWidth);
}
/**
* 判断是否开启时间。
* */
private void checkTimeEnable()
{
// 如果开启了,
if (isTimeEnabled)
{
// 根据关卡设置时间长短。
countTimeBaseLevel();
// 通知主界面显示时间。
mHandler.sendEmptyMessage(TIME_CHANGED);
}
}
// 根据关卡设置时间长短。
private void countTimeBaseLevel()
{
// 用pow可以根据游戏难度增加时间,2的mLevel次,指数增长。
mTime = (int) Math.pow(2, mLevel) * 60;
}
/**
* 进行切图,已经排序。
*/
private void initBitmap()
{
if (mBitmap == null)
{
// 获取Bitmap图片对象
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.image);
}
// 进行切割后返回ImagePiece的List。
mItemBitmaps = ImageSplitterUtil.splitImage(mBitmap, mColumn);
// 使用sort乱序排序图片。重写compare方法。
Collections.sort(mItemBitmaps, new Comparator<ImagePiece>()
{
@Override
public int compare(ImagePiece a, ImagePiece b)
{
// 使用随机数。
return Math.random() > 0.5 ? 1 : -1;
}
});
}
/**
* 设置ImageView(Item)的宽高等属性。
*/
private void initItem()
{
// 获取item宽度。(容器宽度-最左右的空隙-中间图片之间的空隙)/ 一行Item的数目。
mItemWidth = (mWidth - mPadding * 2 - mMargin * (mColumn - 1))
/ mColumn;
// 这个大小是随程序的mColumn动态创建的。多个ImageView。
mGamePintuItems = new ImageView[mColumn * mColumn];
// 生成Item,设置Rule。
for (int i = 0; i < mGamePintuItems.length; i++)
{
// 生产一个一个的ImageView。mItemBitmaps是bitmap的List
ImageView item = new ImageView(getContext());
item.setOnClickListener(this);
item.setImageBitmap(mItemBitmaps.get(i).getBitmap());
mGamePintuItems[i] = item;
item.setId(i + 1);
// 在item的tag中存储了index,index存储的是真正的顺序。
item.setTag(i + "_" + mItemBitmaps.get(i).getIndex());
/**
* 排列顺序
* 0 1 2 3
* 4 5 6 7
* 8 9 10 11
* */
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
mItemWidth, mItemWidth);
// 不是最后一列的列设置右边距,通过rightMargin
if ((i + 1) % mColumn != 0)
{
lp.rightMargin = mMargin;
}
// 不是第一列
if (i % mColumn != 0)
{
// 就是设置它是在谁的右边排列,1 rightof 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());
}
// 给这个item设置它的参数。
addView(item, lp);
}// for
}
public void restart()
{
isGameOver = false;
mColumn--;
nextLevel();
}
private boolean isPause ;
public void pause()
{
isPause = true ;
mHandler.removeMessages(TIME_CHANGED);
}
public void resume()
{
if(isPause)
{
isPause = false ;
mHandler.sendEmptyMessage(TIME_CHANGED);
}
}
/**
* 下一关要做的事。
* */
public void nextLevel()
{
// 取消当前容器中的所有view
this.removeAllViews();
// 取消动画层
mAnimLayout = null;
// 图片碎片变复杂。
mColumn++;
isGameSuccess = false;
checkTimeEnable();
// 重新显示新的图片。
initBitmap();
initItem();
}
/**
* 取多个参数的最小值。
*/
private int min(int... params)
{
int min = params[0];
for (int param : params)
{
if (param < min)
min = param;
}
return min;
}
// 两个被点击的图片。
private ImageView mFirst;
private ImageView mSecond;
@Override
public void onClick(View v)
{
if (isAniming)
return;
// 如果两次点击了相同的图片,代表取消第一个点击,也就去掉点中状态。
if (mFirst == v)
{
mFirst.setColorFilter(null);
mFirst = null;
return;
}
// 如果第一张null,说明此时点的这张图片是第一张图片,否则是第二张图片。
if (mFirst == null)
{
mFirst = (ImageView) v;
// 点中后有一个被点中的状态,这里设置透明的红色。55代表透明度,后面是八位颜色。
mFirst.setColorFilter(Color.parseColor("#55FF0000"));
} else
{
mSecond = (ImageView) v;
// 交换item。
exchangeView();
}
}
/**
* 动画层
*/
private RelativeLayout mAnimLayout;
// 正在动画的时候,就不让用户乱点一通。
private boolean isAniming;
/**
* 交换item
*/
private void exchangeView()
{
// 先去掉点中状态。
mFirst.setColorFilter(null);
// 准备动画层
setUpAnimLayout();
// 复制我们点中的图片。
ImageView first = new ImageView(getContext());
// mItemBitmaps是存放所有的图片碎片的list,它是被乱序排过的。
final Bitmap firstBitmap = mItemBitmaps.get(
getImageIdByTag((String) mFirst.getTag())).getBitmap();
first.setImageBitmap(firstBitmap);
// 要看看是不是RelativeLayout的LayoutParam,或者就是直接写出Relative前缀来。
LayoutParams lp = new LayoutParams(mItemWidth, mItemWidth);
// 减去mPadding是因为,对于底下原本的那层,它的左边是有边距的,
// 而对于上面的动画层,它的整个大小是没有覆盖下面的边距的,这样它里面的元素也就不需要考虑下面的它说拥有的padding
lp.leftMargin = mFirst.getLeft() - mPadding;
lp.topMargin = mFirst.getTop() - mPadding;
first.setLayoutParams(lp);
// 加到动画层中。
mAnimLayout.addView(first);
// 复制我们点中的第二张图片。
ImageView second = new ImageView(getContext());
final Bitmap secondBitmap = mItemBitmaps.get(
getImageIdByTag((String) mSecond.getTag())).getBitmap();
second.setImageBitmap(secondBitmap);
LayoutParams lp2 = new LayoutParams(mItemWidth, mItemWidth);
lp2.leftMargin = mSecond.getLeft() - mPadding;
lp2.topMargin = mSecond.getTop() - mPadding;
second.setLayoutParams(lp2);
mAnimLayout.addView(second);
// 设置动画用这个TranslateAnimation。
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,
-mSecond.getLeft() + mFirst.getLeft(), 0, -mSecond.getTop()
+ mFirst.getTop());
animSecond.setDuration(300);
animSecond.setFillAfter(true);
second.startAnimation(animSecond);
// 监听动画
anim.setAnimationListener(new AnimationListener()
{
@Override
public void onAnimationStart(Animation animation)
{
// 先把下面的那两个隐藏。
mFirst.setVisibility(View.INVISIBLE);
mSecond.setVisibility(View.INVISIBLE);
isAniming = true;
}
@Override
public void onAnimationRepeat(Animation animation)
{
// TODO Auto-generated method stub
}
@Override
public void onAnimationEnd(Animation animation)
{
String firstTag = (String) mFirst.getTag();
String secondTag = (String) mSecond.getTag();
// 交换bitmap和tag
mFirst.setImageBitmap(secondBitmap);
mSecond.setImageBitmap(firstBitmap);
mFirst.setTag(secondTag);
mSecond.setTag(firstTag);
// 结束的时候再把下面的两个图片显示出来。
mFirst.setVisibility(View.VISIBLE);
mSecond.setVisibility(View.VISIBLE);
mFirst = mSecond = null;
// 最后把动画层的东西都消掉
mAnimLayout.removeAllViews();
// 判断用户游戏是否成功
checkSuccess();
isAniming = false;
}
});
}
/**
* 判断用户游戏是否成功
*/
private void checkSuccess()
{
boolean isSuccess = true;
for (int i = 0; i < mGamePintuItems.length; i++)
{
ImageView imageView = mGamePintuItems[i];
if (getImageIndexByTag((String) imageView.getTag()) != i)
{
isSuccess = false;
}
}
if (isSuccess)
{
// 当前一次游戏结束以后,把上一次的Handler动作取消掉。
isGameSuccess = true;
mHandler.removeMessages(TIME_CHANGED);
Toast.makeText(getContext(), "Success,level up !!!",
Toast.LENGTH_LONG).show();
mHandler.sendEmptyMessage(NEXT_LEVEL);
}
}
/**
* 通过tag获取image的id,也就是当初设置的乱排序以后对应的i值。
* tag中包含i和index,split[0]就是i。
* @param tag
* @return
*/
public int getImageIdByTag(String tag)
{
String[] split = tag.split("_");
return Integer.parseInt(split[0]);
}
/**
* 根据tag获取index。
* */
public int getImageIndexByTag(String tag)
{
String[] split = tag.split("_");
return Integer.parseInt(split[1]);
}
/**
* 构造动画层。
*/
private void setUpAnimLayout()
{
if (mAnimLayout == null)
{
mAnimLayout = new RelativeLayout(getContext());
// 加到面板之中。
addView(mAnimLayout);
}
}
}
package com.imooc.game.pintu;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.widget.TextView;
import com.imooc.game.pintu.view.GamePintuLayout;
import com.imooc.game.pintu.view.GamePintuLayout.GamePintuListener;
public class MainActivity extends Activity
{
private GamePintuLayout mGamePintuLayout;
private TextView mLevel ;
private TextView mTime;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTime = (TextView) findViewById(R.id.id_time);
mLevel = (TextView) findViewById(R.id.id_level);
mGamePintuLayout = (GamePintuLayout) findViewById(R.id.id_gamepintu);
// 这个这个
mGamePintuLayout.setTimeEnabled(true);
// 哇,这个是我们自己写的。
mGamePintuLayout.setOnGamePintuListener(new GamePintuListener()
{
@Override
public void timechanged(int currentTime)
{
// 转为字符串的形式
mTime.setText(""+currentTime);
}
@Override
public void nextLevel(final int nextLevel)
{
// 到下一关的时候弹出dialog框让用户选择是否进行下一关。
new AlertDialog.Builder(MainActivity.this)
.setTitle("Game Info").setMessage("LEVEL UP !!!")
.setPositiveButton("NEXT LEVEL", new OnClickListener()
{
@Override
public void onClick(DialogInterface dialog,
int which)
{
mGamePintuLayout.nextLevel();
// 转为字符串的形式。
mLevel.setText(""+nextLevel);
}
}).show();
}
@Override
public void gameover()
{
new AlertDialog.Builder(MainActivity.this)
.setTitle("Game Info").setMessage("Game over !!!")
.setPositiveButton("RESTART", new OnClickListener()
{
@Override
public void onClick(DialogInterface dialog,
int which)
{
mGamePintuLayout.restart();
}
}).setNegativeButton("QUIT",new OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
finish();
}
}).show();
}
});
}
@Override
protected void onPause()
{
super.onPause();
mGamePintuLayout.pause();
}
@Override
protected void onResume()
{
super.onResume();
mGamePintuLayout.resume();
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.imooc.game.pintu.view.GamePintuLayout
android:id="@+id/id_gamepintu"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerInParent="true"
android:padding="3dp" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="@id/id_gamepintu"
android:layout_margin="3dp" >
<TextView
android:id="@+id/id_level"
android:layout_width="70dp"
android:layout_height="70dp"
android:background="@drawable/textbg"
android:gravity="center"
android:padding="4dp"
android:text="1"
android:textColor="#EA7821"
android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:id="@+id/id_time"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_alignParentRight="true"
android:background="@drawable/textbg"
android:gravity="center"
android:padding="4dp"
android:text="120"
android:textColor="#EA7821"
android:textSize="30sp"
android:textStyle="bold" />
</RelativeLayout>
</RelativeLayout>
其中的textbg.xml文件放在drawable文件下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<stroke
android:width="2px"
android:color="#1579DB" />
<solid android:color="#B4CDE6" />
</shape>