Android案例手册 - 实现一个华容道拼图游戏
👉关于作者
众所周知,人生是一个漫长的流程,不断克服困难,不断反思前进的过程。在这个过程中会产生很多对于人生的质疑和思考,于是我决定将自己的思考,经验和故事全部分享出来,以此寻找共鸣!!!
专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站、工具、素材、源码、游戏等)
欢迎关注公众号【空名先生】获取更多资源和交流!
👉前提
这是小空坚持写的Android新手向系列,欢迎品尝。
新手(√√√)
大佬(√)
👉实践过程
Hello,大家好啊,我是小空,前两天我们学习了很多基础内容,今天我们学习点稍微复杂的,提升下自己,刺激一下。
九宫格游戏,或者说华容道游戏,是传承了很久的游戏了,今天我们就用Android实现一下吧。
先看效果图
从图中我们做出如下分析:
-
展示N*N-1的数字列表,并且是随机排序的,还要有一个空置的view也要创建
-
每个数字都拥有点击事件,所以每个数字应该都是独立的View,然后所有数字嵌套在ViewGroup中
-
每个数字View都有移动动画并且有上下左右的区分,且移动距离为自身View的宽度
-
每个View记录自己正确的位置,对外提供方法移动该view位置,当移动的位置和记录的位置一直表示正确,回调出去
-
ViewGroup进行for循环创建指定数量的子View,并且设置好随机位置
-
子View回调出来的点击事件记录正确个数,满足条件则再次回调更上层做业务处理。
😜使用
我们先来看看使用方式:
<com.akitaka.math.ui.widget.KlotskiViewGroup
android:id="@+id/pv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#dddddd" />
😜子View的移动
😜ViewGroup创建棋子
😜全部源码
/**
* 华容道的实体类
*/
public class ModelKlotskiPosition {
/**
* X轴定位
*/
private int x;
/**
* Y轴定位
*/
private int y;
public ModelKlotskiPosition(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof ModelKlotskiPosition)) {
return false;
}
ModelKlotskiPosition o = (ModelKlotskiPosition) obj;
return x == o.x && y == o.y;
}
}
@SuppressLint("ViewConstructor")
public class KlotskiChildView extends View {
/**
* 当前数字
*/
private int intNumber;
/**
* 正确的定位
*/
private ModelKlotskiPosition positionCorrect;
/**
* 当前的定位
*/
private ModelKlotskiPosition positionCurrent;
/**
* 当前View的长款
*/
private int length;
/**
* 画笔
*/
private Paint paint;
/**
* 位置更改监听
*/
private OnPositionChangedListener listener;
/**
* 当前定位是否正确
*/
private boolean correct = true;
/**
* 初始化
*
* @param context 上下文对象
* @param intDifficulty 难度系数
* @param intNumber 数字
*/
public KlotskiChildView(Context context, int intDifficulty, int intNumber, OnPositionChangedListener listener) {
super(context);
init(intDifficulty, intNumber, listener);
}
private void init(int intDifficulty, int intNumber, OnPositionChangedListener listener) {
this.intNumber = intNumber;
//根据难度系数与棋子的数字,得出棋子的正确坐标
int x = intNumber % intDifficulty == 0 ? intDifficulty : intNumber % intDifficulty;
int y = (intNumber - 1) / intDifficulty + 1;
positionCorrect = new ModelKlotskiPosition(x, y);
positionCurrent = new ModelKlotskiPosition(x, y);
//初始化 画笔
paint = new Paint();
paint.setColor(Color.BLUE);
paint.setTextSize(100f * (4f / intDifficulty));
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
this.listener = listener;
}
/**
* 设置棋子当前位置
*/
public void setCurrentPosition(int x, int y) {
if (positionCurrent == null) {
positionCurrent = new ModelKlotskiPosition(positionCorrect.getX(), positionCorrect.getY());
}
positionCurrent.setX(x);
positionCurrent.setY(y);
verifyCorrect();
}
@Override
protected void onDraw(Canvas canvas) {
//将画布坐标系移到中间
canvas.translate(length / 2f, length / 2f);
//将数字画在画布上
@SuppressLint("DrawAllocation") Rect rect = new Rect();
String s = String.valueOf(intNumber);
paint.getTextBounds(s, 0, s.length(), rect);
canvas.drawText(s, -rect.width() / 2f, rect.height() / 2f, paint);
}
/**
* X轴移动
*
* @param left 是否向左移动
*/
public void moveX(boolean left) {
final int currX = getLeft();
//目标X位置
int targetX = currX + (left ? -length : length);
ValueAnimator valueAnimator = ValueAnimator.ofInt(currX, targetX);
valueAnimator.addUpdateListener(animation -> {
int value = (int) animation.getAnimatedValue();
//重新布局自己
layout(value, getTop(), value + length, getBottom());
});
valueAnimator.setDuration(200);
valueAnimator.start();
positionCurrent.setX(positionCurrent.getX() + (left ? -1 : 1));
verifyCorrect();
}
/**
* Y轴移动
*
* @param top 是否向上移动
*/
public void moveY(boolean top) {
final int currY = getTop();
int targetY = currY + (top ? -length : length);
ValueAnimator valueAnimator = ValueAnimator.ofInt(currY, targetY);
valueAnimator.addUpdateListener(animation -> {
int value = (int) animation.getAnimatedValue();
layout(getLeft(), value, getRight(), value + length);
});
valueAnimator.setDuration(200);
valueAnimator.start();
positionCurrent.setY(positionCurrent.getY() + (top ? -1 : 1));
verifyCorrect();
}
/**
* 验证当前位置是否正确,并根据逻辑调用改变监听
*/
private void verifyCorrect() {
if (correct != (positionCorrect.equals(positionCurrent))) {
correct = !correct;
listener.onPositionChanged(correct);
}
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public ModelKlotskiPosition getCorrectPosition() {
return positionCorrect;
}
public ModelKlotskiPosition getCurrentPosition() {
return positionCurrent;
}
public interface OnPositionChangedListener {
/**
* 位置调动时调用
*
* @param correct 位置是否正确
*/
void onPositionChanged(boolean correct);
}
}
/**
* @author 芝麻粒儿
*/
public class KlotskiViewGroup extends ViewGroup implements KlotskiChildView.OnPositionChangedListener {
/**
* 难度系数
* 默认为3
*/
private int intDifficulty = 3;
/**
* 棋子正确的数量
*/
private int intCorrectCount = 0;
/**
* 空闲位置
*/
private ModelKlotskiPosition idlePosition;
/**
* 游戏结束监听
*/
private OnPlayOverListener listener;
public KlotskiViewGroup(Context context) {
super(context);
}
public KlotskiViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public KlotskiViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void init() {
intCorrectCount = intDifficulty * intDifficulty - 1;
//初始化空格定位,默认为最后一个格子
idlePosition = new ModelKlotskiPosition(intDifficulty, intDifficulty);
//清空所有child 如果不清空,会出现底层背景重叠的问题
removeAllViews();
for (int i = 1; i < intDifficulty * intDifficulty; i++) {
//创建棋子
KlotskiChildView pieceView = new KlotskiChildView(getContext(), intDifficulty, i, this);
//给棋子添加布局属性
pieceView.setBackgroundResource(R.drawable.bg_piece);
LayoutParams layoutParams = new LayoutParams(pieceView.getLength(), pieceView.getLength());
pieceView.setLayoutParams(layoutParams);
//将棋子添加有序临时数组中
addView(pieceView);
//设置棋子的点击事件
pieceView.setOnClickListener(v -> processPieceClick((KlotskiChildView) v));
}
upsetPieces();
}
/**
* 打乱棋子
* 之前使用的随机数打乱法,会导致最终无解
*/
public void upsetPieces() {
//打乱次数,难度系数的平方
int count = intDifficulty * intDifficulty * 10;
do {
//随机数,模拟点击位置
int round = (int) Math.floor(Math.random() * getChildCount());
//根据难度系数与模拟点击的位置数字,得出棋子的正确坐标
int randomX = round % intDifficulty == 0 ? intDifficulty : round % intDifficulty;
int randomY = (round - 1) / intDifficulty + 1;
//空格坐标
int idleX = idlePosition.getX();
int idleY = idlePosition.getY();
if (randomX == idleX && randomY != idleY) {
count--;
idlePosition.setY(randomY);
for (int i = 0; i < getChildCount(); i++) {
KlotskiChildView pieceView = (KlotskiChildView) getChildAt(i);
int currX = pieceView.getCurrentPosition().getX();
int currY = pieceView.getCurrentPosition().getY();
if (currX == idleX) {
if (currY <= randomY && currY > idleY) {
pieceView.setCurrentPosition(currX, currY - 1);
} else if (currY >= randomY && currY < idleY) {
pieceView.setCurrentPosition(currX, currY + 1);
}
}
}
} else if (randomY == idleY && randomX != idleX) {
count--;
idlePosition.setX(randomX);
for (int i = 0; i < getChildCount(); i++) {
KlotskiChildView pieceView = (KlotskiChildView) getChildAt(i);
int currY = pieceView.getCurrentPosition().getY();
int currX = pieceView.getCurrentPosition().getX();
if (currY == idleY) {
if (currX <= randomX && currX > idleX) {
pieceView.setCurrentPosition(currX - 1, currY);
} else if (currX >= randomX && currX < idleX) {
pieceView.setCurrentPosition(currX + 1, currY);
}
}
}
}
} while (count >= 0);
}
/**
* 处理棋子的点击事件
*
*/
private void processPieceClick(KlotskiChildView pieceView) {
ModelKlotskiPosition clickPosition = pieceView.getCurrentPosition();
int idleX = idlePosition.getX();
int idleY = idlePosition.getY();
//若点击的棋子与空格在X轴交汇,则为上下移动
if (clickPosition.getX() == idleX) {
//将空格定位到点击的棋子位置
idlePosition.setY(clickPosition.getY());
//遍历棋子,找出被点击的棋子与原空格位置之间的所有棋子,一起移动
for (int i = 0; i < getChildCount(); i++) {
KlotskiChildView pv = (KlotskiChildView) getChildAt(i);
int currX = pv.getCurrentPosition().getX();
int currY = pv.getCurrentPosition().getY();
if (currX == idleX) {
if (clickPosition.getY() > idleY && currY <= clickPosition.getY() && currY > idleY) {
pv.moveY(true);
} else if (clickPosition.getY() < idleY && currY >= clickPosition.getY() && currY < idleY) {
pv.moveY(false);
}
}
}
}
//此处逻辑与上面类似,不重复
else if (clickPosition.getY() == idleY) {
idlePosition.setX(clickPosition.getX());
for (int i = 0; i < getChildCount(); i++) {
KlotskiChildView pv = (KlotskiChildView) getChildAt(i);
int currY = pv.getCurrentPosition().getY();
int currX = pv.getCurrentPosition().getX();
if (currY == idleY) {
if (clickPosition.getX() > idleX && currX <= clickPosition.getX() && currX > idleX) {
pv.moveX(true);
} else if (clickPosition.getX() < idleX && currX >= clickPosition.getX() && currX < idleX) {
pv.moveX(false);
}
}
}
}
}
/**
* 设置难度系数
*/
public void setDifficulty(int dif) {
intDifficulty = dif;
init();
invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//将宽高设置为统一长度,取最小值
int defaultWidth = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int defaultHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
int minSpec = defaultWidth < defaultHeight ? widthMeasureSpec : heightMeasureSpec;
super.onMeasure(minSpec, minSpec);
int childLength = Math.min(defaultWidth, defaultHeight) / intDifficulty;
for (int i = 0; i < getChildCount(); i++) {
((KlotskiChildView) getChildAt(i)).setLength(childLength);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//遍历所有的child,将子View依次排开
for (int i = 0; i < getChildCount(); i++) {
KlotskiChildView child = (KlotskiChildView) getChildAt(i);
ModelKlotskiPosition cp = child.getCurrentPosition();
int cX = (cp.getX() - 1) * child.getLength();
int cY = (cp.getY() - 1) * child.getLength();
child.layout(cX, cY, cX + child.getLength(), cY + child.getLength());
}
}
@Override
public void onPositionChanged(boolean correct) {
if (correct) {
intCorrectCount++;
} else {
intCorrectCount--;
}
if (intCorrectCount >= ((intDifficulty * intDifficulty) - 1)) {
if (listener != null) {
listener.onPlayOver(0);
}
}
}
public void setListener(OnPlayOverListener listener) {
this.listener = listener;
}
public interface OnPlayOverListener {
/**
* 游戏结束回调
*
* @param state 结束状态,目前默认为0
* 预留后面添加超时失败等状态
*/
void onPlayOver(int state);
}
}
📢作者:小空和小芝中的小空
📢转载说明-务必注明来源:芝麻粒儿 的个人主页 - 专栏 - 掘金 (juejin.cn)
📢这位道友请留步☁️,我观你气度不凡,谈吐间隐隐有王者霸气💚,日后定有一番大作为📝!!!旁边有点赞👍收藏🌟今日传你,点了吧,未来你成功☀️,我分文不取,若不成功⚡️,也好回来找我。
- 点赞
- 收藏
- 关注作者
评论(0)