mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
getPaddingBottom());
mGestureDetector = new GestureDetector(context , new MyGestureDetector());
}
我们在构造方法里面得到Item间的边距(margin)和我们容器的内边距(padding,),这个值应该四边一致,于是我们取四边的最小值;这两个属性可以抽取为自定义的属性;然后初始化了我们的mGestureDetector
有了margin和padding,我们就可以计算我们item的边长了。这个计算过程肯定在onMeasure里面,因为我们需要在onMeasure获取容器的宽和高
3、onMeasure
private boolean once;
/**
- 测量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 (mGame2048Items == null)
{
mGame2048Items = new Game2048Item[mColumn * mColumn];
}
// 放置Item
for (int i = 0; i < mGame2048Items.length; i++)
{
Game2048Item item = new Game2048Item(getContext());
mGame2048Items[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,//
mGame2048Items[i - 1].getId());
}
// 如果不是第一行,//设置纵向边距,非最后一行
if ((i + 1) > mColumn)
{
lp.topMargin = mMargin;
lp.addRule(RelativeLayout.BELOW,//
mGame2048Items[i - mColumn].getId());
}
addView(item, lp);
}
generateNum();
}
once = true;
setMeasuredDimension(length, length);
}
首先设置容器的边长为宽高中的最小值;然后(length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn ; 去计算Item的边长;
拿到以后,根据我们的mColumn初始化我们的Item数组,然后遍历生成Item,设置Item的LayoutParams以及Rule(RIGHT_OF , BELOW),最后添加到我们的容器中;
最后我们通过setMeasuredDimension(length, length);改变我们布局占据的空间;
到此,我们整个面板绘制完成了;
接下来,就是根据用户的手势,去进行游戏逻辑操作了,手势那么肯定是onTouchEvent了:
4、onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event)
{
mGestureDetector.onTouchEvent(event);
return true;
}
我们把触摸事件交给了mGestureDetector,我们去看看我们的mGestureDetector,在构造方法中有这么一句:
mGestureDetector = new GestureDetector(context , new MyGestureDetector());
so,我们需要去看看MyGestureDetector:
class MyGestureDetector extends GestureDetector.SimpleOnGestureListener
{
final int FLING_MIN_DISTANCE = 50;
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY)
{
float x = e2.getX() - e1.getX();
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;
}
}
很简单,就是判读用户上、下、左、右滑动;然后去调用action(ACTION)方法;ACTION是个枚举:
/**
-
运动方向的枚举
-
@author zhy
*/
private enum ACTION
{
LEFT, RIGHT, UP, DOWM
}
这么看,核心代码都在action方法里面了:
5、根据用户手势重绘Item
看代码前,先考虑下,用户从右向左滑动时,面板应该如何变化;取其中一行,可能性为:
0 0 0 2 -> 2 0 0 0
2 0 4 0 -> 2 4 0 0
2 2 4 0 -> 4 4 0 0
大概就这3中可能;
我们算法是这么做的:
拿2 2 4 0 来说:
1、首先把每行有数字的取出来,临时存储下来,即[ 2, 2, 4 ];
2、然后遍历合并第一个相遇的相同的,即[ 4, 4 ,0 ]
3、然后直接放置到原行,不足补0,即[ 4, 4, 0 ,0 ];
中间还有几个操作:
1、生成一个新的数字,游戏在每次用户滑动时,可能会生成一个数字;我们的生成策略:如果发生移动或者合并,则生成一个数字;
移动的判断,拿原数据,即【 2 ,2,4,0】和我们第一步临时存储的做比较,一一对比(遍历临时表),发现不同,则认为移动了;
合并的判断,在合并的时候会设置合并的标志位为true;
2、加分,如果发生合并,则加分,分值为合并得到的数字,比如 4,4 -> 8 ,即加8分 ; 也只需要在合并的时候进行相加就行了;
介绍完了,来看我们的代码:
/**
- 根据用户运动,整体进行移动合并值等
*/
private void action(ACTION action)
{
// 行|列
for (int i = 0; i < mColumn; i++)
{
List row = new ArrayList();
// 行|列
for (int j = 0; j < mColumn; j++)
{
// 得到下标
int index = getIndexByAction(action, i, j);
Game2048Item item = mGame2048Items[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);
Game2048Item item = mGame2048Items[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)
{
mGame2048Items[index].setNumber(row.get(j).getNumber());
} else
{
mGame2048Items[index].setNumber(0);
}
}
}
generateNum();
}
大体上是两层循环,外层循环代码循环次数,内层有3个for循环;
第一个for循环,对应上述:首先把每行有数字的取出来,临时存储下来,即[ 2, 2, 4 ];
第二个for循环,判断是否发生移动;
// 合并相同的
mergeItem(row); 是去进行合并操作,对应上述:然后遍历合并第一个相遇的相同的,即[ 4, 4 ,0 ];以及加分和设置合并标志位都在方法中;
第三个for循环:设置合并后的值,对应上述:然后直接放置到原行,不足补0,即[ 4, 4, 0 ,0 ];
最后生成数字,方法内部会进行判断游戏是否结束,是否需要生成数字;
那么先看mergeItem的代码:
/**
-
合并相同的Item
-
@param row
*/
private void mergeItem(List row)
{
if (row.size() < 2)
return;
for (int j = 0; j < row.size() - 1; j++)
{
Game2048Item item1 = row.get(j);
Game2048Item item2 = row.get(j + 1);
if (item1.getNumber() == item2.getNumber())
{
isMergeHappen = true;
int val = item1.getNumber() + item2.getNumber();
item1.setNumber(val);
// 加分
mScore += val;
if (mGame2048Listener != null)
{
mGame2048Listener.onScoreChange(mScore);
}
// 向前移动
for (int k = j + 1; k < row.size() - 1; k++)
{
row.get(k).setNumber(row.get(k + 1).getNumber());
}
row.get(row.size() - 1).setNumber(0);
return;
}
}
}
也比较简单,循环查找相同的number,发现合并数字,加分;
加分我们设置了一个回调,把分数回调出去:
if (mGame2048Listener != null)
{
mGame2048Listener.onScoreChange(mScore);
}
最后看我们生成数字的代码:
/**
- 产生一个数字
*/
public void generateNum()
{
if (checkOver())
{
Log.e(“TAG”, “GAME OVER”);
if (mGame2048Listener != null)
{
mGame2048Listener.onGameOver();
}
return;
}
if (!isFull())
{
if (isMoveHappen || isMergeHappen)
{
Random random = new Random();
int next = random.nextInt(16);
Game2048Item item = mGame2048Items[next];
while (item.getNumber() != 0)
{
next = random.nextInt(16);
item = mGame2048Items[next];
}
item.setNumber(Math.random() > 0.75 ? 4 : 2);
isMergeHappen = isMoveHappen = false;
}
}
}
首先判断是否结束,如果结束了,依然是回调出去,得让玩的人知道结束了;
然后判断当然面板是有木有空的格子,如果没有,在判断需要生成新的数字么,需要则随机生成一个新的2或4;
那么如何判断是否结束呢?
首先肯定是没有空格了,然后四个方向上没有相同的数字就结束了:
/**
-
检测当前所有的位置都有数字,且相邻的没有相同的数字
-
@return
*/
private boolean checkOver()
{
// 检测是否所有位置都有数字
if (!isFull())
{
return false;
}
for (int i = 0; i < mColumn; i++)
{
for (int j = 0; j < mColumn; j++)
{
int index = i * mColumn + j;
// 当前的Item
Game2048Item item = mGame2048Items[index];
// 右边
if ((index + 1) % mColumn != 0)
{
Log.e(“TAG”, “RIGHT”);
// 右边的Item
Game2048Item itemRight = mGame2048Items[index + 1];
if (item.getNumber() == itemRight.getNumber())
return false;
}
// 下边
if ((index + mColumn) < mColumn * mColumn)
{
Log.e(“TAG”, “DOWN”);
Game2048Item itemBottom = mGame2048Items[index + mColumn];
if (item.getNumber() == itemBottom.getNumber())
return false;
}
// 左边
if (index % mColumn != 0)
{
Log.e(“TAG”, “LEFT”);
Game2048Item itemLeft = mGame2048Items[index - 1];
if (itemLeft.getNumber() == item.getNumber())
return false;
}
// 上边
if (index + 1 > mColumn)
{
Log.e(“TAG”, “UP”);
Game2048Item itemTop = mGame2048Items[index - mColumn];
if (item.getNumber() == itemTop.getNumber())
return false;
}
}
}
return true;
}
/**
-
是否填满数字
-
@return
*/
private boolean isFull()
{
// 检测是否所有位置都有数字
for (int i = 0; i < mGame2048Items.length; i++)
{
if (mGame2048Items[i].getNumber() == 0)
{
return false;
}
}
return true;
}
到此,我们的代码介绍完毕~~~完成了我们的Game2048Layout ; 接下来看如何使用呢?
写游戏的过程很艰辛,但是用起来,看看什么叫so easy ; 当成普通的View即可:
4、实践
====
1、布局文件:
<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:tools=“http://schemas.android.com/tools”
android:layout_width=“fill_parent”
android:layout_height=“fill_parent” >
<com.zhy.game2048.view.Game2048Layout
android:id=“@+id/id_game2048”
android:layout_width=“fill_parent”
android:layout_height=“fill_parent”
android:layout_centerInParent=“true”
android:background=“#ffffff”
android:padding=“10dp” >
</com.zhy.game2048.view.Game2048Layout>
<LinearLayout
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_above=“@id/id_game2048”
android:layout_centerHorizontal=“true”
android:layout_marginBottom=“20dp”
android:background=“#EEE4DA”
android:orientation=“horizontal” >
<TextView
android:id=“@+id/id_score”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:padding=“4dp”
android:text=“Score: 0”
android:textColor=“#EA7821”
android:textSize=“30sp”
android:textStyle=“bold” />
2、MainActivity
package com.zhy.game2048;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.widget.TextView;
import com.zhy.game2048.view.Game2048Layout;
import com.zhy.game2048.view.Game2048Layout.OnGame2048Listener;
public class MainActivity extends Activity implements OnGame2048Listener
{
private Game2048Layout mGame2048Layout;
private TextView mScore;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mScore = (TextView) findViewById(R.id.id_score);
mGame2048Layout = (Game2048Layout) findViewById(R.id.id_game2048);
mGame2048Layout.setOnGame2048Listener(this);
}
@Override
public void onScoreChange(int score)
{
mScore.setText("SCORE: " + score);
}
@Override
public void onGameOver()
{
new AlertDialog.Builder(this).setTitle(“GAME OVER”)
.setMessage("YOU HAVE GOT " + mScore.getText())
.setPositiveButton(“RESTART”, new OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
mGame2048Layout.restart();
}
}).setNegativeButton(“EXIT”, new OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
finish();
}
}).show();
}
}
很简单,代码主要就是设置个接口,当发生加分已经游戏结束时会交给Activity去处理~如果喜欢,你可以在一个界面放4个游戏~
当然了游戏Item的个数也可以动态设置~最后贴一个5*5游戏的截图
好了,2048到此结束,拿只笔开始设计,然后根据自定义View的经验去写,相信你可以学会不少东西~~~
并且我们的View是抽取出来的,其实换成图片也很简单~~
今天又看了war3十大经典战役,献上war3版,代码就不贴了,改动也就几行代码,贴个截图,纪念我们曾经的war3~~~:
额,咋都弄成5*5了大家可以把mColumn改为4~~
---------------------------------------------------------------------------------------------------------
我建了一个QQ群,方便大家交流。群号:55032675
----------------------------------------------------------------------------------------------------------
博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
最后
总而言之,Android开发行业变化太快,作为技术人员就要保持终生学习的态度,让学习力成为核心竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才能跟紧行业的步伐,才能不被时代所淘汰。
在这里我分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-uare6WCn-1712823769393)]
[外链图片转存中…(img-kAiNLN9z-1712823769393)]
[外链图片转存中…(img-7NITgR3O-1712823769393)]
[外链图片转存中…(img-aNTSaSTw-1712823769394)]
[外链图片转存中…(img-HHaN2B6Z-1712823769394)]
[外链图片转存中…(img-jJqhZQYz-1712823769394)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-QsAdKCvo-1712823769395)]
最后
总而言之,Android开发行业变化太快,作为技术人员就要保持终生学习的态度,让学习力成为核心竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才能跟紧行业的步伐,才能不被时代所淘汰。
在这里我分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
[外链图片转存中…(img-v15oCpx3-1712823769395)]
[外链图片转存中…(img-eiOz0AIh-1712823769395)]
[外链图片转存中…(img-tPbjnO1Y-1712823769396)]
还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-gL9aDddx-1712823769396)]