自学HarmonyOS应用开发(66)- 自定义布局(1)
Harmony应用开发文档中为Java开发者提供了6种UI布局,可以满足开发者的大部分需求。但是有一个问题是:这些布局一旦显示,用户便无法进行调整。我们开发一个自定义布局来解决这个问题。以下是效果演示:
内容比较多,今天是第一部分,先实现一个按比例分配显示空间的布局。
定义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计算每个子窗口的位置,最后是计算布局自身的大小。
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方法为组件设定坐标。只有经过这一步用户才能看到布局结果。
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 语言进行图像分析、数据处理工作的读者可以直接以本书中的示例为基础,迅速构建自己的系统架构。
觉得本文有帮助?请分享给更多人。
关注微信公众号【面向对象思考】轻松学习每一天!
面向对象开发,面向对象思考!