本博客地址:http://blog.csdn.net/talentclass_ctt/article/details/51952378

最近在学自定义View,无意中看到鸿洋大神以前写过的2048(附上他的博客地址http://blog.csdn.net/lmj623565791/article/details/40020137),看起来很不错,所以自己在他的基础上做一个加强版的2048。先看图:

功能除了正常的2048外,还支持数字与图片无缝切换而没有任何影响,此外,图片不是嵌在自定义View里面的,而是开发者自己在调用时再自己添加的,如:在MainActivity里面添加图片,缺点是Activity被销毁后再进入是重新开始的,不过这只是做一个demo而已,就不讲究这么多了。其实想要开发者改变更多的样式而不用改自定义View内部的关键在于对外暴露的方法的多少,如你可以在自定义View里面写4行4列,也可以暴露一个改变行列数的方法,结果其实没差,只是说这样会减少对自定义View内部的直接操作。

下面这两张图是对应的,切换只需按一下按钮。

下面开始挑战2048:

一共两个自定义View:一个容器GameLayout,一个小方格GameItem。容器主要监听整体变化如数的变化,逻辑处理、小方格的位置等等,具体画小方格的颜色、图片、数字还是由小方块自己画,而调用的时候是对GameLayout进行操作。
写自定义View的第一步:分析有什么属性。
一、容器GameLayout,很明显,必须要知道有多少行多少列,小方格的间距,这是靠上下左右滑动的当然就有检测用户滑动的手势,玩的过程肯定要计分啦...
接着开始实现

1、可以用一个数组来存放小方格,数组的大小由行数决定,之后数字变化了都会对这个数组进行操作,保证每时每刻位置和数字都是对的;

    /*** 测量Layout的宽和高,以及设置Item的宽和高,这里忽略wrap_content 以宽、高之中的最小值绘制正方形*/@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 获得正方形的边长int length = Math.min(getMeasuredHeight(), getMeasuredWidth());// 获得Item的宽度int childWidth = (length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn;if (!once) {if (mItems == null) {mItems = new GameItem[mColumn * mColumn];}// 放置Itemfor (int i = 0; i < mItems.length; i++) {GameItem item = new GameItem(getContext());mItems[i] = item;item.setId(i + 1);RelativeLayout.LayoutParams lp = new LayoutParams(childWidth, childWidth);// 设置横向边距,不是最后一列if ((i + 1) % mColumn != 0) {lp.rightMargin = mMargin;}// 如果不是第一列if (i % mColumn != 0) {lp.addRule(RelativeLayout.RIGHT_OF, mItems[i - 1].getId());}// 如果不是第一行,设置纵向边距,非最后一行if ((i + 1) > mColumn) {lp.topMargin = mMargin;lp.addRule(RelativeLayout.BELOW, mItems[i - mColumn].getId());}addView(item, lp);}//生成数字generateNum();}once = true;setMeasuredDimension(length, length);}

2、对于手势,为了简单方便,我们枚举四个方向,自己写一个类继承GestureDetector.SimpleOnGestureListener,在里面判断向那边滑动,注释写的很清楚就不多说了,对于里面的action方法,它会根据你向哪边滑动做出响应的处理,如对小方格移动、数字的合并等等;

    /*** 运动方向的枚举*/private enum ACTION {LEFT, RIGHT, UP, DOWM}/*** 根据坐标变化判断手势*/class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {// 设置最小滑动距离final int FLING_MIN_DISTANCE = 50;@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {// 得到在X轴移动的距离float x = e2.getX() - e1.getX();// 得到在Y轴移动的距离float y = e2.getY() - e1.getY();if (x > FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) {// 向右滑action(ACTION.RIGHT);} else if (x < -FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) {// 向左滑action(ACTION.LEFT);} else if (y > FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) {// 向下滑action(ACTION.DOWM);} else if (y < -FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) {// 向上滑action(ACTION.UP);}return true;}}

3、不从界面,单纯从逻辑考虑,当用户向某一方向移动时,其实就是不断遍历再判断,表的遍历需要两重for循环,根据方向从方向的最前面开始,一个一个判断是不是0(0表示空白),从而判断能不能移动,然后判断是否能合并以及设置合并后的值,之后在值为0的空白小方格中随机选一块产生2或4,当然,到最后无法产生随机数就说明游戏结束了,逻辑差不多就这样吧。

    /*** 根据用户运动,整体进行移动合并值等*/private void action(ACTION action) {// 行|列for (int i = 0; i < mColumn; i++) {List<GameItem> row = new ArrayList<>();// 行|列//记录不为0的数字for (int j = 0; j < mColumn; j++) {// 得到下标int index = getIndexByAction(action, i, j);GameItem item = mItems[index];// 记录不为0的数字if (item.getNumber() != 0) {row.add(item);}}//判断是否发生移动for (int j = 0; j < mColumn && j < row.size(); j++) {int index = getIndexByAction(action, i, j);GameItem item = mItems[index];if (item.getNumber() != row.get(j).getNumber()) {isMoveHappen = true;}}// 合并相同的mergeItem(row);// 设置合并后的值for (int j = 0; j < mColumn; j++) {int index = getIndexByAction(action, i, j);if (row.size() > j) {mItems[index].setNumber(row.get(j).getNumber());} else {mItems[index].setNumber(0);}}}//生成数字generateNum();}

二、接下来轮到小方格了,他应该设什么属性呢?你可能会想到边长吧,其实边长是可以不用考虑的,因为容器的边长确定了,行数确定了,内边距也确定了,小方格的边长也就确定了,这也符合自定义View的原则之一,能又其他属性算出来的就直接算出来而不重复设。它的属性应该有类型(是图片还是数字)、数字、图片、背景色。
1、默认类型是数字,可以用setType方法改变模式;

    /*** 设置类型* @param type 0为数字, 1为图片*/
public void setType(int type) {this.type = type;invalidate();}

2、通过setNumber方法改变内容,改变时又会根据不同的数字选取不同的颜色(这些颜色是我自己一个一个试的,感觉还可以,还有就是我比较喜欢蓝色的,所以你会看到demo运行后基本上界面都是蓝色的),同理,图片也是根据这个来变化的。

    /*** 得到图片id数组,并转换成Bitmap类型** @param iamges*/public void setImages(int[] Images) {this.mImages = Images;if (mBitmaps == null) {mBitmaps = new Bitmap[mImages.length];for (int i = 0; i < mImages.length; i++) {// 将图片id转化成BitmapmBitmaps[i] = BitmapFactory.decodeResource(getResources(), mImages[i]);}}invalidate();}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);if (type == TYPE_NUMBER) {String bgColor = null;switch (mNumber) {case 0:bgColor = "#616ba1";break;case 2:bgColor = "#bfc8f7";break;case 4:bgColor = "#b0bbf7";break;case 8:bgColor = "#9facf5";break;case 16:bgColor = "#909ff4";break;case 32:bgColor = "#8394f2";break;case 64:bgColor = "#788bf4";break;case 128:bgColor = "#6f83f2";break;case 256:bgColor = "#6379f2";break;case 512:bgColor = "#5971f4";break;case 1024:bgColor = "#4f69f2";break;case 2048:bgColor = "#3F51B5";break;default:bgColor = "#8899f5";break;}// 用对应的颜色充满整个小方格mPaint.setColor(Color.parseColor(bgColor));mPaint.setStyle(Paint.Style.FILL);canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);// 如果有数字就画出来if (mNumber != 0) {mPaint.setColor(Color.BLACK);float x = (getWidth() - mBound.width()) / 2;float y = getHeight() / 2 + mBound.height() / 2;canvas.drawText(mNumber + "", x, y, mPaint);}} else {int index = -1;// 将数字转换成图片下标switch (mNumber) {case 2:index = 0;break;case 4:index = 1;break;case 8:index = 2;break;case 16:index = 3;break;case 32:index = 4;break;case 64:index = 5;break;case 128:index = 6;break;case 256:index = 7;break;case 512:index = 8;break;case 1024:index = 9;break;case 2048:index = 10;break;}// 如果没有图片,则直接用颜色充满整个小方格if (mNumber == 0) {mPaint.setColor(Color.parseColor("#616ba1"));mPaint.setStyle(Paint.Style.FILL);canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);}// 如果有图片就画出来if (mNumber != 0)canvas.drawBitmap(mBitmaps[index], null, new Rect(0, 0, getWidth(), getHeight()), null);}}

三、接下来就是使用了,其实很简单,加入xml后,在Activity 中找到控件,设置各种监听和处理

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"android:orientation="vertical"android:paddingBottom="8dp"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="8dp"tools:context=".MainActivity"><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:gravity="center_vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:textColor="@color/colorPrimary"android:textSize="18sp"android:text="当前得分:" /><TextViewandroid:id="@+id/id_score"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="32sp"android:text="0"android:textColor="@color/colorAccent"android:textStyle="bold"/></LinearLayout><Viewandroid:layout_width="match_parent"android:layout_height="2dp"android:background="@color/colorPrimary" /><com.talentclass.numberimage2048.GameLayoutandroid:id="@+id/id_game2048"android:layout_width="match_parent"android:layout_height="match_parent"android:padding="10dp" /><LinearLayoutandroid:layout_width="wrap_content"android:layout_height="42dp"android:layout_marginBottom="12dp"><Buttonandroid:id="@+id/id_type"android:layout_width="wrap_content"android:layout_height="42dp"android:background="@drawable/shape"android:textColor="@color/white"android:text="图片模式"/><Viewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_weight="1"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="18sp"android:textColor="@color/colorPrimary"android:text="最高分:"/><TextViewandroid:id="@+id/id_max_score"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="24sp"android:textColor="@color/red"android:textStyle="bold"android:text="0" /></LinearLayout><Buttonandroid:id="@+id/id_restart"android:layout_width="match_parent"android:layout_height="42dp"android:background="@drawable/shape"android:textColor="@color/white"android:text="不服重来"android:layout_gravity="bottom" /></LinearLayout>

Activity也只是简答的判断逻辑

package com.talentclass.numberimage2048;import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.preference.Preference;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;/*** 程序入口** @author talentClass*/
public class MainActivity extends AppCompatActivity implements GameLayout.Game2048Listener {public static final String SCORE = "score";/*** 模式:false为数字,true为图片*/private boolean bType;private TextView tvScore, tvMaxScore; // 当前分数、最高分private Button btnType, btnRestart; // 设置类型、重新开始private GameLayout mGameLayout; // 自定义View容器// 放置图片的数组private int[] mImages = {R.mipmap.image1, R.mipmap.image2, R.mipmap.image3, R.mipmap.image4, R.mipmap.image5, R.mipmap.image6,R.mipmap.image7, R.mipmap.image8, R.mipmap.image9, R.mipmap.image10, R.mipmap.image11};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化界面init();}/*** 初始化界面*/private void init() {tvScore = (TextView) findViewById(R.id.id_score);tvMaxScore = (TextView) findViewById(R.id.id_max_score);btnType = (Button) findViewById(R.id.id_type);btnRestart = (Button) findViewById(R.id.id_restart);mGameLayout = (GameLayout) findViewById(R.id.id_game2048);mGameLayout.setOnGame2048Listener(this);btnType.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if(bType){// 如果当前是图片模式,则此时按钮显示数字模式,所以点下去后,按钮显示图片模式bType = false;btnType.setText("图片模式");// 设置类型为数字模式mGameLayout.setType(GameItem.TYPE_NUMBER);}else {// 如果当前是数字模式,则按钮显示图片模式,所以点下去后,按钮显示数字模式bType = true;btnType.setText("数字模式");// 先把图片放进去,然后再设置类型为图片模式mGameLayout.setImage(mImages);mGameLayout.setType(GameItem.TYPE_IMAGE);}}});btnRestart.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {saveScore(tvScore.getText().toString());// 重新开始mGameLayout.restart();}});tvMaxScore.setText(getScore());}/*** 获取最高分** @return*/private String getScore() {return getSharedPreferences(SCORE, MODE_PRIVATE).getString(SCORE, "0");}/*** 根据得分判断是否保存到最高分** @param score*/private void saveScore(String score) {// 先转换成int类型比较大小int now = Integer.parseInt(tvScore.getText().toString());int max = Integer.parseInt(tvMaxScore.getText().toString());// 如果超过最高分if (now > max) {tvMaxScore.setText(score);// 保存起来,下次启动再拿出来SharedPreferences.Editor editor = getSharedPreferences(SCORE, MODE_PRIVATE).edit();editor.putString(SCORE, score);editor.commit();}}@Overridepublic void onBackPressed() {// 推出前先保存分数saveScore(tvMaxScore.getText().toString());super.onBackPressed();}@Overridepublic void onScoreChange(int score) {tvScore.setText(score + "");}@Overridepublic void onGameOver() {new AlertDialog.Builder(this).setTitle("游戏结束").setMessage("你的得分是:" + tvScore.getText()).setPositiveButton("再来一次", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {saveScore(tvScore.getText().toString());mGameLayout.restart();}}).setNegativeButton("不玩了", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// 保存分数后直接退出应用saveScore(tvScore.getText().toString());finish();}}).show();}
}

其实源代码我注释也写的很详细,大家可以下载,相信一看就懂的。

最后附上完整源代码:源代码

Android开发自定义View实现数字与图片无缝切换的2048相关推荐

  1. android开发自定义View(三)仿芝麻信用积分

    此文参考了https://github.com/HotBitmapGG/CreditSesameRingView 感谢作者的分享!! 首先看一下支付宝上显示的样子 然后看一下模仿的效果 代码 基础部分 ...

  2. Android开发自定义View

    Android中View组件的作用类似于Swing变成中的JPanel,它只是一个空白的矩形区域,View组件中没有任何内容.对于Android应用的其他UI组件来说,它们都继承了View组件,然后在 ...

  3. Android开发自定义View之滑动按钮与自定义属性

    写博客辛苦了,转载的朋友请标明出处哦,finddreams:(http://blog.csdn.net/finddreams/article/details/40392975) 话不多说,先运行效果图 ...

  4. Android开发自定义View之仿米家APP双色灯控制UI:做一个智能家居产品的简单智能灯UI !(附带Demo)

    一. 前言: 兜兜转转,不知不觉做Android开发已经快2年了,上半年一直在搞wifi模块开发,导致不务正业,写个自定义UI还要折腾半天,真是对不起自己的良心了!最近要对接小米开放平台,不小心看到了 ...

  5. android开发自定义View(四)仿掌上英雄联盟能力值分析效果

    本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 原始图效果 模仿效果 PNG GIF 流程 绘制中心线,用于计算外层多边形各点的坐标 绘制最外层多边形 分析原型图算出每个多边形之间 ...

  6. Android 中自定义View 裁剪扇形图片

    Android 中自定义View 裁剪扇形图片 当需要裁剪图片为扇形区域时,使用Canvas.clipPath(path)方法可以裁剪为扇形区域 ps:此方法会导致绘制图片边缘有锯齿,暂无解决方法(知 ...

  7. android 最新 九宫格,Android开发中怎么显示一个九宫格图片

    Android开发中怎么显示一个九宫格图片 发布时间:2020-11-21 15:09:33 来源:亿速云 阅读:86 作者:Leah 本篇文章给大家分享的是有关Android开发中怎么显示一个九宫格 ...

  8. Android开发自定义UI组件

    Android开发自定义UI组件实现红色小球跟随手指移动 要写实现自定义UI组件,要创建一个BallView类,继承View类,在BallView类中创建画笔,然后重写OnDraw()方法和OnTou ...

  9. 【Android】自定义View、画家(画布)Canvas与画笔Paint的应用——画图、涂鸦板app的实现

    利用一个简单的画图app来说明安卓的图形处理类与自定义View的应用. 如下图,有一个供用户自己任意画图.涂鸦的app, 这里不做那么花俏了,仅提供黑白两色,但可以改变笔尖的粗细. 实质上这里的橡皮擦 ...

最新文章

  1. 如何促使团队紧密协作
  2. boyer moore算法 java_Boyer-Moore算法
  3. 进程、线程与应用程序域
  4. 【MM配置】Batch Determination 批次决定
  5. 大牛书单 | 大数据存储方向好书分享
  6. sqlserver 人名_sqlserver上机试题
  7. 预防死锁可以破坏哪些死锁的必要条件
  8. python实训目的意义_Python实训第二天--基础知识2
  9. 18-mysql-数据类型
  10. 什么是13薪,真的有18薪、25薪的不?
  11. VMware NSX-T Data Center 2.5 下载(转载)
  12. Eclipse学习笔记——快捷键
  13. LINUX一个正确的mime xml范例
  14. 红帽Linux多点触摸,Linux系统实现多点触摸
  15. 对话乔会君丨两年服务1700家企业,洪泰智造如何锻造下一个独角兽?
  16. Designing Data-Intensive Application《数据密集型应用系统设计》笔记
  17. android多任务列表高斯模糊
  18. kaggle竞赛 - Home Credit Default Risk金牌心得
  19. SVG中以任意直线为对称轴的镜像变换及其矩阵
  20. 安装配置nagios+nrpe+nconf

热门文章

  1. [亲测可用]springBoot调用对方webService接口的几种方法示例
  2. Python ctypes 模块
  3. python学习第十三节:模块
  4. 与机房收费系统重相见
  5. 计算机电子科技生产质量标准,微型数字电子计算机+质量分等标准.pdf
  6. 入门数据结构,看这一篇就够了!
  7. 蒙皮外包与变形器的交互使用一例--Marvelous到3dmax
  8. java+uniapp对接app微信支付
  9. STM32启动文件和链接脚本
  10. 芒芒分期换“套路”以VIP会员费收割用户,海尔金控旗下快捷通提供支付通道