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

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

一、效果演示

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

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

最终实现效果:

动画.gif

当前实现效果:


动画2.gif

二、实现过程

暂时说一下现在的进度,每一个方块可以表示一个棋子,棋子的名称也就是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引入自定义组件的时候,单个组件也是可以直接引入的);报错原因是,我最外层没有放置布局导致不能直接识别单个组件,但是如果我加上一个布局的话,文件不会报错,但是在我的棋盘上不能拿到这个棋子的组件;


image.png

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

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

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

}

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

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

第二个坑

image.png

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PvmUPB0c-1634810943992)(C:\Users\HHCH\AppData\Roaming\Typora\typora-user-images\image-20211021175237912.png)]

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

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

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


image.png

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容