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

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

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


一、效果演示

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


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


最终实现效果:

956c5aa0c584d1602d4aff118643e69a.gif



当前实现效果:


4cdcf00b9043a598b0f87268f65672fc.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引入自定义组件的时候,单个组件也是可以直接引入的);报错原因是,我最外层没有放置布局导致不能直接识别单个组件,但是如果我加上一个布局的话,文件不会报错,但是在我的棋盘上不能拿到这个棋子的组件;

c3824b70cf0c5002d09023476624d587.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();
}

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


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


第二个坑


49c0d24a5721d4411229f15a075483a3.png

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


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


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


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

0895ee10223ad01eeaab4ce80f7d064d.png


总结

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


我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3w24mefasg8wc


相关文章
|
21天前
|
存储 数据安全/隐私保护
鸿蒙开发:自定义一个动态输入框
在鸿蒙开发中,如何实现这一效果呢,最重要的解决两个问题,第一个问题是,如何在上一个输入框输入完之后,焦点切换至下一个输入框中,第二个问题是,如何禁止已经输入的输入框的焦点,两个问题解决完之后,其他的就很是简单了。
45 13
鸿蒙开发:自定义一个动态输入框
|
24天前
|
前端开发 搜索推荐 开发者
「Mac畅玩鸿蒙与硬件20」鸿蒙UI组件篇10 - Canvas 组件自定义绘图
Canvas 组件在鸿蒙应用中用于绘制自定义图形,提供丰富的绘制功能和灵活的定制能力。通过 Canvas,可以创建矩形、圆形、路径、文本等基础图形,为鸿蒙应用增添个性化的视觉效果。本篇将介绍 Canvas 组件的基础操作,涵盖绘制矩形、圆形、路径和文本的实例。
60 12
「Mac畅玩鸿蒙与硬件20」鸿蒙UI组件篇10 - Canvas 组件自定义绘图
|
24天前
|
搜索推荐 前端开发 开发者
「Mac畅玩鸿蒙与硬件19」鸿蒙UI组件篇9 - 自定义动画实现
自定义动画让开发者可以设计更加个性化和复杂的动画效果,适合表现独特的界面元素。鸿蒙提供了丰富的工具,支持通过自定义路径和时间控制来创建复杂的动画运动。本篇将带你学习如何通过自定义动画实现更多样化的效果。
70 11
「Mac畅玩鸿蒙与硬件19」鸿蒙UI组件篇9 - 自定义动画实现
|
24天前
|
UED 开发者
「Mac畅玩鸿蒙与硬件18」鸿蒙UI组件篇8 - 高级动画效果与缓动控制
高级动画可以显著提升用户体验,为应用界面带来更流畅的视觉效果。本篇将深入介绍鸿蒙框架的高级动画,包括弹性动画、透明度渐变和旋转缩放组合动画等示例。
60 12
「Mac畅玩鸿蒙与硬件18」鸿蒙UI组件篇8 - 高级动画效果与缓动控制
|
20天前
|
UED
「Mac畅玩鸿蒙与硬件31」UI互动应用篇8 - 自定义评分星级组件
本篇将带你实现一个自定义评分星级组件,用户可以通过点击星星进行评分,并实时显示评分结果。为了让界面更具吸引力,我们还将添加一只小猫图片作为评分的背景装饰。
57 6
「Mac畅玩鸿蒙与硬件31」UI互动应用篇8 - 自定义评分星级组件
|
24天前
|
UED
「Mac畅玩鸿蒙与硬件17」鸿蒙UI组件篇7 - Animation 组件基础
在应用开发中,动画效果可以增强用户体验。鸿蒙框架提供了 translate、scale 和 rotate 等动画功能,允许对组件进行平移、缩放和旋转等操作。本篇将介绍 Animation 组件的基础知识和示例代码。
73 10
「Mac畅玩鸿蒙与硬件17」鸿蒙UI组件篇7 - Animation 组件基础
|
22天前
|
前端开发 开发者
「Mac畅玩鸿蒙与硬件21」鸿蒙UI组件篇11 - Canvas 组件的静态进阶应用
在鸿蒙应用开发中,Canvas 组件不仅用于基础绘图,还提供了处理复杂路径和渐变效果的多种手段,帮助开发者实现精美的静态图形。本篇将介绍如何在 Canvas 中绘制复杂路径、创建渐变填充效果。
44 7
「Mac畅玩鸿蒙与硬件21」鸿蒙UI组件篇11 - Canvas 组件的静态进阶应用
|
24天前
|
大数据 UED
「Mac畅玩鸿蒙与硬件16」鸿蒙UI组件篇6 - List 和 Grid 组件展示数据列表
List 和 Grid 是鸿蒙开发中的核心组件,用于展示动态数据。List 适合展示垂直或水平排列的数据列表,而 Grid 则适用于展示商品或图片的网格布局。本篇将展示如何封装组件,并通过按钮实现布局切换,提升界面的灵活性和用户体验。
60 9
「Mac畅玩鸿蒙与硬件16」鸿蒙UI组件篇6 - List 和 Grid 组件展示数据列表
|
22天前
|
前端开发 开发者
「Mac畅玩鸿蒙与硬件22」鸿蒙UI组件篇12 - Canvas 组件的动态进阶应用
在鸿蒙应用中,Canvas 组件可以实现丰富的动态效果,适合用于动画和实时更新的场景。本篇将介绍如何在 Canvas 中实现动画循环、动态进度条、旋转和缩放动画,以及性能优化策略。
45 6
「Mac畅玩鸿蒙与硬件22」鸿蒙UI组件篇12 - Canvas 组件的动态进阶应用
|
22天前
|
前端开发 开发者
「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用
自定义组件可以帮助开发者实现复用性强、逻辑清晰的界面模块。通过自定义组件,鸿蒙应用能够提高代码的可维护性,并简化复杂布局的构建。本篇将介绍如何创建自定义组件,如何向组件传递数据,以及如何在不同页面间复用这些组件。
34 5
「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用