自定义View实现2048
一直觉得能写游戏的都是大神!因为学习方向以及时间的问题,很少动手开发游戏。在校的时候,记得写过当时很火的游戏“像素鸟”,哈哈,作为菜鸟来说还是挺有成就感的!进入正题,本文主要从自定义view,以及自定义layout来实现2048游戏。
思路:
1. 首先,当然是将游戏的所有格子画出来。这里,定义N,表示N行N列,即N*N个格子可以移动。每一个方块为一个自定义的GameItem。方块的长宽由layout决定。
2. 自定义Layout,用于绘制所有方块,以及相应滑动监听。这是最主要的一部分,涉及到具体的算法。
3. 上面的两个步骤实质上定义了view,当然需要主程序跑起来啰。设置游戏结束以及得分的监听接口。
自定义GameItem
每一个方块都是一个正方形,根据不同数字绘制方块的背景色,如果数字不为零,则绘制数字。比较简单,详见代码:
package com.example.huangzheng.game;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;/*** Created by huangzheng on 2017/11/20.*/public class GameItem extends View {private int mNumber;private String mNumberVal;private Paint mPaint;private Rect mRect;//绘制文字区域public GameItem(Context context) {this(context,null);}public GameItem(Context context, @Nullable AttributeSet attrs) {this(context, attrs,0);}public GameItem(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mPaint = new Paint();}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);String mBgColor = "#CCC0B3";switch (mNumber){case 0:mBgColor = "#CCC0B3";break;case 2:mBgColor = "#EEE4DA";break;case 4:mBgColor = "#EDE0C8";break;case 8:mBgColor = "#F2B179";break;case 16:mBgColor = "#F49563";break;case 32:mBgColor = "#F5794D";break;case 64:mBgColor = "#F55D37";break;case 128:mBgColor = "#EEE863";break;case 256:mBgColor = "#EDB04D";break;case 512:mBgColor = "#ECB04D";break;case 1024:mBgColor = "#EB9437";break;case 2048:mBgColor = "#EA7821";break;default:mBgColor = "#EA7821";break;}mPaint.setColor(Color.parseColor(mBgColor));mPaint.setStyle(Paint.Style.FILL);canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);//宽高由layout决定if (mNumber != 0){drawText(canvas);}}private void drawText(Canvas mCanvas){mPaint.setColor(Color.BLACK);float x = (getWidth() - mRect.width()) / 2;float y = (getHeight() + mRect.height()) / 2;//值得注意的是,y是text的下边际,x为起始位置mCanvas.drawText(mNumberVal,x,y,mPaint);}public void setNumber(int number){this.mNumber = number;mNumberVal = mNumber + "";mPaint.setTextSize(30.0f);mRect = new Rect();mPaint.getTextBounds(mNumberVal, 0, mNumberVal.length(), mRect);invalidate();}public int getNumber(){return mNumber;}
}
自定义Layout
重要的部分来了!整体思路:
- 获取布局的长宽,在根据方块的行列数绘制所有初始方块,并随机将一个方块的值设为2;
- 按键监听用户的上向左右滑动事件,对每行每列的方块进行重新的排列并重新绘制
- 游戏结束的判断
先上代码,再庖丁解牛。
package com.example.huangzheng.game;import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.RelativeLayout;import java.util.ArrayList;
import java.util.List;
import java.util.Random;/*** Created by huangzheng on 2017/11/22.*/public class GameLayout extends RelativeLayout {private final static String TAG = "GameLayout";private int mN = 4; //n行n列private int mMargin = 3;//item间隔private int mItemSize;//方块边长private int mWidth;private int mHeight;private int mPinding;private int mScore = 0;private GameItem[] mGameItem;private GestureDetector mGestureDetector;private CallBackInterface mCallBack;private boolean mIsFirst = true;//是否第一次启动private boolean mIsMove = false;//是否发生了移动private boolean mIsMarge = false;//是否发生了合并/** 动作枚举*/private enum ACTION{UP,RIGHT,DOWN,LEFT}private final static float MIX_DISTANCE = 10;//滑动的有效距离public GameLayout(Context context) {this(context,null);}public GameLayout(Context context, AttributeSet attrs) {this(context, attrs,0);}public GameLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);/** px、dp的相互转换,* type1:需要转换的是dp or px* type2:具体值* type3:DisplayMetrics,屏幕信息类*/mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,mMargin, getResources().getDisplayMetrics());//获取边距mPinding = Math.min(getPaddingLeft(), getPaddingTop());//手势监听mGestureDetector = new GestureDetector(new MyGestureDetector());}//注册回调public void setRegister(CallBackInterface callBackInterface){this.mCallBack = callBackInterface;}//重新开始public void reStart(){//requestLayout();//执行onMeasure、onLayout、onDraw方法//invalidate();//只会执行onDraw方法for (GameItem item: mGameItem){item.setNumber(0);}mScore = 0;if (mCallBack != null){mCallBack.setScore(mScore);}getNewNumber();}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {Log.d(TAG,"onLayout");super.onLayout(changed, l, t, r, b);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {Log.d(TAG,"onMeasure");super.onMeasure(widthMeasureSpec, heightMeasureSpec);mWidth = getMeasuredWidth();mHeight = getMeasuredHeight();int lenght = Math.min(mWidth,mHeight);mItemSize = (lenght - mPinding * 2 - (mN - 1) * mMargin ) / mN;if (mIsFirst){if (mGameItem == null){mGameItem = new GameItem[mN * mN];}for (int i = 0; i < mGameItem.length; i++){GameItem item = new GameItem(getContext());mGameItem[i] = item;item.setId(i + 1);RelativeLayout.LayoutParams lp = new LayoutParams(mItemSize,mItemSize);//非最后一列if ((i + 1) % mN != 0){lp.rightMargin = mMargin;}//非第一列if (i % mN != 0){lp.addRule(RelativeLayout.RIGHT_OF,mGameItem[i -1].getId());}//非第一行if ((i + 1) > mN){lp.topMargin = mMargin;lp.addRule(RelativeLayout.BELOW,mGameItem[i - mN].getId());}addView(item,lp);}getNewNumber();}mIsFirst = false;setMeasuredDimension(lenght, lenght);}@Overrideprotected void onDraw(Canvas canvas) {Log.d(TAG,"onDraw");super.onDraw(canvas);}@Overridepublic boolean onTouchEvent(MotionEvent event) {mGestureDetector.onTouchEvent(event);return true;}private class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {//按下@Overridepublic boolean onDown(MotionEvent motionEvent) {return false;}//按下后没有松开或者拖动@Overridepublic void onShowPress(MotionEvent motionEvent) {}//轻触后松开@Overridepublic boolean onSingleTapUp(MotionEvent motionEvent) {return false;}//滑动@Overridepublic boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {return false;}//长按@Overridepublic void onLongPress(MotionEvent motionEvent) {}//快速移动(e1 滑动起点,e2 当前手势位置,Vx 每秒x轴移动像素,Vy每秒y轴方向移动像素)@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float Vx, float Vy) {float x = e2.getX() - e1.getX();float y = e2.getY() - e1.getY();if (x > MIX_DISTANCE && (Math.abs(Vx) > Math.abs(Vy))){doAction(ACTION.RIGHT);} else if (x < -MIX_DISTANCE && (Math.abs(Vx) > Math.abs(Vy))){doAction(ACTION.LEFT);} else if (y > MIX_DISTANCE && (Math.abs(Vx) < Math.abs(Vy))){doAction(ACTION.DOWN);} else if (y < -MIX_DISTANCE && (Math.abs(Vx) < Math.abs(Vy))){doAction(ACTION.UP);}return true;}}/** 手指移动时,四个方向的所有行都需要移动* 1、将每行的值取出并保存到数值* 2、根据手势对数组进行移动和合并(判断是否移动、合并)* 3、将新的数组放置到每行中*/private void doAction(ACTION action){Log.d(TAG,"doAction:" + action);for (int i = 0;i < mN; i++){List<GameItem> row = new ArrayList<GameItem>();//1、将每行的值取出并保存到数值for (int j = 0;j < mN; j++){int index = getIndexByAction(action,i,j);GameItem item = mGameItem[index];if (item.getNumber() != 0){//Log.d(TAG,"number:" + item.getNumber());row.add(item);}}//判断是否移动for (int j = 0;j < row.size();j++){int index = getIndexByAction(action,i,j);GameItem item = mGameItem[index];if (item.getNumber() != row.get(j).getNumber()){mIsMove = true;break;}}//2、根据手势对数组进行移动和合并row = doMerageItem(row);//3、将新的数组放置到每行中for (int j = 0; j < mN; j++){int index = getIndexByAction(action, i, j);if (row.size() > j){mGameItem[index].setNumber(row.get(j).getNumber());} else{mGameItem[index].setNumber(0);}}}getNewNumber();}private List<GameItem> doMerageItem(List<GameItem> row) {List<GameItem> backRow = new ArrayList<GameItem>();if (row.size() < 2){backRow = row;return backRow;}for (int j = 0;j < row.size() - 1;j++){GameItem item1 = row.get(j);GameItem item2 = row.get(j + 1);if (item1.getNumber() == item2.getNumber()){mIsMarge = true;int value = item1.getNumber() + item2.getNumber();item1.setNumber(value);item2.setNumber(0);//回调显示分数mScore += value;mCallBack.setScore(mScore);}}for (int j = 0;j < row.size();j++){if (row.get(j).getNumber() != 0){backRow.add(row.get(j));}}return backRow;}//根据action获取对应下标,如果为down right则反向储存private int getIndexByAction(ACTION action, int i, int j) {int index = 0;switch (action){case UP:index = j*mN + i;break;case DOWN:index = (mN-j-1)*mN + i;break;case LEFT:index = i*mN + j;break;case RIGHT:index = i*mN + (mN-j-1);break;}return index;}//随机生成数字private void getNewNumber(){if (isGameOver()){if (mCallBack != null){mCallBack.setGameOver();return;}}if (!isFull()){if (mIsMarge || mIsMove || mIsFirst){int n = mN * mN;Random random = new Random();int next = random.nextInt(n);GameItem item = mGameItem[next];while (item.getNumber() != 0){next = random.nextInt(n);item = mGameItem[next];}item.setNumber(2);mIsMarge = mIsMove = false;}}}//判断是否还有空格private boolean isFull(){boolean result = true;for (int i = 0;i < mN;i++){for (int j = 0;j < mN;j++){int index = i*mN + j;GameItem item = mGameItem[index];if (item.getNumber() == 0){return false;}}}return result;}//判断是否结束游戏(是否还有空格,如果无,是否相同数字)private boolean isGameOver(){boolean result = true;if (!isFull()){return false;}for (int i = 0;i < mN;i++){for (int j = 0;j < mN;j++){int index = i*mN + j;GameItem item = mGameItem[index];//上if (index - mN > -1){if (item.getNumber() == mGameItem[index - mN].getNumber()){return false;}}//下if (index + mN < mN*mN){if (item.getNumber() == mGameItem[index + mN].getNumber()){return false;}}//左if (index%mN !=0){if (item.getNumber() == mGameItem[index -1].getNumber()){return false;}}//右if ((index + 1)%mN !=0){if (item.getNumber() == mGameItem[index + 1].getNumber()){return false;}}}}return result;}
}
初始化所有方块
首先获取布局的长宽,从而计算出每个方块的边长;其次为每个方块设定位置约束规则;最后随机为某一方块赋值为“2”。
事件监听
1. 定义事件枚举,根据滑动前后的位置相应Up、Down、Left、Right事件。
2. 对每行每列进行排列重绘
通过两层循环,getIndexByAction(action,i,j)方法返回每一个方块的位置信息。
private int getIndexByAction(ACTION action, int i, int j) {int index = 0;switch (action){case UP:index = j*mN + i;break;case DOWN:index = (mN-j-1)*mN + i;break;case LEFT:index = i*mN + j;break;case RIGHT:index = i*mN + (mN-j-1);break;}return index;}
如果为Up、Left,则顺序获取;如果为Down、Right则逆向获取。等等,就猜到你会问为什么!这样做的目的是为了方便我们后面的每行或没列的合并。举个例子:假如现在有第一行数据,2 2 4 4,如果此时相应Left事件,返回的位置id为0,1,2,3,数据合并后的值为4 8 0 0 ,则按位置信息放入行;如果响应的是Right事件,返回的位置id为3,2,1,0,因此返回的数据为4 4 2 2 ,合并后的数据为8 4 0 0 ,最后将合并后的值赋值给获取到的逆向id,为0 0 4 8。有点绕,拿张纸,画一画规律就好理解了!
关于数值的合并:将通过位置id获取的数值存储到列表row中,通过一层for循环对相同的数进行合并,第一个数设置数值为合并后的值,第二个数设置数值为0.
在合并的过程中,如果有合并,则mIsMarge为true;如果没有合并,但是有移动,mIsMove为true。合并或者移动结束后,如果mIsMove或者mIsMarge为true,则随机为某一空白方块赋值2。
游戏结束判断
条件:1、没有数值为0的方块;2、没有相连方块的数值相同。同时满足两个条件,则游戏结束。
应用View
有了上面的准备工作,我们只需要将GameLayout当做类似TextView的组件使用就可以了。采用回调机制,更新分数以及响应游戏结束。
回调接口:
public interface CallBackInterface {void setScore(int score);void setGameOver();
}
MainActivity:
package com.example.huangzheng.game;import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;public class MainActivity extends AppCompatActivity implements CallBackInterface{private TextView mScore;private GameLayout mGameLayout;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mScore = (TextView) findViewById(R.id.sorce);mGameLayout = (GameLayout) findViewById(R.id.gameLayout);mGameLayout.setRegister(this);}@Overridepublic void setScore(int score) {mScore.setText("Score: " + score);}@Overridepublic void setGameOver() {new AlertDialog.Builder(this).setTitle("GAME OVER").setMessage("Do you want to try again?").setPositiveButton("Yes", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int i) {mGameLayout.reStart();}}).setNegativeButton("No", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int i) {finish();}}).show();}
}
哈哈,写游戏还是挺有成就感的!到这里,我的2048就可以跑起来了。因为代码中注释的都比较清楚,所以具体的细节就没有写出来,只是从总体思路进行了阐述。最重要的还是着手去写,遇到问题,解决问题,最后都不是问题。
效果图:
自定义View实现2048相关推荐
- Android开发自定义View实现数字与图片无缝切换的2048
本博客地址:http://blog.csdn.net/talentclass_ctt/article/details/51952378 最近在学自定义View,无意中看到鸿洋大神以前写过的2048(附 ...
- Android自定义View基本步骤
一.自定义属性 1.在res下的values下面新建attrs.xml 2.在布局中使用,声明命名空间 3.在自定义View构造方法中通过TypedArray获取属性 4.必须回收 array.rec ...
- Android自定义View —— TypedArray
在上一篇中Android 自定义View Canvas -- Bitmap写到了TypedArray 这个属性 下面也简单的说一下TypedArray的使用 TypedArray 的作用: 用于从该结 ...
- Android 自定义View Canvas —— Bitmap
Bitmap 绘制图片 常用的方法有一下几种 (1) drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint ...
- Android 自定义View —— Canvas
上一篇在android 自定义view Paint 里面 说了几种常见的Point 属性 绘制图形的时候下面总有一个canvas ,Canvas 是是画布 上面可以绘制点,线,正方形,圆,等等,需要和 ...
- Android 自定义View —— Paint
上一篇说了自定义view的坐标系以及view 的使用,下面说下自定义view Paint 的使用 Paint 相对于画笔 ,可以使用Paint 来决定画的内容的颜色,边距粗细,设置样式,字体大小 ,等 ...
- Android 自定义View (入门 篇) 的使用
每次都是过了很久都需要温习一下,自己打算整理一下方便查阅, 自定义view 首选需要明白的就是它的坐标系了,以手机左上角为起始点(0.0),横向的为x轴,竖向的为y轴 为了更好的理解我画了一幅草图如下 ...
- 28自定义View 模仿联系人字母侧栏
自定义View LetterView.java package com.qf.sxy.customview02;import android.content.Context; import andro ...
- android炫酷的自定义view,Android自定义View实现炫酷进度条
本文实例为大家分享了Android实现炫酷进度条的具体代码,供大家参考,具体内容如下 下面我们来实现如下效果: 第一步:创建attrs文件夹,自定义属性: 第二步:自定义View: /** * Cre ...
- android 自定义音乐圆形进度条,Android自定义View实现音频播放圆形进度条
本篇文章介绍自定义View配合属性动画来实现如下的效果 实现思路如下: 根据播放按钮的图片大小计算出圆形进度条的大小 根据音频的时间长度计算出圆形进度条绘制的弧度 通过Handler刷新界面来更新圆形 ...
最新文章
- 那些《西游记》中你不知道的野史,信不信由你
- 励志!86岁的他,申请获得国家自然科学基金!
- 压缩和解压文件:tar gzip bzip2 compress(转)
- 无线策略服务器,无线网络中的分布式资源管理策略研究
- 【Java】为什么java构造函数的构造器只能在第一行写this() 或者super() ?
- 文案一方面需要创意,但一方面不需要过分沉溺于创意
- 2017 Chinese Multi-University Training, BeihangU Contest
- windows2003——工作组和域控制器
- 【IMX6ULL笔记】--内核底层驱动初步探究
- 在SQL server 2008 R2进行数据查询操作时提示 “对象名无效”的问题
- Axure-产品交互设计师的利器
- 测试用例设计——微信发朋友圈(详细)
- Android 自带工具生成图标
- PHP header网页安全认证
- 『中级篇』什么是Container(15)
- 湖南省中职学业水平考试复习试题(数学)
- Ant Design vue 改变表格背景颜色
- MGRE结合OSPF
- 快速上手golang
- 小小Python爬虫一
热门文章
- 武汉大学计算机学院选考要求,武汉大学高考必选科目-考武汉大学需要选哪三科...
- Nginx日志管理——了解Nginx日志选项配置以及自定义日志格式使用
- 中国机床行业投资现状与十四五发展战略决策报告2022版
- openwrt环境下,使用externel commissioning组网openthread
- 监控网页内容,发现需要的内容后弹框和声音提醒
- LABjs分析 http://labjs.com/documentation.php#queuescript
- linux应用程序故障排查,为Linux应用程序排查故障的另类方法
- java课程设计通讯录_java课程设计(通讯录管理软件源代码)
- 任何共享软件作者都能挣到一年10万美金以上的收入,只要他想的话
- stm32h743单片机嵌入式学习笔记2-单片机获取电容触摸屏原理