专栏首页C++核心准则原文翻译自学HarmonyOS应用开发(66)- 自定义布局(1)

自学HarmonyOS应用开发(66)- 自定义布局(1)

Harmony应用开发文档中为Java开发者提供了6种UI布局,可以满足开发者的大部分需求。但是有一个问题是:这些布局一旦显示,用户便无法进行调整。我们开发一个自定义布局来解决这个问题。以下是效果演示:

https://v.qq.com/x/page/d3270bzjw6r.html

内容比较多,今天是第一部分,先实现一个按比例分配显示空间的布局。

定义DynamicLayout类

自定义布局类除了要继承ComponentContainer类的功能之外,还要实现

EstimateSizeListener和ArrangeListener接口的功能。

public class DynamicLayout extends ComponentContainer
        implements ComponentContainer.EstimateSizeListener,
        ComponentContainer.ArrangeListener {
    public DynamicLayout(Context context) {
        super(context);
    }
    //如需支持xml创建自定义布局,必须添加该构造方法
    public DynamicLayout(Context context, AttrSet attrSet) {
        super(context, attrSet);
        setEstimateSizeListener(this);
        setArrangeListener(this);
        setDraggedListener(DRAG_HORIZONTAL_VERTICAL, dragListener);
    }
}

处理和管理weight属性

DynamicLayout的基本功能和DirectionalLayout相似,可以使用weight属性指定每个组件的在整个布局所占比重。做法是定义一个可以保管weight属性的LayoutConfig类并重写DyamicLayout的createLayoutConfig方法:

public class LayoutConfig extends ComponentContainer.LayoutConfig{
    int weight = 0;
    LayoutConfig(Context context, AttrSet attrSet){
        super(context, attrSet);
        Optional<Attr> attr = attrSet.getAttr("weight");
        if(attr.isPresent()){
            weight = attr.get().getIntegerValue();
        }
    }
}

public ComponentContainer.LayoutConfig createLayoutConfig(Context context, AttrSet attrSet){
    return new LayoutConfig(context, attrSet);
}

架构会在必要的时候调用这个createLayoutConfig方法,从而保证所有DynamicLayout布局的下级组件都用这个LayoutConfig管理自己的属性。

实现ComponentContainer接口

EstimateSizeListener接口只有一个onEstimateSize方法,按照华为文档的做法,首先调用measureChildren方法计算每个子窗口的大小,然后通过addChild计算每个子窗口的位置,最后是计算布局自身的大小。

@Override
public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
    invalidateValues();
    //通知子组件进行测量
    measureChildren(widthEstimatedConfig, heightEstimatedConfig);
    //关联子组件的索引与其布局数据
    for (int idx = 0; idx < getChildCount(); idx++) {
        Component childView = getComponentAt(idx);
        addChild(childView, idx, EstimateSpec.getSize(widthEstimatedConfig));
    }
    //测量自身
    measureSelf(widthEstimatedConfig, heightEstimatedConfig);

    measureChildrenWithWeight(widthEstimatedConfig, heightEstimatedConfig);
    invalidateValues();
    //关联子组件的索引与其布局数据
    for (int idx = 0; idx < getChildCount(); idx++) {
        Component childView = getComponentAt(idx);
        addChild(childView, idx, EstimateSpec.getSize(widthEstimatedConfig));
    }
    //测量自身
    measureSelf(widthEstimatedConfig, heightEstimatedConfig);
    return true;
}

为了增加根据weight值计算子窗口高度的功能,又增加了从14行开始的内容,细节我们在下面介绍每个方法时说明。

初始化布局数据

invalidaValuse方法用于初始化每个组件布局信息的数据。xx和yy分别是最后一个组件的右下角坐标。maxWidth,maxHeight是组件摆放完成之后的最大宽度和高度。axis用于管理所有组件的布局信息。

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

计算每个组件高度和宽度
下面的代码和官方文档中的代码基本相同,只是增加了第2行和第28-30行,
计算所有weight属性的合计值。这里的前提是所以组件排成纵列。

private void measureChildren(int widthEstimatedConfig, int heightEstimatedConfig) {
    total_weight = 0;
    for (int idx = 0; idx < getChildCount(); idx++) {
        Component childView = getComponentAt(idx);
        if (childView != null) {
            DynamicLayout.LayoutConfig lc = (DynamicLayout.LayoutConfig)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);
                if(lc.height == 0 && lc.weight > 0){
                    total_weight += lc.weight;
                }
            }
            childView.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}


计算每个组件的位置
按照纵列方式摆放所有组件。对官方文档中的代码进行了修改。可能还有改善的余地。
private void addChild(Component 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();
    xx = 0;
    axis.put(id, layout);
    yy += Math.max(lastHeight, layout.height + component.getMarginBottom());
    maxWidth = Math.max(maxWidth, layout.positionX + layout.width + component.getMarginRight());
    maxHeight = Math.max(maxHeight, layout.positionY + layout.height + component.getMarginBottom());
}

计算结果会保存在axis中,后面的介绍的onArrange方法会用到。

计算布局自身的大小

这段代码和官方文档完全相同。

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

为使用weight属性的组件计算高度

这段代码是对measureChildren稍加修改得来的。第2-4行根据之前计算布局时得到的的布局高度减去组件总高度计算出可以分配给指定了weight属性的组件的高度值,然后用它除以之前计算的总weight值,得了每个weight单位对应的像素数。

private void measureChildrenWithWeight(int widthEstimatedConfig, int heightEstimatedConfig) {
    int layout_height = getEstimatedHeight();
    int weight_height = layout_height - maxHeight;
    weight_rate = (double)weight_height / total_weight;
    for (int idx = 0; idx < getChildCount(); idx++) {
        Component childView = getComponentAt(idx);
        if (childView != null) {
            DynamicLayout.LayoutConfig lc = (DynamicLayout.LayoutConfig)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 {
                if(lc.height ==0 && lc.weight >0){
                    childHeightMeasureSpec = EstimateSpec.getSizeWithMode((int)(lc.weight * weight_rate), EstimateSpec.PRECISE);
                }
                else{
                    childHeightMeasureSpec = EstimateSpec.getSizeWithMode(lc.height, EstimateSpec.PRECISE);
                }
            }
            childView.estimateSize(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

接下来在在第29行为使用了weight属性的组件计算高度值。通过这种方式实现了根据weight值分配布局空间的功能。

需要说明的是,由于目前还无法了解Harmony架构和布局组件互动的所有细节,有些代码只是权宜之计,可能有很多冗余处理。请各位读者理解。

反映布局计算结果

Harmony架构通过EstimateSizeListener接口的onEstimateSize方法计算出的每个组件的布局结果之后,还会调用ArrangeListener接口的onArrange方法为组件设定坐标。只有经过这一步用户才能看到布局结果。

@Override
public boolean onArrange(int left, int top, int width, int height) {
    // 对各个子组件进行布局
    for (int idx = 0; idx < getChildCount(); idx++) {
        Component childView = getComponentAt(idx);
        Layout layout = axis.get(idx);
        if (layout != null) {
            childView.arrange(left + layout.positionX, top + layout.positionY, layout.width, layout.height);
        }
    }
    return true;
}

参考资料

自定义布局

https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ui-java-custom-layouts-0000001092683918

参考代码

完整代码可以从以下链接下载:

https://github.com/xueweiguo/Harmony/tree/master/FileBrowser

作者著作介绍

《实战Python设计模式》是作者去年3月份出版的技术书籍,该书利用Python 的标准GUI 工具包tkinter,通过可执行的示例对23 个设计模式逐个进行说明。这样一方面可以使读者了解真实的软件开发工作中每个设计模式的运用场景和想要解决的问题;另一方面通过对这些问题的解决过程进行说明,让读者明白在编写代码时如何判断使用设计模式的利弊,并合理运用设计模式。

对设计模式感兴趣而且希望随学随用的读者通过本书可以快速跨越从理解到运用的门槛;希望学习Python GUI 编程的读者可以将本书中的示例作为设计和开发的参考;使用Python 语言进行图像分析、数据处理工作的读者可以直接以本书中的示例为基础,迅速构建自己的系统架构。

本文分享自微信公众号 - 面向对象思考(OOThinkingDalian)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 自学HarmonyOS应用开发(67)- 自定义布局(2)

    除了这两个限制之外,调整对象组件的类型/个数,分隔的组件的高度都可以任意指定。够灵活了吧。

    面向对象思考
  • 自学HarmonyOS应用开发(47)- 自定义switch组件

    HarmonyOS应用开发都会用到各种各样的UI组件,开发者可以根据需求在布局文件设定UI组件各种属性。但是需求是多种多样

    面向对象思考
  • 自学鸿蒙应用开发(33)- 在布局中使用自定义UI组件

    组建容器类ArcProgressBarContainer负责协调每个ArcProgressBar的描画动作。

    面向对象思考
  • 自学鸿蒙应用开发(30)- 自定义UI组件(1)

    任何一种开发工具也不可能为开发者提供所有的组件,根据现有组件定义自己的组件也就成为必需。接下来的几篇文章我们定义一个多层圆弧形进度条。本文是第一篇。

    面向对象思考
  • 自学HarmonyOS应用开发(54)- 校正定位偏差

    经过一番调查,结论是gps信号使用的是WGS-84坐标系,而高德地图使用的是GCJ-02火星坐标系,只有经过坐标变换才能显示正确的位置。这方面的文章网上有很多,...

    面向对象思考
  • 自学鸿蒙应用开发(26)- 自定义CommonDialog

    上一篇文章中说过,直接使用鸿蒙系统中的CommonDialog大致是下面的效果:

    面向对象思考
  • 自学鸿蒙应用开发(27)- 自定义ListDialog

    这个效果过于简陋,无法用于实际的产品开发。本文介绍如何定制自己的ListDialog。还是先看演示视频:

    面向对象思考
  • 【HarmonyOS 专题】02 搭建简单登录页面

    和尚在搭建完 HarmonyOS 环境之后,有很长时间没有研究过 HarmonyOS,DevEco Studio 已经更新了多个版本,和尚在升级完 ID...

    阿策小和尚
  • 技术分析 | HarmonyOS到底是不是Android套皮?

    最近鸿蒙系统关注度好高,支持与反对、看好和看衰、「自主的全场景分布式系统」和「Android套壳」各执一词,吵的不可开交。

    刘盼
  • 自学HarmonyOS应用开发(48)- Tablist组件进阶

    但是有一个问题是这篇文章,包括HarmonyOS应用开发的官方文档都只是实现了Tab切换的基本功能,对于每个Tab页内组件的处理没有详细说明。本文就来补上这个短...

    面向对象思考
  • 自学鸿蒙应用开发(4)- 画面布局

    在原有布局基础上,增加另外的DirectionalLayout管理图像和文字,并增加三处Component用于调整个要素之间的间隔。

    面向对象思考
  • 【第22期】HarmonyOS应用开发(基础篇)

    这不就是说,以后华为手机都是鸿蒙系统了嘛?鸿蒙还发出了一条视频,视频中显示2021年6月2号将开启鸿蒙操作系统及华为全场景新品发布会。预计现在支持EMUI11升...

    siberiawolf
  • 自学HarmonyOS应用开发(64)- 处理屏幕旋转

    旋转屏幕是手机用户的一个日常操作,本文介绍如何在屏幕旋转时自动调整屏幕布局的方法。效果如下:

    面向对象思考
  • 自学HarmonyOS应用开发(59)- 处理拖动事件

    在Harmony应用中通过实现Component.DraggedListener接口处理拖动事件,这个接口的方法一共有6个,这里我们只是用其中的3个:

    面向对象思考
  • 自学HarmonyOS应用开发(57)- 与Service进行交互

    构建自己的Connection类 StopWatchServiceConnection类的主要功能有两个:一是接受连接成功通知并获取服务端传过来的用于通信的IR...

    面向对象思考
  • 自学HarmonyOS应用开发(49)- 引入地图功能

    秒表应用的功能就是计时,其中有一种情况就是计算地图上两点之间移动的时间。但是作者在实际使用这个应用的时候,经常会忘了在预定地点开始和停止计时。解决这个问题的想法...

    面向对象思考
  • 自学HarmonyOS应用开发(53)- 获取当前位置

    在registerLocationEvent方法用来注册一个单次定位事件请求;在定位事件响应对象中我们将获得的位置信息通知给地图对象。

    面向对象思考
  • 程序员看华为HarmonyOS首发

    HarmonyOS代码正式开源,9月10日下午朋友圈散布着这条消息,科技圈炸锅了。各种声音的都有,我也挺好奇的,目前Android、iOS一统江湖,Harmon...

    马上就说
  • Android 自定义流布局。使用开源库SimpleFlowLayout

    实际项目中需要实现一个 热门搜索 的栏目,类似下图: 由于 子项(子view) 中的文字是可变的,一行能显示的 子项 的个数也无法确定。需要支持自动换行和计算...

    zhangyunfeiVir

扫码关注云+社区

领取腾讯云代金券