Harmony应用开发文档中为Java开发者提供了6种UI布局,可以满足开发者的大部分需求。但是有一个问题是:这些布局一旦显示,用户便无法进行调整。我们开发一个自定义布局来解决这个问题。以下是效果演示:
内容比较多,今天是第一部分,先实现一个按比例分配显示空间的布局。
定义DynamicLayout类
自定义布局类除了要继承ComponentContainer类的功能之外,还要实现
EstimateSizeListener和ArrangeListener接口的功能。
public class DynamicLayout extends ComponentContainerimplements 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计算每个子窗口的位置,最后是计算布局自身的大小。
@Overridepublic 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方法为组件设定坐标。只有经过这一步用户才能看到布局结果。
@Overridepublic 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 语言进行图像分析、数据处理工作的读者可以直接以本书中的示例为基础,迅速构建自己的系统架构。
觉得本文有帮助?请分享给更多人。
关注微信公众号【面向对象思考】轻松学习每一天!
面向对象开发,面向对象思考!
我正在尝试设置一个puppet节点,但rubygems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由rubygems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby1.9+ 关于ruby-主要:Objectwhenrun
我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R