相信大家在小的时候都玩过拼图游戏,现如今,手机普及,能在手机上玩的游戏越来越多,于是乎,重温小时候,编写这个简易拼图游戏,而且也能进一步加深Android的一些基础知识。
老规矩,先是效果图:
这里我把为了演示效果,把图片打乱的很少,在代码里可以更改。
首先,有个默认的图片,可以用来拼图,也可以选择你喜欢的图片进行拼图,拼图的过程会记录移动的步数,并且当游戏胜利的时候会弹出一个笑脸提示,游戏胜利,用了多少步数。
ps:感兴趣的完全可以继续在这上面进行扩展,比如增加游戏难度的选项,可以将图片分成更多的小方块等等。
大体思路:将大图切割成各个小方块,用数组记录每个小方块的信息,用GridLayout来显示各个小方块,并且将某个小方块标记为空白方块(空白方块可以和相邻方块进行交换),在GridLayout上的各个小方块上增加点击事件和在整个屏幕上添加手势事件,每次点击或者有手势时,判断小方块是否能移动,最后在游戏胜利时弹出胜利提示。
话不多说,接下来,就是一步步实现拼图游戏的过程啦。
1.小方块相关的类。
这是小方块的各种变量的item,类,用来管理将大图切割成每个小方块的每个小方块的信息。很简单,就是各种变量和Setter和Getter方法直接上代码~
public class GameItemView{
//每个小方块的实际位置x,
private int x=0;
//每个小方块的实际位置y,
private int y=0;
//每个小方块的图片,
private Bitmap bm;
//每个小方块的图片位置x,
private int p_x=0;
//每个小方块的图片位置y.
private int p_y=0;
public GameItemView(int x, int y, Bitmap bm) {
super();
this.x = x;
this.y = y;
this.bm = bm;
this.p_x=x;
this.p_y=y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public Bitmap getBm() {
return bm;
}
public void setBm(Bitmap bm) {
this.bm = bm;
}
public int getP_x() {
return p_x;
}
public void setP_x(int p_x) {
this.p_x = p_x;
}
public int getP_y() {
return p_y;
}
public void setP_y(int p_y) {
this.p_y = p_y;
}
public boolean isTrue(){
if (x==p_x&&y==p_y){
return true;
}
return false;
}
}
2.主界面的布局
主界面很简单,一个Button,用来换图片,一个ImageView,用来显示原图,一个GridLayout用来进行拼图游戏,最后,一个TextView,用来显示完成这个拼图用了多少步数。布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:id="@+id/ll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/bt_choice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="选择图片"
android:adjustViewBounds="true"
/>
</LinearLayout>
<ImageView
android:id="@+id/iv"
android:layout_below="@id/ll"
android:adjustViewBounds="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/haizei"
android:layout_marginTop="3dp"
></ImageView>
<!-- 游戏的主界面-->
<GridLayout
android:layout_marginTop="3dp"
android:layout_below="@id/iv"
android:id="@+id/gl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="5"
android:rowCount="3"
android:adjustViewBounds="true"
>
</GridLayout>
<TextView
android:id="@+id/tv_step"
android:layout_below="@id/gl"
android:layout_marginTop="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="已用步数:0"
android:textSize="26sp"
/>
</RelativeLayout>
3.打开图片选择图片
给Button设置点击事件,调用startActivityForResult(Intent intent,int requestCode);方法,来获取图片。
bt_choice.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent= new Intent("android.intent.action.GET_CONTENT");
intent.setType("image
@TargetApi(Build.VERSION_CODES.KITKAT)
private void handleImageOnKitKat(Intent data) {
String imagePath=null;
Uri uri=data.getData();
if (DocumentsContract.isDocumentUri(this,uri)){
//如果是document类型的url,则通过document的id处理。
String docId=DocumentsContract.getDocumentId(uri);
if ("com.android.providers.media.documents".equals(uri.getAuthority())){
String id =docId.split(":")[1];//解析出数字格式的id;
String selection= MediaStore.Images.Media._ID+"="+id;
imagePath=getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,selection);
}else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())){
Uri contenturi= ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(docId));
imagePath=getImagePath(contenturi,null);
}
}else if ("content".equalsIgnoreCase(uri.getScheme())){
//如果不是document类型的uri,则使用普通的方式处理。
imagePath=getImagePath(uri,null);
}
displayImage(imagePath);
}
private void displayImage(String imagePath) {
if (imagePath != null) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
if (isHeigthBigWidth(bitmap)) {
Bitmap bt = rotaingImageView(bitmap);//将图片旋转90度。
Bitmap disbitmapt = ajustBitmap(bt);
photo.setImageBitmap(disbitmapt);
} else {
Bitmap disbitmap = ajustBitmap(bitmap);
photo.setImageBitmap(disbitmap);
}
}
}
private Bitmap rotaingImageView(Bitmap bitmap) {
//旋转图片 动作
Matrix matrix = new Matrix();;
matrix.postRotate(270);
// 创建新的图片
Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix, true);
return resizedBitmap;
}
private String getImagePath(Uri externalContentUri, String selection) {
String path=null;
Cursor cursor=getContentResolver().query(externalContentUri, null, selection, null, null);
if (cursor!=null){
if (cursor.moveToFirst()){
path=cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
}
cursor.close();
return path;
}
4.拼图的各个小方块的形成过程。
看着各个小方块,我们用GridLayout来实现是最为方便的。所以,用一个GridLayout来显示大图切割后的各个小方块,用一个ImageView数组来保存各个小方块的信息,并且,我们默认把最后一个小方块设置为空白方块。
首先是各种需要的变量。注释很清楚。
private ImageView [][] iv_game_arr=new ImageView[3][5];
private GridLayout gl_game_layout;
//小方块的行和列
private int i;
private int j;
private ImageView iv_null_imagview;
接着是从Imageview获取图片,并且将图片按照一定的行和列进行切割(这里将拼图设置为3行5列)。将切割后的各个小方块的信息保存在一个ImageView数组中。给每个小方块设置Tag,和点击监听。
private void setGameItem() {
//调整图片的尺寸
Bitmap abitmap=ajustBitmap(bt_tupan);
int ivWidth=getWindowManager().getDefaultDisplay().getWidth()/5;//每个游戏小方块的宽和高。切成正方形
int tuWidth=abitmap.getWidth()/5;
for (int i=0;i<iv_game_arr.length;i++){
for (int j=0;j<iv_game_arr[0].length;j++){
//将大图切成小方块
Bitmap bm=Bitmap.createBitmap(abitmap,j*tuWidth,i*tuWidth,tuWidth,tuWidth);
iv_game_arr[i][j]=new ImageView(this);
iv_game_arr[i][j].setImageBitmap(bm);//设置每一个小方块的图案
iv_game_arr[i][j].setLayoutParams(new RelativeLayout.LayoutParams(ivWidth, ivWidth));
//设置方块之间的间距
iv_game_arr[i][j].setPadding(2, 2, 2, 2);
iv_game_arr[i][j].setTag(new GameItemView(i, j, bm)); //绑定自定义数据
iv_game_arr[i][j].setOnClickListener(new View.OnClickListener() {
.......
);
当然,我们选择的图片不可能都是符合标准的大小的,所以,在切割图片之前我们要对图片进行调整。将图片调整为5:3的比例。(这样切割成3行5列的小方块才能正确切割)这里对于width,我把每个小方块的间隔事先也算到里面去。
//调整图片的大小
private Bitmap ajustBitmap(Bitmap bitmap) {
int width=getWindowManager().getDefaultDisplay().getWidth()-(iv_game_arr[0].length-1)*2;
int heigth=width/5*3;
Bitmap scaledBitmap=Bitmap.createScaledBitmap(bitmap, width, heigth, true);
return scaledBitmap;
}
将每个小方格放入到GridLayout中。
private void startGame() {
tv_step.setText("已用步数:0");
for (i = 0; i <iv_game_arr.length; i++){
for (j = 0; j <iv_game_arr[0].length; j++){
gl_game_layout.addView(iv_game_arr[i][j]);
}
}
//将最后一个方块设置为设置空方块。
setNullImageView(iv_game_arr[i-1][j-1]);
5.小方块的点击事件和手势判断过程。
这里是拼图游戏的核心,弄懂了小方块的移动变化规律,也就弄懂了拼图游戏。
对于点击事件,首先拿到被点击的小方块的各种信息(位置、图案)和空白小方块的位置信息,判断被点击的小方块是否和空白小方块相邻,如果相邻,就移动交换数据(用TranslateAnimation来实现移动动画),如果不相邻则无操作。
a.判断点击方块和空白方块是否相邻的方法
public boolean isAdjacentNullImageView(ImageView imageView){
//获取当前空方块的位置与点击方块的位置
GameItemView null_gameItemView= (GameItemView) iv_null_imagview.getTag();
GameItemView now_gameItem_view = (GameItemView) imageView.getTag();
if(null_gameItemView.getY()==now_gameItem_view.getY()&&now_gameItem_view.getX()+1==null_gameItemView.getX()){//当前点击的方块在空方块的上面
return true;
}else if(null_gameItemView.getY()==now_gameItem_view.getY()&&now_gameItem_view.getX()==null_gameItemView.getX()+1){//当前点击的方块在空方块的下面
return true;
}else if(null_gameItemView.getY()==now_gameItem_view.getY()+1&&now_gameItem_view.getX()==null_gameItemView.getX()){//当前点击的方块在空方块的左面
return true;
}else if(null_gameItemView.getY()+1==now_gameItem_view.getY()&&now_gameItem_view.getX()==null_gameItemView.getX()){ ////当前点击的方块在空方块的右面
return true;
}
return false;
}
b.接着是如果相邻就进入方块数据交换的方法
这里有个方法重载,是否需要动画效果,没有动画效果的数据交换是为初始化游戏时打乱拼图做准备的。这里将核心交换代码列出。每次交换后还要判断是否游戏胜利(即是否拼图完成~)。
//得到点击方块绑定的数据
GameItemView gameItemView = (GameItemView) itemimageView.getTag();
//将空方块的图案设置为点击方块
iv_null_imagview.setImageBitmap(gameItemView.getBm());
//得到空方块绑定的数据
GameItemView null_gameItemView = (GameItemView) iv_null_imagview.getTag();
//交换数据(将点击方块的数据传入空方块)
null_gameItemView.setBm(gameItemView.getBm());
null_gameItemView.setP_x(gameItemView.getP_x());
null_gameItemView.setP_y(gameItemView.getP_y());
//设置当前点击的方块为空方块。
setNullImageView(itemimageView);
if (isStart){
isGameWin();//成功时,会弹一个吐司。
}
c.交换时的动画设置
交换设置动画时,首先判断移动的方向,根据方向设置不同的移动动画,然后再监听动画完成后,进行数据交换操作。即上面b.接着是如果相邻就进入方块数据交换的方法.最后执行动画。
//1.创建一个动画,设置方向,移动的距离
//判断方向,设置动画
if (itemimageView.getX()>iv_null_imagview.getX()){//当前点击的方块在空方块的上面
//下移
translateAnimation = new TranslateAnimation(0.1f,-itemimageView.getWidth(),0.1f,0.1f);
}else if (itemimageView.getX()<iv_null_imagview.getX()){//当前点击的方块在空方块的下面
//上移
boolean f=itemimageView.getX()<iv_null_imagview.getX();
//Log.i("点击方块","sssssssssssssssssssssssss"+f);
translateAnimation = new TranslateAnimation(0.1f,itemimageView.getWidth(),0.1f,0.1f);
}else if (itemimageView.getY()>iv_null_imagview.getY()){//当前点击的方块在空方块的左面
//右移
translateAnimation=new TranslateAnimation(0.1f,0.1f,0.1f,-itemimageView.getWidth());
}else if(itemimageView.getY()<iv_null_imagview.getY()){//当前点击的方块在空方块的右面
//左移
translateAnimation=new TranslateAnimation(0.1f,0.1f,0.1f,itemimageView.getWidth());
}
//2.设置动画的各种参数
translateAnimation.setDuration(80);
translateAnimation.setFillAfter(true);
//3.设置动画的监听
translateAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
isAminMove=true;
}
@Override
public void onAnimationEnd(Animation animation) {
//动画结束,交换数据
......
}
//动画执行
itemimageView.startAnimation(translateAnimation);
点击事件的流程就完了,接下来是手势判断的事件。即不仅可以通过点击小方块进行移动,也可以通过手势移动小方块。
One.创建手势对象
在onFling方法中完成手势相关的操作。
//创建手势对象
gestureDetector =new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//手势相关的操作
......
}
接着我们onFling方法中做具体操作
Two.判断手势移动的方向
根据返回值的不同得到不同的移动方向。
public int getDirctionByGesure(float start_x,float start_y,float end_x,float end_y){
boolean isLeftOrRight =(Math.abs(end_x-start_x)>Math.abs(end_y-start_y))?true:false; //是否是左右
if(isLeftOrRight){//左右
boolean isLeft=(end_x-start_x)>0?false:true;
if(isLeft){
return 3;
}else {
return 4;
}
}else{//上下
boolean isUp=(end_y-start_y)>0?false:true;
if (isUp){
return 1;
}else {
return 2;
}
}
}
Three.根据空方块和移动的方向,判断能否移动以及进行移动操作。
因为是手势,移动的肯定是空方块周围的方块,所以重点就是要判断空方块在要移动的方块的那个方向,再根据方向判断能否移动,进行移动操作。(其中changeDateByImageView()中的方法就是具体的方块交换数据及移动的操作。就是点击事件的那个方法。)
public void changeByDirGes(int type,boolean isAnim){
//1.获取当前空方块的位置。
GameItemView null_gameItemView= (GameItemView) iv_null_imagview.getTag();
int new_x=null_gameItemView.getX();
int new_y=null_gameItemView.getY();
//2.根据方向,设置相应相邻的位置坐标。
if (type==1){//说明空方块在要移动的方块的上面。
new_x++;
}else if (type==2){//空方块在要移动的方块的下面
new_x--;
}else if (type==3){//空方块在要移动的方块的左面
new_y++;
}else if (type==4){//空方块在要移动的方块的右面
new_y--;
}
//3.判断这个新坐标是否存在
if(new_x>=0&&new_x<iv_game_arr.length&&new_y>=0&&new_y<iv_game_arr[0].length){
//存在,可以移动交换数据
if(isAnim){//有动画
changeDateByImageView(iv_game_arr[new_x][new_y]);
}else{
changeDateByImageView(iv_game_arr[new_x][new_y],isAnim);
}
}else{
//什么也不做
}
}
好了,手势事件也就大功告成了~
当然这里有两个要注意的地方。1.首先要对当前的Activity设置onTouchEvent()方法,将onTouch事件交由手势去处理,其次也要设置dispatchTouchEvent()方法,在里面也要向下分发给手势事件,如果不设置向下分发给手势判断,那么在GridLayout里,就只能触发点击事件而手势事件就不会起作用了。2.要增加一个是否在移动过程中的flag,如果在移动过程中,就什么也不做,要不然每次点击小方块即使在移动过程中,也会触发点击事件从而又进行动画移动,造成不好的用户体验。
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
gestureDetector.onTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
6.游戏开始打乱方块以及游戏结束时弹出Toast提示的方法。
代码很简单,直接上代码,其中,弹出的Toast是一个带有自定义View动画的Toast.
//随机打乱图片的顺序
public void randomOrder(){
//打乱的次数,为了测试方便,设置很小。
for (int i=0;i<5;i++){
//根据手势,交换数据,无动画。
int type = (int) (Math.random()*4)+1;
// Log.i("sssssssssfdfdfd","交换次数"+i+"type的值"+type);
changeByDirGes(type, false);
}
}
public void isGameWin(){
//游戏胜利标志
boolean isGameWin =true;
//遍历每个小方块
for (i = 0; i <iv_game_arr.length; i++){
for (j = 0; j <iv_game_arr[0].length; j++){
//为空的方块不判断 跳过
if (iv_game_arr[i][j]==iv_null_imagview){
continue;
}
GameItemView gameItemView= (GameItemView) iv_game_arr[i][j].getTag();
if (!gameItemView.isTrue()){
isGameWin=false;
//跳出内层循环
break;
}
}
if (!isGameWin){
//跳出外层循环
break;
}
}
//根据一个开关变量觉得游戏是否结束,结束时给提示。
if (isGameWin){
// Toast.makeText(this,"游戏胜利",Toast.LENGTH_SHORT).show();
ToastUtil.makeText(this,"恭喜你,游戏胜利,用了"+step+"步",ToastUtil.LENGTH_SHORT,ToastUtil.SUCCESS);
step=0;
}
}
好了,重要的部分都已经完了,这里还有个自定义View的Toast,关于Toast的详解,将在下篇文章,这里先简单说明下自定义Toast的实现过程。
首先,新建一个SuccessToast类,(笑脸包括左眼,有眼,笑脸弧)。我们将核心的过程给出。利用动画,实现动态笑脸画制过程。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setStyle(Paint.Style.STROKE);
//话笑脸弧
canvas.drawArc(rectF, 180, endAngle, false, mPaint);
mPaint.setStyle(Paint.Style.FILL);
if (isSmileLeft) {
//如果是左眼,画左眼
canvas.drawCircle(mPadding + mEyeWidth + mEyeWidth / 2, mWidth / 3, mEyeWidth, mPaint);
}
if (isSmileRight) {
//如果是有眼,画右眼。
canvas.drawCircle(mWidth - mPadding - mEyeWidth - mEyeWidth / 2, mWidth / 3, mEyeWidth, mPaint);
}
}
private ValueAnimator startViewAnim(float startF, final float endF, long time) {
//设置valueAnimator 的起始值和结束值。
valueAnimator = ValueAnimator.ofFloat(startF, endF);
//设置动画时间
valueAnimator.setDuration(time);
//设置补间器。控制动画的变化速率
valueAnimator.setInterpolator(new LinearInterpolator());
//设置监听器。监听动画值的变化,做出相应方式。
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatedValue = (float) valueAnimator.getAnimatedValue();
//如果value的值小于0.5
if (mAnimatedValue < 0.5) {
isSmileLeft = false;
isSmileRight = false;
endAngle = -360 * (mAnimatedValue);
//如果value的值在0.55和0.7之间
} else if (mAnimatedValue > 0.55 && mAnimatedValue < 0.7) {
endAngle = -180;
isSmileLeft = true;
isSmileRight = false;
//其他
} else {
endAngle = -180;
isSmileLeft = true;
isSmileRight = true;
}
//重绘
postInvalidate();
}
});
if (!valueAnimator.isRunning()) {
valueAnimator.start();
}
return valueAnimator;
}
然后新建一个success_toast_layout.xml,完成toast的布局。布局是左右(左边笑脸view,右边TextView的提示。)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00000000"
android:orientation="vertical">
<LinearLayout
android:id="@+id/base_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="25dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_marginTop="25dp"
android:background="@drawable/background_toast"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<com.example.yyh.puzzlepicture.activity.Util.SuccessToast
android:id="@+id/successView"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_vertical|left"
android:layout_margin="10px"
android:gravity="center_vertical|left" />
</LinearLayout>
<TextView
android:id="@+id/toastMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="10dp"
android:text="New Text" />
</LinearLayout>
</LinearLayout>
最后新建一个ToastUtil类,管理自定义的Toast.
public class ToastUtil {
public static final int LENGTH_SHORT = 0;
public static final int LENGTH_LONG = 1;
public static final int SUCCESS = 1;
static SuccessToast successToastView;
public static void makeText(Context context, String msg, int length, int type) {
Toast toast = new Toast(context);
switch (type) {
case 1: {
View layout = LayoutInflater.from(context).inflate(R.layout.success_toast_layout, null, false);
TextView text = (TextView) layout.findViewById(R.id.toastMessage);
text.setText(msg);
successToastView = (SuccessToast) layout.findViewById(R.id.successView);
successToastView.startAnim();
text.setBackgroundResource(R.drawable.success_toast);
text.setTextColor(Color.parseColor("#FFFFFF"));
toast.setView(layout);
break;
}
}
toast.setDuration(length);
toast.show();
}
}
这样就可以在ManiActivity中调用这个自定义Toast了。
好了,完结。
游戏源码:拼图游戏的实现过程
gitHub:拼图游戏。