android自定义View 别踩白块儿
废话不说,先贴张原游戏的图片,和我们自己最后的效果图,直接进入正题。
原图效果图
可以看到别踩白块儿,将屏幕的宽和高都分成了四部分,十六小块,然后将不同的小块填充为不同的颜色,在原游戏中还有一种长长的黑块,而我们这个是简化板,只保留了每个黑块只占一小块。
由于每个小块,都是矩形块,我们选择用canvas.drawRect()
方法来绘制,那么首先要定义一个PiecesRectF类继承自RectF,并在PiecesRectF类中定义矩形块不同的状态。
public class PiecesRectF extends RectF {
private int type;
public final static int BLAKE = 0;//黑块
public final static int WRITE = 1;//白块
public final static int BLUE = 2;//黑块按下时的显示蓝块
public final static int START = 3;//标记有开始的黑块
public final static int RED = 4;//按到白块,或有黑块漏按,游戏结束时的红块
public PiecesRectF(){
super();
type = Math.random() > 0.5 ? 0:1;//初始化时,给type随机一个白块或黑块
}
public void setType(int type) {
this.type = type;
}
public int getType() {
return type;
}
}
接着就是最关键的自定义view了
private final static String TAG = WhitePiecesView.class.getSimpleName();
private Paint mPaint;
private PiecesRectF[][] ovals = new PiecesRectF[5][4];//屏幕上的方格块
private SparseArray<PiecesRectF> selectOvals = new SparseArray<PiecesRectF>();//点击时选中的方格块
private int topOvalHeight = 0;//最上面一行方格的高度
private int score = 0;
private boolean isGameOver;//游戏是否结束
private boolean once = true;
public WhitePiecesView(Context context) {
this(context,null);
}
public WhitePiecesView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public WhitePiecesView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
initOvals();//初始化方块
}
private void initOvals(){
for (int i = 0;i < 5;i++){
for (int j = 0;j<4;j++){
ovals[i][j] = new PiecesRectF();
if (j == 1){
//如果是第二列,则若第一列的是黑块将这个方块设为白块
if (ovals[i][j-1].getType() == PiecesRectF.BLAKE ||
ovals[i][j-1].getType() == PiecesRectF.START) {
ovals[i][j].setType(PiecesRectF.WRITE);
}
}else if (j == 3){
//如果是第四列,同样若第一列的是黑块将这个方块设为白块
if (ovals[i][j-1].getType() == PiecesRectF.BLAKE ||
ovals[i][j-1].getType() == PiecesRectF.START) {
ovals[i][j].setType(PiecesRectF.WRITE);
}
//如果是第四列,且前四列的都为白块,则设为黑块
else if (ovals[i][j-2].getType() == PiecesRectF.WRITE &&
ovals[i][j-3].getType() == PiecesRectF.WRITE) {
ovals[i][j].setType(PiecesRectF.BLAKE);
}
}
if (i == 4){
if (ovals[i][j].getType() == PiecesRectF.BLAKE)
ovals[i][j].setType(PiecesRectF.START);//若是最后一行,将黑块的type替换为START
}
}
}
}
根据对原游戏的观察,在方块向下移动过程中,一共有5 * 4个,因此定义一个二维数组,并且在每一行的第一第二列只会有一个黑块,同样第三第四列也是。而且每一行一定会有一个黑块。因此在初始化时加上以上的判断条件,并且设置type。
再来设置一个监听,来监听分数变化和游戏是否结束
private WhitePiecesListener MyWhitePiecesListener;
public void setWhitePiecesListener(WhitePiecesListener myWhitePiecesListener) {
MyWhitePiecesListener = myWhitePiecesListener;
}
public interface WhitePiecesListener{
void getScore(int score);
void gameOver();
}
重写onDraw
方法,绘制方块
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawRect(canvas);
}
private void drawRect(Canvas canvas){
int w = getWidth()/4;//得到每个小方块的宽
int h = getHeight()/4;//得到每个小方块的高
if (isGameOver){
//游戏结束
MyWhitePiecesListener.gameOver();
isGameOver = false;
}
for (int i = 0;i < 5;i++){
for (int j = 0;j < 4;j++){
ovals[i][j].left = w * j;
ovals[i][j].right = w * (j + 1);
ovals[i][j].bottom = topOvalHeight + i * h;
ovals[i][j].top = ovals[i][j].bottom - h;
mPaint.setStyle(Paint.Style.FILL);//绘制色块时,设置画笔为FILL
switch (ovals[i][j].getType()){
case PiecesRectF.BLAKE:{
//绘制黑块
mPaint.setColor(Color.BLACK);
canvas.drawRect(ovals[i][j],mPaint);
break;
}
case PiecesRectF.BLUE:{
//绘制蓝块
mPaint.setColor(Color.BLUE);
canvas.drawRect(ovals[i][j],mPaint);
break;
}
case PiecesRectF.RED:{
//绘制红块
mPaint.setColor(Color.RED);
canvas.drawRect(ovals[i][j],mPaint);
break;
}
case PiecesRectF.START:{
//先绘制黑块
mPaint.setColor(Color.BLACK);
canvas.drawRect(ovals[i][j],mPaint);
//在绘制文字
mPaint.setColor(Color.parseColor("#ffffff"));
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(50);
String start = "开始";
Rect bounds = new Rect();
mPaint.getTextBounds(start,0,start.length(),bounds);
float x = ovals[i][j].left / 2 + ovals[i][j].right / 2;
float y = ovals[i][j].top / 2 + ovals[i][j].bottom / 2 + bounds.bottom / 2 - bounds.top / 2;
canvas.drawText(start,x,y,mPaint);
break;
}
}
//设置画笔为STROKE,绘制边框
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.parseColor("#ffffff"));
mPaint.setStrokeWidth(3);
canvas.drawRect(ovals[i][j],mPaint);
}
}
}
在drawRect()中,对二维数组进行遍历,并设定每个方块的位置,然后根据每个方块type的不同,进行了不同的绘制方法,最后绘制白色边框。
绘制完后,我们需要设定点击事件,在点击STRAT的方块时,游戏开始所有方块向下滑,得分+1,开始后点击BLACK,得分+1,点击WHITE,游戏结束。因此重写onTouchEvent(MotionEvent event)
,并且使用多点触控。
@Override
public boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
switch (event.getActionMasked()){
case ACTION_DOWN:
case ACTION_POINTER_DOWN: {
int id = event.getPointerId(index);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 4; j++) {
PiecesRectF f = ovals[i][j];
if (event.getX() > f.left && event.getX() < f.right
&& event.getY() > f.top && event.getY() < f.bottom) {
selectOvals.put(id, f);//点击选中的方块存入SparseArray
switch (f.getType()){
case PiecesRectF.BLAKE:{
if (!once) {
f.setType(PiecesRectF.BLUE);
score++;
}
break;
}
case PiecesRectF.START:{
if (once){
//判断第一次按中
startThread();
once = false;
}
//按黑块处理
f.setType(PiecesRectF.BLUE);
score++;
break;
}
case PiecesRectF.WRITE:{
if (!once) {
//点中白块GameOver
f.setType(PiecesRectF.RED);
isGameOver = true;
invalidate();
}
break;
}
}
MyWhitePiecesListener.getScore(score);
}
}
}
break;
}
case ACTION_UP:
case ACTION_POINTER_UP:{
int id = event.getPointerId(index);
PiecesRectF f = selectOvals.get(id,null);//得到某个手指选中的方块
if (f != null && f.getType() == PiecesRectF.BLUE ){
//手指抬起后,将蓝色重新变红
f.setType(PiecesRectF.WRITE);
}
break;
}
}
return true;
}
在onTouchEvent(MotionEvent event)
方法中,对点击不同方块进行了不同的处理,并且在第一次按下START的白块时,执行了startThread()
方法来,实现所有方块的向下滑动.
private void startThread(){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
topOvalHeight =topOvalHeight + 7;//这里写死了滑动速度
if (isGameOver) {//如果游戏已经结束,结束循环
return;
}
if (topOvalHeight > getHeight()/4) {
topOvalHeight = 0;//若最顶层的方块的高,超出正常方块的的高,清零。
if (checkBottomOvals()){//检测是否有黑色方块漏点
//有漏点,游戏结束
isGameOver = true;
postInvalidate();
return;
}else
//没有漏点,更新方块游戏结束
updateRectF();
}
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
postInvalidate();
}
}
}).start();
}
然后是检测是否有黑色方块漏点的checkBottomOvals()
,和更新滑块的updateRectF()
。
/**
*检测是否有漏点
*/
private boolean checkBottomOvals(){
boolean haveBlake = false;
for (int i = 0;i < 4;i++){
if (ovals[4][i].getType() == PiecesRectF.BLAKE
|| ovals[4][i].getType() == PiecesRectF.START) {
//判断最后一行是否存在BLAKE或START方块
//如果有将其设为红色
ovals[4][i].setType(PiecesRectF.RED);
haveBlake = true;
}
}
return haveBlake;
}
/**
* 更新方块
*/
private void updateRectF(){
for (int i = 4; i >= 0; i--) {
for (int j = 0; j < 4; j++) {
if (i == 0) {//如果是第一行,重新初始化一行
ovals[i][j] = new PiecesRectF();
if (j == 1){
if (ovals[i][j-1].getType() == PiecesRectF.BLAKE)
ovals[i][j].setType(PiecesRectF.WRITE);
}else if (j == 3){
if (ovals[i][j-1].getType() == PiecesRectF.BLAKE)
ovals[i][j].setType(PiecesRectF.WRITE);
else if (ovals[i][j-2].getType() == PiecesRectF.WRITE &&
ovals[i][j-3].getType() == PiecesRectF.WRITE)
ovals[i][j].setType(PiecesRectF.BLAKE);
}
}
else//否则,将行数后移
ovals[i][j] = ovals[i - 1][j];
}
}
}
最后在加上一个重新开始的方法
public void restart(){
once = true;
topOvalHeight = 0;//顶层高度归零
score = 0;//分数归零
MyWhitePiecesListener.getScore(score);
initOvals();//重新初始化
invalidate();//再次绘制
}
实测
先是一个很简单的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.whitepieces.MainActivity"
android:background="@drawable/bg">
<com.whitepieces.WhitePiecesView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/write_pieces"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/score"
android:textSize="70sp"
android:text="0"
android:textColor="#FF3080"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
然后是MainActivity
public class MainActivity extends AppCompatActivity implements WhitePiecesView.WhitePiecesListener{
private WhitePiecesView whitePiecesView;
private TextView scoreText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
whitePiecesView = (WhitePiecesView)findViewById(R.id.write_pieces);
scoreText = (TextView)findViewById(R.id.score);
whitePiecesView.setWhitePiecesListener(this);
}
@Override
public void getScore(int score) {
scoreText.setText(""+ score);
}
@Override
public void gameOver() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Game Over")
.setMessage("您获得的分数为"+scoreText.getText()+"是否重新开始")
.setNegativeButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
whitePiecesView.restart();
return;
}
})
.setPositiveButton("否", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
}).show();
}
}
同样很简单,就是显示分数,以及游戏结束时,弹出Dialog选择退出或重新开始.
最后来张GIF图