【本文正在参与优质创作者激励】前两天看到HarmonyOS开发者官网上发布的一个挑战HarmonyOS分布式趣味应用的帖子,然后有个想法想搞一个小游戏出来,结果三天的时间都卡在了自定义组件上,使用了各种方式方法去实现功能,但是还是没有达到预期的效果,暂时先做个小总结,其实坑有的时候真的很深…

一、效果演示

小应用其实也挺简单,以前也见到过,叫做数字华容道,当你把所在的数字以顺序放置完成后游戏结束。

其实属于益智类的小游戏了;

最终实现效果:

鸿蒙小游戏-数字华容道 自定义组件的踩坑记录 _xml

当前实现效果:

鸿蒙小游戏-数字华容道 自定义组件的踩坑记录 _java_02

二、实现过程

暂时说一下现在的进度,每一个方块可以表示一个棋子,棋子的名称也就是3*3的九宫格,1-9的数字,只是最后一个数字单独设置为空白。点击空白周围的棋子可以与这个空白棋子做一次位置调换,直到将所有棋子顺序排列完成为止。

这里先说一个这个棋子,棋子有两个东西需要被记住,一个是棋子的坐标就是在九宫格里面的位置,另一个就是棋子的名称;所以选择使用自定义组件的方式将坐标和名称进行一个绑定。

Position.java

/**
* 定义棋子的位置
*/
public class Position {
public int sizeX; // 总列数
public int sizeY; // 总行数
public int x; // 横坐标
public int y; // 纵坐标

public Position() {
}

public Position(int sizeX, int sizeY) {
this.sizeX = sizeX;
this.sizeY = sizeY;
}

public Position(int sizeX, int sizeY, int x, int y) {
this.sizeX = sizeX;
this.sizeY = sizeY;
this.x = x;
this.y = y;
}

public Position(Position orig) {
this(orig.sizeX, orig.sizeY, orig.x, orig.y);
}

/**
* 移动到下一个位置
*/
public boolean moveToNextPosition() {
if (x < sizeX - 1) {
x++;
} else if (y < sizeY - 1) {
x = 0;
y++;
} else {
return false;
}
return true;
}

@Override
public String toString() {
return "Position{" +
"x=" + x +
", y=" + y +
'}';
}
}

复制

CubeView.java

public class CubeView extends ComponentContainer {

private Position mPosition;
private int mNumber;

private Text mTextCub;
private int mTextSize = 20;

public CubeView(Context context) {
super(context);
init();
}

public CubeView(Context context, AttrSet attrSet) {
super(context, attrSet);
init();
}

private void init(){
Component component = LayoutScatter.getInstance(getContext()).parse(ResourceTable.Layout_cube_view_item, this, false);
mTextCub = (Text) component.findComponentById(ResourceTable.Id_tv_item);
mTextCub.setTextSize(mTextSize, Text.TextSizeType.VP);
}

public void setNumber(int n) {
mNumber = n;
mTextCub.setText(String.valueOf(n));
}


public int getNumber() {
return mNumber;
}

public Position getPosition() {
return mPosition;
}

public void setPosition(Position position) {
this.mPosition = position;
}

@Override
public String toString() {
return "CubeView{" +
"mPosition=" + mPosition +
", mNumber=" + mNumber +
'}';
}
}

复制

cube_view_item.xml

<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_content"
ohos:width="match_content">
<Text
ohos:id="$+id:tv_item"
ohos:height="100vp"
ohos:width="100vp"
ohos:background_element="$graphic:cube_view_bg"
ohos:text="1"
ohos:text_alignment="center"
ohos:text_color="$color:cubeViewStroke"
ohos:text_size="20vp">
></Text>
</DirectionalLayout>

复制

到这问题就来了,因为在代码中只是使用到了setText()方法,那么有人会问我为什么不直接继承Text组件,多写一个布局有点麻烦了不是?

第一个坑

这里就是第一个坑了,因为在以前写Android自定义控件的时候,对于简单的组件来说直接继承它的组件名称就可以了,不用去继承公共类然后再去使用布局去定位到里面的组件。原本我也是这么写的,CubeView直接继承Text没有毛病可以使用,可以看到两者间并无差别。

public class CubeView extends Text {

private Position mPosition;
private int mNumber;


public CubeView(Context context) {
super(context);
init();
}

public CubeView(Context context, AttrSet attrSet) {
super(context, attrSet);
init();
}

private void init(){

}

public void setNumber(int n) {
mNumber = n;
setText(String.valueOf(n));
}


public int getNumber() {
return mNumber;
}

public Position getPosition() {
return mPosition;
}

public void setPosition(Position position) {
this.mPosition = position;
}

@Override
public String toString() {
return "CubeView{" +
"mPosition=" + mPosition +
", mNumber=" + mNumber +
'}';
}
}

复制

但是在调用组件的时候出现了问题,因为我需要把这个棋子的组件添加到我的棋盘布局中,那么就需要先引入这个组件。引入组件后出问题了,布局报错(在原来Android引入自定义组件的时候,单个组件也是可以直接引入的);报错原因是,我最外层没有放置布局导致不能直接识别单个组件,但是如果我加上一个布局的话,文件不会报错,但是在我的棋盘上不能拿到这个棋子的组件;

鸿蒙小游戏-数字华容道 自定义组件的踩坑记录 _ide_03

为此我只能将棋子的自定义组件写成了布局引入方式。

到这里,棋子的开发工作也就基本做完了,下面要对棋盘进行布局。还是选择自定义组件的方式;

cube_view.xml

<?xml version="1.0" encoding="utf-8"?>
<com.example.codelabs_games_hrd.CubeView
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:background_element="$graphic:cube_view_bg"
ohos:height="100vp"
ohos:width="100vp"
ohos:id="$+id:title_bar_left"
ohos:text="1"
ohos:text_alignment="center"
ohos:text_color="$color:cubeViewStroke"
ohos:text_size="20vp"

>
</com.example.codelabs_games_hrd.CubeView>

复制

ability_game.xml

<?xml version="1.0" encoding="utf-8"?>
<StackLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="$color:cubeViewBg">

<com.example.codelabs_games_hrd.BoardView
ohos:id="$+id:board"
ohos:height="300vp"
ohos:width="300vp"
ohos:layout_alignment="center"
ohos:background_element="$color:boardViewBg">
</com.example.codelabs_games_hrd.BoardView>

<Text
ohos:id="$+id:tvCheat"
ohos:height="10vp"
ohos:width="10vp"></Text>

<Text
ohos:id="$+id:mask"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="$color:cubeViewBg"
ohos:text="123456789"
ohos:text_size="48vp"></Text>

</StackLayout>

复制

BoardView.java

public class BoardView extends ComponentContainer implements ComponentContainer.EstimateSizeListener, ComponentContainer.ArrangeListener {
private static final String TAG = "BoardView";
/**
* 每一行有多少个棋子
*/
private int mSizeX = 3;
/**
* 有多少行棋子
*/
private int mSizeY = 3;


private int maxWidth = 0;

private int maxHeight = 0;

private int mChildSize;

private Position mBlankPos;
private CubeView[] mChildren;

private OnFinishListener mFinishListener;

private int xx = 0;

private int yy = 0;

private int lastHeight = 0;

// 子组件索引与其布局数据的集合
private final Map<Integer, Layout> axis = new HashMap<>();

//位置及大小
private static class Layout {
int positionX = 0;
int positionY = 0;
int width = 0;
int height = 0;
}


private void invalidateValues() {
xx = 0;
yy = 0;
maxWidth = 0;
maxHeight = 0;
axis.clear();
}

public BoardView(Context context) {
super(context);
}

public BoardView(Context context, AttrSet attrs) {
super(context, attrs);
setEstimateSizeListener(this);
setArrangeListener(this);
init();
}

private void init() {
mChildSize = mSizeX * mSizeY - 1;
mChildren = new CubeView[mChildSize];
Position p = new Position(mSizeX, mSizeY);
for (int i = 0; i < mChildSize; i++) {
//添加棋子
CubeView view = (CubeView) LayoutScatter.getInstance(getContext()).parse(ResourceTable.Layout_cube_view, this, false);
view.setPosition(new Position(p));
view.setClickedListener(component -> moveChildToBlank(view));
addComponent(view);
p.moveToNextPosition();
mChildren[i] = view;
}
//最后一个空白棋子
mBlankPos = new Position(mSizeX, mSizeY, mSizeX - 1, mSizeY - 1);
}



public void setData(List<Integer> data) {
for (int i = 0; i < mChildSize; i++) {
CubeView view = (CubeView) getComponentAt(i);
view.setNumber(data.get(i));
}
}

//测量监听方法
@Override
public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
invalidateValues();
//测量子组件的大小
measureChildren( widthEstimatedConfig, heightEstimatedConfig);
//关联子组件的索引与其布局数据
for (int idx = 0; idx < getChildCount(); idx++) {
CubeView childView = (CubeView) getComponentAt(idx);
addChild(childView, idx, EstimateSpec.getSize(widthEstimatedConfig));
}
//测量本身大小
setEstimatedSize( widthEstimatedConfig, heightEstimatedConfig);


return true;
}

private void measureChildren(int widthEstimatedConfig, int heightEstimatedConfig) {
for (int idx = 0; idx < getChildCount(); idx++) {
CubeView childView = (CubeView) getComponentAt(idx);
if (childView != null) {
LayoutConfig lc = childView.getLayoutConfig();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
if (lc.width == LayoutConfig.MATCH_CONTENT) {
childWidthMeasureSpec = EstimateSpec.getSizeWithMode(lc.width, EstimateSpec.NOT_EXCEED);
} else if (lc.width == LayoutConfig.MATCH_PARENT) {
int parentWidth = EstimateSpec.getSize(widthEstimatedConfig);
int childWidth = parentWidth - childView.getMarginLeft() - childView.getMarginRight();
childWidthMeasureSpec = EstimateSpec.getSizeWithMode(childWidth, EstimateSpec.PRECISE);
} else {
childWidthMeasureSpec = EstimateSpec.getSizeWithMode(lc.width, EstimateSpec.PRECISE);
}

if (lc.height == LayoutConfig.MATCH_CONTENT) {
childHeightMeasureSpec = EstimateSpec.getSizeWithMode(lc.height, EstimateSpec.NOT_EXCEED);
} else if (lc.height == LayoutConfig.MATCH_PARENT) {
int parentHeight = EstimateSpec.getSize(heightEstimatedConfig);
int childHeight = parentHeight - childView.getMarginTop() - childView.getMarginBottom();
childHeightMeasureSpec = EstimateSpec.getSizeWithMode(childHeight, EstimateSpec.PRECISE);
} else {
childHeightMeasureSpec = EstimateSpec.getSizeWithMode(lc.height, EstimateSpec.PRECISE);
}
childView.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}


private void measureSelf(int widthEstimatedConfig, int heightEstimatedConfig) {
int widthSpce = EstimateSpec.getMode(widthEstimatedConfig);
int heightSpce = EstimateSpec.getMode(heightEstimatedConfig);
int widthConfig = 0;
switch (widthSpce) {
case EstimateSpec.UNCONSTRAINT:
case EstimateSpec.PRECISE:
int width = EstimateSpec.getSize(widthEstimatedConfig);
widthConfig = EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE);
break;
case EstimateSpec.NOT_EXCEED:
widthConfig = EstimateSpec.getSizeWithMode(maxWidth, EstimateSpec.PRECISE);
break;
default:
break;
}

int heightConfig = 0;
switch (heightSpce) {
case EstimateSpec.UNCONSTRAINT:
case EstimateSpec.PRECISE:
int height = EstimateSpec.getSize(heightEstimatedConfig);
heightConfig = EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE);
break;
case EstimateSpec.NOT_EXCEED:
heightConfig = EstimateSpec.getSizeWithMode(maxHeight, EstimateSpec.PRECISE);
break;
default:
break;
}
setEstimatedSize(widthConfig, heightConfig);
}



//每个棋子组件的位置及大小
@Override
public boolean onArrange(int l, int t, int r, int b) {

for (int idx = 0; idx < getChildCount(); idx++) {
Component childView = getComponentAt(idx);
Layout layout = axis.get(idx);
if (layout != null) {
childView.arrange(layout.positionX, layout.positionY, layout.width, layout.height);
}
}
return true;
}


private void addChild(CubeView component, int id, int layoutWidth) {
Layout layout = new Layout();
layout.positionX = xx + component.getMarginLeft();
layout.positionY = yy + component.getMarginTop();
layout.width = component.getEstimatedWidth();
layout.height = component.getEstimatedHeight();
if ((xx + layout.width) > layoutWidth) {
xx = 0;
yy += lastHeight;
lastHeight = 0;
layout.positionX = xx + component.getMarginLeft();
layout.positionY = yy + component.getMarginTop();
}
axis.put(id, layout);
lastHeight = Math.max(lastHeight, layout.height + component.getMarginBottom());
xx += layout.width + component.getMarginRight();
maxWidth = Math.max(maxWidth, layout.positionX + layout.width + component.getMarginRight());
maxHeight = Math.max(maxHeight, layout.positionY + layout.height + component.getMarginBottom());
}

//点击棋子后进行位置切换
public void moveChildToBlank(@org.jetbrains.annotations.NotNull CubeView child) {
Position childPos = child.getPosition();
Position dstPos = mBlankPos;
if (childPos.x == dstPos.x && Math.abs(childPos.y - dstPos.y) == 1 ||
childPos.y == dstPos.y && Math.abs(childPos.x - dstPos.x) == 1) {
child.setPosition(dstPos);
//component中没有对组件进行物理平移的方法
//setTranslationX(),setTranslationY()两个方法没有
child.setTranslationX(dstPos.x * xx);
child.setTranslationY(dstPos.y * yy);

mBlankPos = childPos;
mStepCounter.add();
}
checkPosition();
}

/**
* 检查所有格子位置是否正确
*/
private void checkPosition() {
if (mBlankPos.x != mSizeX - 1 || mBlankPos.y != mSizeY - 1) {
return;
}

for (CubeView child : mChildren) {
int num = child.getNumber();
int x = child.getPosition().x;
int y = child.getPosition().y;
if (y * mSizeX + x + 1 != num) {
return;
}
}

if (mFinishListener != null) {
mFinishListener.onFinished(mStepCounter.step);
}
for (CubeView child : mChildren) {
child.setClickable(false);
}
}

public void setOnFinishedListener(OnFinishListener l) {
mFinishListener = l;
}

public interface OnFinishListener {
void onFinished(int step);
}

public int getSizeX() {
return mSizeX;
}

public int getSizeY() {
return mSizeY;
}

/**
* 步数统计
*/
class StepCounter {
private int step = 0;

void add() {
step++;
}

void clear() {
step = 0;
}
}

private StepCounter mStepCounter = new StepCounter();

}

复制

棋盘的自定义布局也完成了。棋盘的布局稍微复杂一点,因为需要根据棋盘的大小计算每一个棋子的大小,还需要对棋子进行绑定,尤其是需要对最后一个棋子做空白处理。

然后点击棋子进行棋子的平移,平移后与其位置进行互换。

第二个坑

鸿蒙小游戏-数字华容道 自定义组件的踩坑记录 _ide_04

点击棋子进行位置平移,因为在API里面没有找到component公共组件下的平移方法,setTranslationX()/setTranslationY()方法,没有办法做到组件的物理位置平移,导致大家看到开头演示的效果,点击后与空白位置坐了切换但是重新对其进行物理位置赋值的时候没有办法去赋值,这个问题困扰了我两天。

现在还是没有解决掉,试着想想是不是可以使用TouchEvent事件一个滑动处理,不做点击事件做滑动事件。

最终现在项目的结构如下:

鸿蒙小游戏-数字华容道 自定义组件的踩坑记录 _自定义组件_05

总结

后面还会继续去完善,以至于到整个功能可以正常去使用,踩坑还是要踩的,总会有收获的时候…

【本文正在参与优质创作者激励】


鸿蒙小游戏-数字华容道 自定义组件的踩坑记录 _自定义组件_06