android 分割数字图片,Android开发自定义View实现数字与图片无缝切换的2048

最近在学自定义View,无意中看到鸿洋大神以前写过的2048,看起来很不错,所以自己在他的基础上做一个加强版的2048。先看图:

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

122354_0.jpg

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

122354_1.jpg

122354_2.jpg

下面开始挑战2048:

一共两个自定义View:一个容器GameLayout,一个小方格GameItem。容器主要监听整体变化如数的变化,逻辑处理、小方格的位置等等,具体画小方格的颜色、图片、数字还是由小方块自己画,而调用的时候是对GameLayout进行操作。

写自定义View的第一步:分析有什么属性。

一、容器GameLayout,很明显,必须要知道有多少行多少列,小方格的间距,这是靠上下左右滑动的当然就有检测用户滑动的手势,玩的过程肯定要计分啦...

接着开始实现

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

/**

* 测量Layout的宽和高,以及设置Item的宽和高,这里忽略wrap_content 以宽、高之中的最小值绘制正方形

*/

@Override

protected 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];

}

// 放置Item

for (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;

@Override

public 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 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转化成Bitmap

mBitmaps[i] = BitmapFactory.decodeResource(getResources(), mImages[i]);

}

}

invalidate();

}

@Override

protected 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 中找到控件,设置各种监听和处理

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};

@Override

protected 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() {

@Override

public 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() {

@Override

public 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();

}

}

@Override

public void onBackPressed() {

// 推出前先保存分数

saveScore(tvMaxScore.getText().toString());

super.onBackPressed();

}

@Override

public void onScoreChange(int score) {

tvScore.setText(score + "");

}

@Override

public void onGameOver() {

new AlertDialog.Builder(this).setTitle("游戏结束")

.setMessage("你的得分是:" + tvScore.getText())

.setPositiveButton("再来一次", new DialogInterface.OnClickListener() {

@Override

public void onClick(DialogInterface dialog, int which) {

saveScore(tvScore.getText().toString());

mGameLayout.restart();

}

})

.setNegativeButton("不玩了", new DialogInterface.OnClickListener() {

@Override

public void onClick(DialogInterface dialog, int which) {

// 保存分数后直接退出应用

saveScore(tvScore.getText().toString());

finish();

}

}).show();

}

}

bf2804467dd31a4a54bbfce6fa2cf6bb.png

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值