第3章 Java UI 应用开发中,界面设计和UI布局是必不可少的。任何一个应用都需要在屏幕上显示用户界面,包含用户可查看并与之交互的所有内容。在HarmonyOS中,可通过Java和JS两种方式布局UI。本章先讲述Java UI布局,在第5章将讲述JS UI布局。 在学习使用Java对UI进行设计之前,首先需要理解一下HarmonyOS的UI逻辑。依据功能和特点,可将HarmonyOS的UI组件分为两类: 单体组件Component和可用于装载其他组件的容器组件ComponentContainer。 UI组件根据一定的层级结构进行组合形成整体的UI。单体组件在未被添加到容器组件中时,既无法显示也无法交互,因此一个用户界面至少包含一个容器组件。此外,容器组件中不但可以添加单体组件,也可以添加其他的容器组件,这样就可以形成丰富多样的UI样式。HarmonyOS的UI视图树范例如图3.1所示。 图3.1HarmonyOS的UI视图树范例 单体组件和容器组件以树状的层级结构进行组织,这样的布局被称为视图树。视图树的特点是仅有一个根视图,其他视图有且仅有一个父节点,视图之间的关系受到父节点的规则约束。 3.1Java UI单体组件 本章主要讲解使用Java语言设置单体UI组件。 HarmonyOS常用的单体UI组件全部继承自Component类,Component类包含了对UI组件的全部常用方法,例如创建、更新、缩放、旋转及设置各类事件监听器。其他的UI组件,如Text、Button等,都是在Component类的基础上添加了对应的功能实现的。以下是常用单体组件的开发流程。 3.1.1Text组件 Text是用来显示字符串的组件,是应用开发中最基础的组件,在界面上显示为一块文本区域。下面学习Text的用法。 1. Java代码创建Text组件 首先按照第1章的介绍,创建一个Phone设备的Java模板,用来学习使用代码创建UI布局。在新建项目中的界面左侧找到Project项目栏,双击打开entry→src→main→项目Package名称(本书中为com.huawei.mytestapp)→Slice中的MainAbilitySlice.java文件,如图3.2所示。 图3.2MainAbilitySlice文件路径 在打开后的Java文件中找到onStart()方法,修改其中的内容,代码如下: //MainAbilitySlice.java public void onStart(Intent intent) { super.onStart(intent); //容器 DirectionalLayout myLayout = new DirectionalLayout(this); myLayout.setWidth(DirectionalLayout.LayoutConfig.MATCH_PARENT); myLayout.setHeight(DirectionalLayout.LayoutConfig.MATCH_PARENT); myLayout.setAlignment(LayoutAlignment.HORIZONTAL_CENTER); myLayout.setPadding(32,32,32,32); //Text组件 Text text = new Text(this); text.setWidth(DirectionalLayout.LayoutConfig.MATCH_CONTENT); text.setHeight(DirectionalLayout.LayoutConfig.MATCH_CONTENT); text.setText("My name is Text."); text.setTextSize(24, Text.TextSizeType.VP); myLayout.addComponent(text); super.setUIContent(myLayout); } 下面对代码的具体内容进行讲解。 图3.3Text组件的 运行效果 第一个代码块对容器组件进行了设置,页面容器组件的相关说明会在下一节中进行。第二个代码块设置并添加了一个Text组件,其中前5行代码创建并设置了一个Text组件,每一句的具体意义如下: 第1行: 实例化一个Text类的实例,实例名为text。 第2和第3行: 为text设置了宽和高属性,这样可以让组件的大小自动适配其内容(文本)的大小。 第4行: 为text添加要显示的文字信息My name is Text。 第5行: 为text的文本字体设置大小24vp。 至此,text的属性就设置完毕了,为了使text能够在手机屏幕上显示,还需要执行super.setUIContent(myLayout),将创建好的Text组件实例text放入容器组件中,相关知识在后面讲到容器组件时会具体说明。运行上述代码,效果如图3.3所示。 从图3.3可以看出,页面顶部出现了一条黑色的文本信息,显示My name is Text,说明上述设置均已生效。 还可以使用Java代码修改或添加一些属性,例如,使用setTextColor()方法可以修改text的字体颜色,使用setFont()方法可以修改text的字体。在Text组件代码块中添加代码如下: //MainAbilitySlice.java中的Text组件 Text text = new Text(this); text.setWidth(DirectionalLayout.LayoutConfig.MATCH_CONTENT); text.setHeight(DirectionalLayout.LayoutConfig.MATCH_CONTENT); text.setText("My name is Text."); text.setTextSize(24, Text.TextSizeType.VP); text.setTextColor(Color.BLUE); text.setFont(Font.DEFAULT_BOLD); myLayout.addComponent(text); 上述代码将text的文本设置为蓝色,字体加粗。重新运行程序,运行结果如图3.4所示。 2. xml创建Text组件 在实际应用中,为了代码的简洁美观,通常会使用xml进行布局,这里学习通过xml进行UI布局,实现相同的效果。在界面左侧的Project项目栏选择entry→src→main→resources→base→layout,打开ability_main.xml,如图3.5所示。 图3.4Text组件的属性设置效果 图3.5xml布局目录结构 打开ability_main.xml文件,对布局和组件进行描述,代码如下: <!--ability_main.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_parent" ohos:orientation="vertical" ohos:padding="32"> <Text ohos:id="$+id:text" ohos:width="match_content" ohos:height="match_content" ohos:layout_alignment="horizontal_center" ohos:text="My name is Text." ohos:text_size="24vp"/> </DirectionalLayout> 其中,最外层的DirectionalLayout指页面的整体竖向布局。在Text中,使用ohos:id为当前控件定义一个唯一的标识id,布局中的组件通常都需要设置独立的id,以便在程序中查找该组件。若布局中有不同的组件设置了相同的id,则通过id查找会返回查找到的第一个组件,因此尽可能保证id的唯一性。使用ohos:width和ohos:height为控件指定了宽度和高度,其属性值match_parent表示组件大小将扩展为父容器允许的最大值,占据父组件方向上的剩余大小,即由父组件决定当前控件的大小。属性值match_content表示组件大小与它的内容占据的大小范围相适应,即由控件内容决定当前控件的大小。除此之外也可以通过具体的数值设置宽和高,例如24(以像素为单位)或24vp(以屏幕相对像素为单位)。使用ohos:layout_alignment指定文字的对齐方式,其中horizontal_center指水平方向居中。通过ohos:text设置Text的具体内容。 随后,在MainAbilitySlice.java中,注释掉通过Java创建Text组件的代码,并通过setUIContent()加载该xml布局。修改MainAbilitySlice.java中的代码如下: //MainAbilitySlice.java public class MainAbilitySlice extends AbilitySlice { @Override public void onStart(Intent intent) { super.onStart(intent); …//注释通过Java创建Text组件代码 super.setUIContent(ResourceTable.Layout_ability_main); //加载xml布局 } } 现在运行程序,效果如图3.6所示。 在xml中也可以对Text设置字体颜色及字重。在ability_main.xml中,增加属性代码如下: <!--ability_main.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_parent" ohos:orientation="vertical" ohos:padding="32"> <Text ohos:id="$+id:text" ohos:width="match_content" ohos:height="match_content" ohos:layout_alignment="horizontal_center" ohos:text="My name is Text." ohos:text_size="24vp" ohos:text_color="blue" ohos:text_weight="700"/> </DirectionalLayout> 通过ohos:text_color属性可以设置文本的颜色,通过ohos:text_weight属性可以设置文本的字重。重新运行后效果如图3.7所示,可见text文本变为蓝色,且字体加粗。 图3.6xml创建Text组件的运行效果 图3.7xml中设置Text颜色及字重运行效果 除此之外,Text还有很多其他的属性,在开发过程中根据需求查阅相关文档即可。 3.1.2Button组件 Button(按钮)是一种常见的与用户进行交互的组件,单击可以触发对应的操作,可以由图标和文本共同组成。 1. Java代码创建Button组件 同样地,还是先尝试使用Java代码来添加一个Button组件。打开MainAbilitySlice.java,在onStart()函数中添加如下代码: //MainAbilitySlice.java中添加Button组件 Button button = new Button(this); button.setWidth(DirectionalLayout.LayoutConfig.MATCH_PARENT); button.setHeight(DirectionalLayout.LayoutConfig.MATCH_CONTENT); button.setText("button"); button.setTextSize(28, Text.TextSizeType.VP); ShapeElement backgroundElement = new ShapeElement(); backgroundElement.setRgbColor(new RgbColor(0xDC,0xDC,0xDC)); button.setBackground(backgroundElement); myLayout.addComponent(button); 值得注意的是,此时如果直接运行程序会发现UI界面没有变化,这是因为在3.1.1节最后使用了setUIContent()加载了xml布局ability_main.xml,而修改的代码是创建的布局myLayout,这是两套互不干涉的布局,所以如果想看到刚刚在代码布局中添加的button,需要将setUIContent()函数中的变量改为myLayout,然后运行程序,这样就可以看到的text下方多出了button。Button组件也有丰富的自定义属性,大家同样可以根据自己的需求来查阅相关文档。 2. 使用xml创建Button组件 同样也可以使用xml来添加Button组件,在刚才编写的ability_main.xml文件中增加Button,代码如下: //ability_main.xml <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_parent" ohos:orientation="vertical" ohos:padding="32"> ... <Button ohos:id="$+id:button" ohos:width="match_parent" ohos:height="match_content" ohos:text="button" ohos:text_size="28vp" ohos:background_element="$graphic:buttonelement"/> </DirectionalLayout> Button可配置属性与Text差不多一致。其中,ohos:background_element为对Button引用背景色,属性值$graphic:buttonelement指为Button创建的背景色文件。在左侧Project窗口中,打开entry→src→main→resources→base→graphic文件夹,右击选择New→File,命名为buttonelement.xml,并在buttonelement.xml中定义Button的背景,代码如下: <!--buttonelement.xml--> <?xml version="1.0" encoding="utf-8"?> <shape xmlns:ohos ="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle"> <solid ohos:color="#DCDCDC"/> </shape> 图3.8xml中创建Button 组件运行效果 完成上述添加Button的操作后,在MainAbilitySlice.java中,注释通过Java添加布局的代码,并通过super.setUIContent(ResourceTable.Layout_ability_main)引用xml布局文件,运行后界面如图3.8所示。 Button组件的一个重要功能是当用户单击Button组件时,会执行相应的操作。这就需要让系统为Button组件设置一个单击事件监听器,监听器会在一个新的线程中不断地检测这个Button组件的单击事件。当用户单击按钮时,Button对象会收到一个单击事件,开发者可以自定义响应单击事件的方法。 例如,通过创建一个Component.ClickedListener对象,然后通过调用setClickedListener将其分配给Button对象,在onClick()方法中可以添加单击Button后的事件逻辑。通过上述方法实现对Button的事件监听,示例代码如下: //Button单击事件代码示例 public class MainAbilitySlice extends AbilitySlice { @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_main); //从定义的xml中获取Button对象 Button button = (Button) findComponentById(ResourceTable.Id_button); if(button != null) { //为按钮设置单击事件监听器 button.setClickedListener(new Component.ClickedListener() { public void onClick(Component v) { //此处添加单击按钮后的事件处理逻辑 } }); } } } 下面通过一个实例展示一下按钮的功能。打开MainAbilitySlice.java,在onStart()函数中添加如下代码: //MainAbilitySlice.java super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_main); Text text = (Text) findComponentById(ResourceTable.Id_text); Button button = (Button) findComponentById(ResourceTable.Id_button); if(button != null && text!= null){ button.setClickedListener(new Component.ClickedListener() { @Override public void onClick(Component component) { text.setTextColor(Color.RED); text.invalidate(); } }); } 在这段代码中首先从xml中取出了text和button两个实例,然后为button设置了单击事件监听器,并将修改text的文字颜色作为单击响应事件。再次运行程序后,单击button,会看到text中的文本变为了红色,运行效果如图3.9所示。 图3.9使用Button监听逻辑对Text颜色进行更改,左图为修改前,右图为修改后 3.1.3Image组件 Image是用于在屏幕上展示图像资源的组件,是软件开发中非常常用的UI组件。接下来讨论Image的具体使用方法。 Image可以通过setPixelMap(PixelMap)展示PixelMap类组件,或者通过setImageElemen(Element)展示Element组件,也可以通过setImageAndDecodeBounds(int)显示资源文件里的图片。 首先将图片资源picture.jpg放入entry→src→main→resources→base→media文件夹中,修改MainAbilitySlice.java文件,初始化一个DirectionalLayout来承载Image,代码如下: //MainAbilitySlice.java public class MainAbilitySlice extends AbilitySlice { private DirectionalLayout myLayout = new DirectionalLayout(this); private DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(ComponentContainer.LayoutConfig.MATCH_PARENT, ComponentContainer.LayoutConfig.MATCH_PARENT); @Override public void onStart(Intent intent) { super.onStart(intent); } } 随后在onStart()方法中,设置LayoutConfig的属性,并使用setImageAndDecodeBounds(int)方法加载resources资源文件夹中media文件中的图片资源,代码如下: //MainAbilitySlice.java中加载图片资源 @Override public void onStart(Intent intent) { super.onStart(intent); myLayout.setLayoutConfig(layoutConfig); layoutConfig.alignment= LayoutAlignment.HORIZONTAL_CENTER; layoutConfig.width=ComponentContainer.LayoutConfig.MATCH_CONTENT; layoutConfig.height = ComponentContainer.LayoutConfig.MATCH_CONTENT; Image image = new Image(this); image.setImageAndDecodeBounds(ResourceTable.Media_picture); image.setLayoutConfig(layoutConfig); myLayout.addComponent(image); super.setUIContent(myLayout); } 运行上述代码,展示效果如图3.10所示。 图3.10Image组件的效果展示 Image还可以通过setPixelMap(PixelMap)展示PixelMap类组件,修改onStart()中的代码如下: //MainAbilitySlice .java @Override public void onStart(Intent intent) { super.onStart(intent); myLayout.setLayoutConfig(layoutConfig); layoutConfig.alignment = LayoutAlignment.HORIZONTAL_CENTER; layoutConfig.width = ComponentContainer.LayoutConfig.MATCH_CONTENT; layoutConfig.height = ComponentContainer.LayoutConfig.MATCH_CONTENT; ResourceManager resourceManager = this.getResourceManager(); Resource resource = null; try { resource = resourceManager.getResource(ResourceTable.Media_picture); } catch (IOException e) { e.printStackTrace(); } catch (NotExistException e) { e.printStackTrace(); } ImageSource imageSource = ImageSource.create(resource,new ImageSource.SourceOptions()); PixelMap pixelMap = imageSource.createPixelmap(new ImageSource.DecodingOptions()); Image image = new Image(this); image.setPixelMap(pixelMap); image.setLayoutConfig(layoutConfig); myLayout.addComponent(image); super.setUIContent(myLayout); } 运行上述代码,可以得到和图3.10相同的运行效果。 还可以通过setImageElement(Element)令Image组件展示Element组件。构造一个蓝色矩形的element1,然后把这个element1传给Image组件并显示。修改onStart()中代码如下: //MainAbilitySlice.java @Override public void onStart(Intent intent) { super.onStart(intent); myLayout.setLayoutConfig(layoutConfig); layoutConfig.alignment = LayoutAlignment.HORIZONTAL_CENTER; layoutConfig.width = 1000; layoutConfig.height = 1000; ShapeElement element1 = new ShapeElement(); element1.setShape(ShapeElement.RECTANGLE); element1.setRgbColor(new RgbColor(0, 0, 255)); Image image = new Image(this); image.setImageElement(element1); image.setLayoutConfig(layoutConfig); myLayout.addComponent(image); super.setUIContent(myLayout); } 运行上述代码,展示效果如图3.11所示。 还可以根据需求,通过setBounds()方法设置element的位置和大小,如新增element2,设置一定的位置,将颜色设置为红色,并通过setBackground()方法将element1设置为背景,在onStart()方法中调整代码如下: ShapeElement element2 = new ShapeElement(); element2.setShape(ShapeElement.RECTANGLE); element2.setRgbColor(new RgbColor(255, 0, 0)); element2.setBounds(300,100,800,1000); Image image = new Image(this); image.setBackground(element1); image.setImageElement(element2); 运行上述代码,效果如图3.12所示。 图3.11设置element效果图 图3.12调整element大小 image和其中放的图片内容都是有大小的,如果image的大小和内部图片的大小不一致,则需要使用setscalemode()方式来调整图片大小。接下来,详细介绍一下scalemode的使用方法。 scalemode有6种参数,分别演示一下效果。修改onStart()方法中Image部分代码如下: Image image1 = new Image(this); image1.setImageAndDecodeBounds(ResourceTable.Media_picture); Image image2 =new Image(this); image2.setImageAndDecodeBounds(ResourceTable.Media_picture); image2.setHeight(300); image2.setWidth(300); image2.setBackground(element1); myLayout.addComponent(image1); myLayout.addComponent(image2); 运行上述代码,会得到一张显示效果不全的图片,运行效果如图3.13所示。其中,image1是原图,组件大小和图片大小一致。image2把图片组件缩小到300×300,这样就导致图片显示不全。 创建Image实例image3,同样将大小设置为300×300,并将image3的scalemode改变为INSIDE,代码如下: Image image3 = new Image(this); image3.setImageAndDecodeBounds(ResourceTable.Media_picture); image3.setBackground(element1); image3.setHeight(300); image3.setWidth(300); image3.setScaleMode(Image.ScaleMode.INSIDE); myLayout.addComponent(image3); 运行代码效果如图3.14所示,其中第3张是INSIDE的效果,为了直观地看到运行效果,可以将组件的背景用蓝色充满,可以看出image3的scalemode设置为INSIDE后,会根据比例对图像进行缩小,使得图像与组件大小相同或更小,并在中心显示图像内容。image3大小为300×300,但图片被按比例缩小在了element1中。 图3.13Image显示不全效果图 图3.14image3按比例缩放效果图 若将image3的scalemode改变为STRETCH,STRETCH不使用任何比例对图像进行缩放。修改代码如下: image3.setScaleMode(Image.ScaleMode.STRETCH); 运行后效果如图3.15所示,可以清楚地看到,图片原本的比例被破坏,被拉长后充满了300×300的组件。 图3.15image3设置为STRETCH效果图 ZOOM_CENTER可以实现放大效果,将Image组件放大到900×900,使得组件大于图片大小,原图片与组件image2间的空白位置用蓝色填充,scalemode设置为ZOOM_CENTER后的图片与组件image3间的空白位置用青色填充,代码如下: ShapeElement element1 = new ShapeElement(); element1.setShape(ShapeElement.RECTANGLE); element1.setRgbColor(new RgbColor(0, 0, 255)); ShapeElement element2 = new ShapeElement(); element2.setShape(ShapeElement.RECTANGLE); element2.setRgbColor(new RgbColor(0, 255, 255)); Image image1 =new Image(this); image1.setImageAndDecodeBounds(ResourceTable.Media_picture); image1.setLayoutConfig(layoutConfig); image1.setHeight(900); image1.setWidth(900); image1.setBackground(element1); Image image2 = new Image(this); image2.setImageAndDecodeBounds(ResourceTable.Media_picture); image2.setLayoutConfig(layoutConfig); image2.setHeight(900); image2.setWidth(900); image2.setBackground(element2); image2.setScaleMode(Image.ScaleMode.ZOOM_CENTER); 运行效果如图3.16所示。可以清楚地看到第一张为原图片,第二张图片根据比例将图片进行了放大,使得图片宽度与组件宽度相同,并在中心显示图片内容。 随后分别设置image2的scalemode为ZOOM_START和ZOOM_END,可以看到效果如图3.17所示。ZOOM_START可使 图片放大并在左上角显示图片,ZOOM_END可使图片放大并在右下角显示图片。 图3.16图片中心放大效果 图3.17Image放大并分别显示在左上角、右下角 3.2Java UI容器组件 13min Java UI框架提供了一些具有标准布局功能的容器,均继承自ComponentContainer类,一般以Layout结尾,如DirectionalLayout、DependentLayout等(也有例外,例如ListContainer类也是一个布局类)。 由图3.1视图树可知,完整的用户界面是一个容器组件,用户界面中的一部分也可以是一个容器组件。容器组件中可以容纳单个组件与其他容器组件。 HarmonyOS为开发者分别提供了在Java代码和xml格式的文件中声明布局的方法。下面将分别使用代码和XML文件来开发HarmonyOS的常用布局。 3.2.1线性布局DirectionalLayout 图3.18DirectionalLayout 竖直排列示意图 DirectionalLayout是Java UI中的一种重要组件布局,用于将一组组件(Component或ComponentContainer)按照水平或者垂直方向排布,能够方便地对齐布局内的组件。3.1.1和3.1.2节就是将Text和Button等组件放在了DirectionalLayout中,实现了Text与Button自上而下有序排列的效果。 DirectionalLayout的排列方向(orientation)分为水平(horizontal)和竖直(vertical)方向。使用orientation设置布局内组件的排列方式,默认为垂直排列。示意图如图3.18所示。 1. 代码创建DirectionalLayout组件 这里设置了3个单体组件Button和一个线性容器组件,并实现Button组件在线性容器内自上而下竖直排列。在MainAbilitySlice.java中修改onStart()函数,代码如下: //MainAbilitySlice.java public void onStart(Intent intent) { super.onStart(intent); this.setDisplayOrientation(AbilityInfo.DisplayOrientation.PORTRAIT); //创建容器组件DirectionalLayout实例 DirectionalLayout myLayout = new DirectionalLayout(this); //设置DirectionalLayout对内部组件的排列方式并设置为竖直排列 myLayout.setOrientation(Component.VERTICAL); //设置DirectionalLayout内部的组件都排列在屏幕水平的中央位置 myLayout.setAlignment(LayoutAlignment.HORIZONTAL_CENTER); //容器组件DirectionalLayout的LayoutConfig DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig (DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT); myLayout.setLayoutConfig(layoutConfig); //创建3个Button实例 Button button1 = new Button(this); Button button2 = new Button(this); Button button3 = new Button(this); //单体组件Button的LayoutConfig LayoutConfig buttonConfig = new LayoutConfig( DirectionalLayout.LayoutConfig.MATCH_CONTENT, DirectionalLayout.LayoutConfig.MATCH_CONTENT); buttonConfig.setMargins(0,100,0,100); //将LayoutConfig应用到3个Button上 button1.setLayoutConfig(buttonConfig); button2.setLayoutConfig(buttonConfig); button3.setLayoutConfig(buttonConfig); //创建Button的背景 ShapeElement bottomElement = new ShapeElement(); bottomElement.setRgbColor(new RgbColor(200,200,200)); //将背景应用到3个Button上 button1.setBackground(bottomElement); button2.setBackground(bottomElement); button3.setBackground(bottomElement); //设定3个Button内显示的文本 button1.setText("Button 1"); button2.setText("Button 2"); button3.setText("Button 3"); //设定3个Button内显示的文字大小 button1.setTextSize(80, Text.TextSizeType.VP); button2.setTextSize(80, Text.TextSizeType.VP); button3.setTextSize(80, Text.TextSizeType.VP); //将3个Button加入DirectionalLayout中 myLayout.addComponent(button1); myLayout.addComponent(button2); myLayout.addComponent(button3); super.setUIContent(myLayout); } 在这段代码中首先创建了一个DirectionalLayout容器组件实例,然后对它的属性进行了设置,其中比较重要的属性有如下3种。 图3.19代码创建的Directi onalLayout效果展示 Orientation: 代表了DirectionalLayout对其内部组件(子组件)的排列规则。VERTICAL代表从上到下竖直排列,HORIZONTAL则代表从左至右水平排列。 Alignment: 代表了DirectionalLayout内部组件(子组件)的排列位置。HORIZONTAL_CENTER代表水平位置的正中,常用的还有左对齐LEFT、右对齐RIGHT和竖直位置的正中VERTICAL_CENTER。 LayoutConfig: 代表了对DirectionalLayout本身的一些设置,不过在这里只设置了宽和高为MATCH_PARENT,这意味着这个DirectionalLayout的大小将充满它的上一层容器(父容器组件),在这里这个DirectionalLayout没有父容器组件,所以将充满整个屏幕。 尝试运行程序,可以看到3个Button组件在屏幕水平方向的正中央自上而下竖直排列,如图3.19所示。 2. xml创建DirectionalLayout 现在尝试使用xml来创建DirectionalLayout,以便了解更多功能。这里设置3个Button,并将它们设置为垂直排列。首先在layout文件夹中新建布局文件directional_layout.xml,在其中加入3个Button,并将其最外层标签设置为线性布局DirectionalLayout,代码如下: <!--directional_layout.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_content" ohos:orientation="vertical"> <Button ohos:width="100vp" ohos:height="50vp" ohos:bottom_margin="13vp" ohos:left_margin="13vp" ohos:background_element="$graphic:buttonelement" ohos:text="Button 1" ohos:text_size="24vp"/> <Button ohos:width="100vp" ohos:height="50vp" ohos:bottom_margin="13vp" ohos:left_margin="13vp" ohos:background_element="$graphic:buttonelement" ohos:text="Button 2" ohos:text_size="24vp"/> <Button ohos:width="100vp" ohos:height="50vp" ohos:bottom_margin="13vp" ohos:left_margin="13vp" ohos:background_element="$graphic:buttonelement" ohos:text="Button 3" ohos:text_size="24vp"/> </DirectionalLayout> 其中,ohos:orientation的属性值设置为vertical,即垂直排列。在MainAbilitySlice中修改代码,引入directional_layout.xml布局文件,代码如下: //MainAbilitySlice.java public class MainAbility extends Ability { @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_directional_layout); } } 运行效果如图3.20所示,可见3个Button为垂直排列。 图3.203个Button垂直排列 垂直排列默认为左对齐,组件可以通过ohos:layout_alignment控制自身在布局中的对齐方式。在垂直排列中,layout_alignment的属性值left表示左对齐,right表示右对齐,horizontal_center表示水平方向居中,center表示水平方向和垂直方向均居中。当属性值的对齐方式与排列方式方向一致时,对齐方式不会生效,如设置了水平方向的排列方式,则左对齐、右对齐将不会生效。如修改directional_layout.xml布局文件,代码如下: <!--directional_layout.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_content" ohos:orientation="vertical"> <Button ohos:width="100vp" ohos:height="50vp" ohos:layout_alignment="left" …/> <Button ohos:width="100vp" ohos:height="50vp" ohos:layout_alignment="horizontal_center" …/> <Button ohos:width="100vp" ohos:height="50vp" ohos:layout_alignment="right" …/> </DirectionalLayout> 运行后效果如图3.21所示。可见,3个Button分别实现了左对齐、水平居中和右对齐。 在DirectionalLayout布局中,可以将ohos:orientation属性值改为horizontal,这样Button则变为水平排列,修改代码如下: <!--directional_layout.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_content" ohos:orientation="horizontal"> ... </DirectionalLayout> 运行后效果如图3.22所示,可以看到3个Button变为水平排列。 图3.21对齐方式效果示例 图3.223个Button水平排列 注意,DirectionalLayout布局不能自动换行,子视图会按照设定的方向依次排列,若超过布局本身的大小,则超出布局大小的部分将不会被显示。修改3个button的宽度,代码如下: <!--directional_layout.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos=http://schemas.huawei.com/res/ohos ohos:width="match_parent" ohos:height="match_content" ohos:orientation="horizontal"> <Button ohos:width="150vp" ohos:height="50vp" … /> <Button ohos:width="150vp" ohos:height="50vp" …/> <Button ohos:width="150vp" ohos:height="50vp" …/> </DirectionalLayout> 图3.23DirectionalLayout 不可自动换行示例 运行后界面效果如图3.23所示。 此布局包含了3个Button,但是因为宽度超出了布局范围,所以界面上只完整显示了两个Button,超出布局大小的视图部分无法正常显示,因此在开发布局中要注意避免这类问题的出现。 在HarmonyOS的UI组件中,布局之间也可以按层级关系互相组合,DirectionalLayout布局和其他布局的组合,可以实现更加丰富的布局方式。具体逻辑可以参照后面章节的视图树。 3.2.2相对布局DependentLayout DependentLayout是Java UI里另一种常见布局,每个组件可以指定相对于其他同级元素的位置,或者指定相对于父组件的位置进行布局。与DirectionalLayout相比,它拥有更多的排布方式。布局的示意图如图3.24所示。 图3.24DependentLayout布局示意图 DependentLayout的布局方式,有相对同级其他组件和相对父组件两种布局方式。 1. 代码创建DependentLayout 和DirectionalLayout举的例子类似,在这里设置3个单体组件Button和一个DependentLayout容器组件,并实现Button组件在DependentLayout容器内自上而下竖直排列。为了方便展示,还是在MainAbilitySlice.java中修改onStart()函数,代码如下: //MainAbilitySlice.java public void onStart(Intent intent) { super.onStart(intent); this.setDisplayOrientation(AbilityInfo.DisplayOrientation.PORTRAIT); //创建容器组件DependentLayout实例,并为之设置Id DependentLayout myLayout = new DependentLayout(this); int L1 = 0; myLayout.setId(L1); //容器组件DependentLayout的LayoutConfig DependentLayout.LayoutConfig layoutConfig = new DependentLayout.LayoutConfig( DependentLayout.LayoutConfig.MATCH_PARENT, DependentLayout.LayoutConfig.MATCH_PARENT); myLayout.setLayoutConfig(layoutConfig); //创建3个Button实例,并为之设置Id Button button1 = new Button(this); int B1 = 1; button1.setId(B1); Button button2 = new Button(this); int B2 = 2; button2.setId(B2); Button button3 = new Button(this); int B3 = 3; button3.setId(B3); //单体组件Button的LayoutConfig DependentLayout.LayoutConfig buttonConfig1 = new DependentLayout.LayoutConfig( DependentLayout.LayoutConfig.MATCH_CONTENT, DependentLayout.LayoutConfig.MATCH_CONTENT); buttonConfig1.addRule(DependentLayout.LayoutConfig.CENTER_IN_PARENT); buttonConfig1.setMargins(0,100,0,100); DependentLayout.LayoutConfig buttonConfig2 = new DependentLayout.LayoutConfig( DependentLayout.LayoutConfig.MATCH_CONTENT, DependentLayout.LayoutConfig.MATCH_CONTENT); buttonConfig2.addRule(DependentLayout.LayoutConfig.BELOW, B1); buttonConfig2.setMargins(0,100,0,100); DependentLayout.LayoutConfig buttonConfig3 = new DependentLayout.LayoutConfig( DependentLayout.LayoutConfig.MATCH_CONTENT, DependentLayout.LayoutConfig.MATCH_CONTENT); buttonConfig3.addRule(DependentLayout.LayoutConfig.ALIGN_PARENT_BOTTOM); buttonConfig3.addRule(DependentLayout.LayoutConfig.ALIGN_PARENT_RIGHT); buttonConfig3.setMargins(0,100,0,100); //将LayoutConfig应用到3个Button上 button1.setLayoutConfig(buttonConfig1); button2.setLayoutConfig(buttonConfig2); button3.setLayoutConfig(buttonConfig3); //创建Button的背景 ShapeElement bottomElement = new ShapeElement(); bottomElement.setRgbColor(new RgbColor(200,200,200)); //将背景应用到3个Button上 button1.setBackground(bottomElement); button2.setBackground(bottomElement); button3.setBackground(bottomElement); //设定3个Button内显示的文本 button1.setText("Button 1"); button2.setText("Button 2"); button3.setText("Button 3"); //设定3个Button内显示的文字大小 button1.setTextSize(50, Text.TextSizeType.VP); button2.setTextSize(50, Text.TextSizeType.VP); button3.setTextSize(50, Text.TextSizeType.VP); //将3个Button加入DependentLayout中 myLayout.addComponent(button1); myLayout.addComponent(button2); myLayout.addComponent(button3); super.setUIContent(myLayout); } 运行效果如图3.25所示。 图3.25代码创建的Depende ntLayout效果展示 可以发现,与之前DirectionalLayout例子不同的是为每个组件(包括容器组件和单体组件)都设置了一个Id,这是因为在DependentLayout中命令需要利用Id来识别组件对象,进而实现组件之间的相对布局。 为了实现相对布局,此处为每个子组件(范例中的Button实例)添加了一个DependentLayout.LayoutConfig类实例,在对应的DependentLayout.LayoutConfig实例中,使用addRule()函数为这些组件添加想要的排列规则,具体的排列规则如下: 对于button1,为其添加的规则为LayoutConfig.CENTER_IN_PARENT,这个规则规定了button1将位于其父组件(范例中的DependentLayout实例)的正中间。 对于button2,为其添加的规则为LayoutConfig.BELOW和B1,这个规则规定了button2将位于Id为B1(范例中的button1实例)的下方。值得注意的是,在效果图中可以看出button2并没有位于button1的正下方,而是下方的左侧。这是因为这个规则只规定了button2竖直方向的位置,而并没有规定水平方向的位置,所以水平方向的位置仍为系统的默认值,即左侧排列。这一类问题在使用时需要注意。 对于button3,为其添加的规则为LayoutConfig.ALIGN_PARENT_BOTTOM和LayoutConfig.ALIGN_PARENT_RIGHT,这两个规则分别规定了button3要位于其父组件的下方和右侧,所以从效果图中可以看出button3位于界面的右下方。这说明一个DependentLayout.LayoutConfig实例可以添加多个规则。如果规则之间相互“冲突”,可以尝试将button3添加的2个规则修改为LayoutConfig.ALIGN_PARENT_RIGHT和LayoutConfig.ALIGN_PARENT_LEFT,运行后观察效果如何。 2. xml创建DependentLayout 相对于同级其他组件的位置,每个组件有above(位于同级组件的上侧)、below(位于同级组件的下侧)、start_of(位于同级组件的起始侧)、end_of(位于同级组件的结束侧)、left_of(位于同级组件的左侧)、right_of(位于同级组件的右侧)6种相对位置。这里通过实例直观体会一下。首先在layout文件夹中新建布局文件dependent_layout.xml,代码如下: <!--dependent_layout.xml--> <?xml version="1.0" encoding="utf-8"?> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_content"> <Text ohos:id="$+id:text1" ohos:width="match_content" ohos:height="match_content" ohos:left_margin="15vp" ohos:top_margin="15vp" ohos:bottom_margin="15vp" ohos:text="text1" ohos:text_size="20fp" ohos:background_element="$graphic:text_element"/> <Text ohos:id="$+id:text2" ohos:width="match_content" ohos:height="match_content" ohos:left_margin="15vp" ohos:top_margin="15vp" ohos:bottom_margin="15vp" ohos:text="end_of text1" ohos:text_size="20fp" ohos:background_element="$graphic:text_element" ohos:end_of="$id:text1"/> <Text ohos:id="$+id:text3" ohos:width="match_content" ohos:height="match_content" ohos:left_margin="15vp" ohos:text="below text1" ohos:text_size="20fp" ohos:background_element="$graphic:text_element" ohos:below="$id:text1"/> </DependentLayout> 其中,ohos:end_of属性可以让一个组件位于另一个组件的结束侧,属性值中需要指定相对控件的id引用,如text2中设置 ohos:end_of="$id:text1",表示让该组件位于text1的结束侧。text3中通过设置ohos:below="$id:text1",让text3位于text1的下方。其他相对位置同理。 在graphic文件夹中新建text_element.xml文件,代码如下: <!--text_element.xml--> <?xml version="1.0" encoding="utf-8"?> <shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle"> <solid ohos:color="#D5CF72"/> </shape> 在MainAbility中更改布局文件引用,代码如下: super.setUIContent(ResourceTable.Layout_dependent_layout); 图3.26相对同级组件位置布局 运行代码,效果如图3.26所示。 除了可以相对于同级其他组件位置进行定位,也可以相对父组件进行布局。相对于父组件的位置,每个组件有align_parent_left(位于父组件的左侧)、align_parent_right(位于父组件的右侧)、align_parent_start(位于父组件的起始侧)、align_parent_end(位于父组件的结束侧)、align_parent_top(位于父组件的上侧)、align_parent_bottom(位于父组件的下侧)、center_in_parent(位于父组件的中间)7种相对位置。基于以上布局,也可以形成左上角、左下角、右上角和右下角的布局。修改dependent_layout.xml中的代码如下: <!--dependent_layout.xml--> <?xml version="1.0" encoding="utf-8"?> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_parent"> <Text ohos:id="$+id:text1" ohos:width="match_content" ohos:height="match_content" ohos:text="left_top" ohos:text_size="20fp" ohos:background_element="$graphic:text_element" ohos:align_parent_left="true" ohos:align_parent_top="true"/> <Text ohos:id="$+id:text2" ohos:width="match_content" ohos:height="match_content" ohos:text="right_top" ohos:text_size="20fp" ohos:background_element="$graphic:text_element" ohos:align_parent_right="true" ohos:align_parent_top="true"/> <Text ohos:id="$+id:text3" ohos:width="match_content" ohos:height="match_content" ohos:text="center" ohos:text_size="20fp" ohos:background_element="$graphic:text_element" ohos:center_in_parent="true"/> <Text ohos:id="$+id:text4" ohos:width="match_content" ohos:height="match_content" ohos:text="bottom_center" ohos:text_size="20fp" ohos:background_element="$graphic:text_element" ohos:align_parent_bottom="true" ohos:center_in_parent="true"/> </DependentLayout> 上述代码中,通过ohos:align_parent_left和ohos:align_parent_top进行属性设置,使text1位于父组件的左上角,同理令text2位于父组件的右上角,text3位于父组件居中,text4位于父容器底部居中。运行后结果如图3.27所示。 图3.27相对父组件位置布局 图3.28绝对坐标位置布局 3.2.3绝对坐标布局PositionLayout 绝对坐标布局容器PositionLayout也是比较常用的基础布局组件之一,它可以依据绝对坐标对其内部的组件进行布局,示意图如图3.28所示。具体来讲,当PositionLayout作为父组件时,其在屏幕上所占有的空间可视为一个二维直角坐标系,其子组件可以依据坐标值进行布局。 作为最直观的容器组件,PositionLayout的使用方法也与前两种Layout不同,它不需要使用LayoutConfig实现组件之间的布局,而仅需要为每个子组件直接设置坐标即可,实现PositionLayout的代码如下: //MainAbilitySlice.java public void onStart(Intent intent) { super.onStart(intent); this.setDisplayOrientation(AbilityInfo.DisplayOrientation.PORTRAIT); //创建容器组件PositionLayout实例,并为之设置Id PositionLayout myLayout = new PositionLayout(this); //容器组件PositionLayout的LayoutConfig PositionLayout.LayoutConfig layoutConfig = new PositionLayout.LayoutConfig( PositionLayout.LayoutConfig.MATCH_PARENT, PositionLayout.LayoutConfig.MATCH_PARENT); myLayout.setLayoutConfig(layoutConfig); //创建3个Button实例,并为之设置Id Button button1 = new Button(this); Button button2 = new Button(this); Button button3 = new Button(this); //将LayoutConfig应用到3个Button上 button1.setTop(100); button1.setLeft(100); button2.setTop(1000); button2.setLeft(500); button3.setTop(1500); button3.setRight(500); //创建Button的背景 ShapeElement bottomElement = new ShapeElement(); bottomElement.setRgbColor(new RgbColor(200,200,200)); //将背景应用到3个Button上 button1.setBackground(bottomElement); button2.setBackground(bottomElement); button3.setBackground(bottomElement); //设定3个Button内显示的文本 button1.setText("Button 1"); button2.setText("Button 2"); button3.setText("Button 3"); //设定3个Button内显示的文字大小 button1.setTextSize(50, Text.TextSizeType.VP); button2.setTextSize(50, Text.TextSizeType.VP); button3.setTextSize(50, Text.TextSizeType.VP); //将3个Button加入PositionLayout中 myLayout.addComponent(button1); myLayout.addComponent(button2); myLayout.addComponent(button3); super.setUIContent(myLayout); } 图3.29代码创建的Positi onLayout效果展示 在这里我们为button1和button2设置了Left和Top坐标,为button3设置了Top和Right坐标。运行的效果如图3.29所示。 可以看出,button1和button2的左上角分别位于其父布局(范例中的PositionLayout实例)的(100,100)和(100,500)位置上,这是符合预期的,而button3却位于(0,1500)位置上,说明其Right坐标的设置是不能够改变组件位置的。经过一些测试发现对于Bottom坐标的设置也是不能够改变组件位置的。 由于PositionLayout是依据绝对坐标进行的布局,所以代码的灵活性和扩展性都相对较差。例如,PositionLayout在适配各种分辨率的屏幕上是比较困难的,需要对每个组件的坐标值进行修改。在切换横屏竖屏时,若要保持布局的整齐性,每个坐标值都需要重新计算,这是比较麻烦的。 3.2.4滚动菜单ListContainer 手机屏幕空间有限,能显示的内容不多。可以借助ListContainer来显示更多的内容。ListContainer允许用户通过上下滑动来将屏幕外的数据滚动到屏幕内,同时屏幕内原有的数据滚动出屏幕,从而显示更多的数据内容。 但是ListContainer的使用和一般的组件使用不同,ListContainer一定要和Provider适配器搭配使用,HarmonyOS里Provider有BaseItemProvider和它的子类RecycleItemProvider。 在前面的章节中都是分别使用Java代码或者xml来创建布局容器(或单体组件),事实上还可以采用两者结合的手段进行创建,在这里将采用xml来创建ListContainer,然后使用Java代码为其添加各种属性。首先在resource里定义一个带有ListContainer的布局,代码如下: <!--listcontainer.xml--> <?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:width="match_parent" ohos:height="match_parent" ohos:orientation="vertical"> <ListContainer ohos:id="$+id:list" ohos:width = "match_content" ohos:height="match_content" ohos:margin="50px" /> </DirectionalLayout> 然后需要设置一个适配器provider来为ListContainer提供内容,代码如下: //MainAbititySlice.java public class SamplePagerAdapter extends BaseItemProvider { private List mList; public SamplePagerAdapter(List list) { mList = list; } @Override public int getCount() { return mList.size(); } @Override public Object getItem(int i) { return mList.get(i); } @Override public long getItemId(int i) { ret rn i; } @Override public Component getComponent(int i, Component component, ComponentContainer componentContainer) { Text title = new Text(getContext()); title.setText((String)mList.get(i)); title.setTextSize(200); title.setTextAlignment(TextAlignment.CENTER); title.setLayoutConfig(new StackLayout.LayoutConfig( StackLayout.LayoutConfig.MATCH_CONTENT, StackLayout.LayoutConfig.MATCH_CONTENT)); title.setTextColor(Color.RED); ShapeElement shapeElement = new ShapeElement(); shapeElement.setShape(ShapeElement.RECTANGLE); shapeElement.setRgbColor(new RgbColor(0,0,255)); title.setBackground(shapeElement); return title; } } Provider必须继承自BaseItemProvider和它的子类RecycleItemProvider,然后需要覆写getCount()、getItem()、getItemId()和getComponent()这4种方法。 在ListContainer绘制之前,会首先调用public int getCount()方法,这种方法的返回值代表listContaimer的长度,然后根据这个值来确定getComponent()的执行次数。 在绘制每一行之前都会执行一次getComponent(),用于获取当前行需要显示的内容,其中,第一个回调参数i代表当前的行数,第二个回调参数component代表上一次绘制时本行显示的内容,所以易知首次绘制时此参数为空(因为还没有旧的component)。 另外两个函数public Object getItem(int i)的作用是返回一个子Component,即ListContainer中的一个子条目。public long getItemId(int i)的作用是返回一个item的id,由参数i决定是哪个id。 覆写完上述4个函数后,就完成了Provider的简易构建。上述Provider的业务逻辑是使getComponent()每次运行时得到列表mList中的一个字符串,然后实例化一个Text组件来展示这个字符串,所以还需要定义一个名为mList的ArrayList用于承载这些字符串,代码如下: //MainAbility.java DirectionalLayout myLayout = new DirectionalLayout(this); DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_PARENT); layoutConfig.alignment= LayoutAlignment.HORIZONTAL_CENTER; myLayout.setLayoutConfig(layoutConfig); layoutConfig.width=ComponentContainer.LayoutConfig.MATCH_CONTENT; layoutConfig.height = ComponentContainer.LayoutConfig.MATCH_CONTENT; List aList=new ArrayList<>(); ListContainer listContainer = new ListContainer(this); listContainer.setLayoutConfig(layoutConfig); aList.add("测试1"); aList.add("测试2"); aList.add("测试3"); aList.add("测试4"); aList.add("测试5"); aList.add("测试6"); aList.add("测试7"); listContainer.setItemProvider(new SamplePagerAdapter(aList)); myLayout.addComponent(listContainer); 上述代码创建了一个list,用于承载7个字符串,把这个list传递给Provider,最后对xml中的布局和ListContainer进行加载,将创建的Provider和布局中的ListContainer进行绑定,这样就能展示整个ListContainer了,展示效果如图3.30所示。 图3.30ListContainer效果展示 3.2.5滑动布局管理器 PageSlider PageSlider和ListContainer类似,是一种容器布局,需要PageSliderProvider作为Provider才能显示内容。PageSlider可以允许用户左右或者上下滑动来翻页,其中每页都可以添加其他Component进行显示。 与上一节的ListContainer类似,首先需要定义一个Provider,代码如下: //MainAbilitySlice.java private class PagerAdapter extends PageSliderProvider { private List list; public PagerAdapter(List<Image> list) { this.list = list; } @Override public int getCount() { return list.size(); } @Override public Object createPageInContainer( ComponentContainer componentContainer, int i) { componentContainer.addComponent((Component)list.get(i)); return list.get(i); } @Override public void destroyPageFromContainer( ComponentContainer componentContainer, int i, Object o) { componentContainer.removeComponent((Component)list.get(i)); } @Override public boolean isPageMatchToObject(Component component, Object o) { return component==o; } } 可以看到,所有的PageProvider都需要继承自PageSliderProvider类,并且需要覆写 isPageMatchToObject(Componentcomponent,Object o)、destroyPageFromContainer(ComponentContainer componentContainer,int i,Object o)、createPageInContainer(ComponentContainer componentContainer,int i)、getCount()这4个函数。 其中,getCount返回要滑动的Component的个数,createPageInContainer是从当前componentContainer的指定位置i中添加Component,然后返回这个Component。 destroyPageFromContainer是把component从componentContainer的当前位置i销毁。 在示例中首先构筑布局,然后初始化一个用来存储Component的列表,代码如下: //MainAbilitySlice.java DirectionalLayout myLayout = new DirectionalLayout(this); DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(ComponentContainer.LayoutConfig.MATCH_PARENT, ComponentContainer.LayoutConfig.MATCH_PARENT); private List<Image> list = new ArrayList<>(); 然后给LayoutConfig设置一些参数,这样可以保证Component居中展示,代码如下: layoutConfig.alignment= LayoutAlignment.HORIZONTAL_CENTER; myLayout.setLayoutConfig(layoutConfig); layoutConfig.width=ComponentContainer.LayoutConfig.MATCH_CONTENT; layoutConfig.height = ComponentContainer.LayoutConfig.MATCH_CONTENT; 接下来新建一个PageSlider,代码如下: PageSlider pageSlider = new PageSlider(this); 其中,Provider中放入了包含4个Image的list,代码如下: //MainAbilitySlice.java Image image = new Image(this); image.setImageAndDecodeBounds(ResourceTable.Media_picture); Image image1 = new Image(this); image1.setImageAndDecodeBounds(ResourceTable.Media_pic); image1.setWidth(1125); image1.setHeight(760); image1.setScaleMode(Image.ScaleMode.ZOOM_CENTER); Image image2 = new Image(this); image2.setImageAndDecodeBounds(ResourceTable.Media_pic2); image2.setScaleMode(Image.ScaleMode.ZOOM_CENTER); image2.setWidth(1125); image2.setHeight(760); Image image3 = new Image(this); image3.setImageAndDecodeBounds(ResourceTable.Media_pic3); image3.setScaleMode(Image.ScaleMode.ZOOM_CENTER); image3.setWidth(1125); image3.setHeight(760); list.add(image); list.add(image1); list.add(image2); list.add(image3); 然后可以把PageSlider和Provider关联起来,代码如下: pageSlider.setProvider(new PagerAdapter(list)); myLayout.addComponent(pageSlider); 这样就可以看到翻页效果,运行上述代码,效果如图3.31所示,分别为第一张图片到最后一张图片的切换过程。 完成PageSlider后还可以初始化一个PageSliderIndicator,然后把Indicator和PageSlider关联起来,代码如下: PageSliderIndicator pageSliderIndicator = new PageSliderIndicator(this); pageSliderIndicator.setViewPager(pageSlider); pageSliderIndicator.setLayoutConfig(layoutConfig); myLayout.addComponent(pageSliderIndicator); 这样就完成了指示器的构造,运行代码可以看到翻页效果,如图3.32所示。 可以看到灰色的长条表示当前所在的页面,Indicator明确地指示了PageSlider的页面。 3.2.6其他布局容器 除以上布局容器之外,HarmonyOS还提供了多种布局容器,这里只对它们做一个简单的介绍,具体的实现可以自行尝试。 图3.31PageSlide翻页展示效果 图3.32指示器效果运行效果图 窗口布局容器StackLayout提供一个窗口,提供一个框架布局,其中的元素可以重叠。StackLayout用于在屏幕上保留一个区域来显示视图中的元素。通常,框架布局中只应该放置一个子组件。如果存在多个子组件,则显示最新添加的组件,之前添加的组件会被遮盖掉。 桌面布局容器TableLayout是一种像Windows桌面一样的排列布局容器。该布局容器用于在带有表的组件中安排组件。TableLayout提供了对齐和安排组件的接口,以在带有表的组件中显示组件。可以配置排列方式、行数和列数及组件的位置。 还有自适应框容器AdaptiveBoxLayout。自适应框将自动分为具有相同宽度和可能不同高度的框的行和列。框的宽度取决于布局宽度和每行中框的数量,这由它的LayoutConfig指定。新行仅在上一行填充后才开始。每个框都包含一个子组件。每个框的高度取决于其包含的子组件的高度。每行的高度由该行中的最高框确定。自适应框布局容器的宽度只能设置为MATCH_PARENT或固定值,但是开发者可以为容器中的组件自由设置长度、宽度和对齐方式。 3.3Java UI动画 3.3.1动画类介绍 动画是组件的基础特性之一,精心设计的动画使 UI 变化更直观,有助于改进应用程序的外观并改善用户体验。HarmonyOS在Java UI框架下提供了Animator类对各种组件添加动画效果,它的子类有数值动画(AnimatorValue)和属性动画(AnimatorProperty) ,还提供了将多个动画同时操作的动画集合(AnimatorGroup),开发者可以自由组合这些动画元素,从而构建丰富多样的动画效果。Animator作为动画的基类,提供了与动画的启动、停止、暂停和恢复相关的API,同时还支持为动画设定持续时间、启动延迟、重复次数和指定的曲线类型。AnimatorValue提供随时间变化的变动数值,开发者可以自定义动画的样式,代入这个数值即可实现动画效果。AnimatorProperty则是对AnimatorValue的自动化封装,原生组件提供了缩放、平移、旋转等动画效果。AnimatorGroup则可以通过创建一个动画组,实现多个组建的序列或并行播放动画。 3.3.2数值动画AnimatorValue 前文说到AnimatorValue提供随时间变化的数值,这个数值是从0到1变化的浮点数,本身与Component对象或种类无关。由于AnimatorValue是Animator的子类,所以开发者可以在AnimatorValue中调用Animator中API并自定义数值从0到1的变化过程,例如更长的动画持续时间意味着数值的变化会更慢、不同的变化曲线则可以让数值实现各种各样的非匀速变化、设定重复次数可以让数值从0到1变化循环数次…… 开发者可以利用数值随时间变化的特性,实现动画效果。例如通过值的变化改变控件的属性,从而实现控件的运动。 在本示例中主要演示一个Text组件的动画,并通过Button启动动画播放。先定义布局设置LayoutConfig和用于设定背景颜色的ShapeElement等参数,然后定义一个Button用于启动动画,代码如下: //动画效果布局定义 layoutConfig.alignment= LayoutAlignment.CENTER; layoutConfig.setMargins(100,600,100,100); myLayout.setLayoutConfig(layoutConfig); layoutConfig.width=ComponentContainer.LayoutConfig.MATCH_CONTENT; layoutConfig.height = ComponentContainer.LayoutConfig.MATCH_CONTENT; ShapeElement shapeElement = new ShapeElement(); shapeElement.setShape(ShapeElement.RECTANGLE); shapeElement.setCornerRadius(80); shapeElement.setRgbColor(new RgbColor(0,255,255)); Button button = new Button(this); button.setLayoutConfig(layoutConfig); button.setBackground(shapeElement); button.setText("启动动画效果"); button.setTextSize(130); 接下来声明一个用于展示的Text,此时这个Text可以视为动画的初始状态,代码如下: Text t = new Text(this); t.setLayoutConfig(layoutConfig); t.setText("动画测试"); t.setTextColor(Color.RED); t.setTextAlignment(Component.HORIZONTAL); t.setTextSize(60); 然后实例化AnimatorValue,并设置变化属性,在这里设置动画的持续时间为2000ms、启动延时为1000ms、循环播放2次,并设置变化曲线为Bounce型,代码如下: AnimatorValue animator = new AnimatorValue(); animator.setDuration(2000); animator.setDelay(1000); animator.setLoopedCount(2); animator.setCurveType(Animator.CurveType.BOUNCE); 接下来需要对AnimatorValue实例设置监听器ValueUpdateListener,其中的回调参数v就是前述中从0到1变化的数值,这个数值会在设置的Duration时间内从0变化到1,接下来将动画样式与v实现连接,如本例中使用了设定文本大小的函数setTextSize((int)(200*v)),那么文本大小会在2s内从200*0=0变化到200*1=200,即完成文本字体大小从0到200的变化,每一时刻的变化速度和之前设置的变化曲线有关。添加监听事件的代码如下: animator.setValueUpdateListener((animatorValue, v) -> t.setTextSize((int) (200 * v))); 为Button设置监听事件,如果检测到按钮单击,则启动动画,代码如下: button.setClickedListener(component -> animator.start()); AnimatorValue动画效果如图3.33所示。 图3.33在Text组件上添加AnimatorValue动画效果图 可以清楚地看到,用于测试的Text组件已经被添加了动画效果,由于Duration设置值为2000,因此这个动画效果会在2s内播放完毕。 3.3.3属性动画 AnimatorProperty 因为AnimatorProperty是对AnimatorValue的自动化封装,内置实现了Component的平移、旋转、缩放的动画效果,使用方法较AnimatorValue更为简单。 首先直接对一个Component(此处以3.3.2节中的Button为例)实例化AnimatorProperty对象,代码如下: AnimatorProperty animator= button.createAnimator(); 为AnimatorProperty实例设置变化属性,可链式调用,代码如下: animator.moveFromX(50).moveToX(1000).alpha(0).setDuration(2500).setDelay(500).setLoopedCount(5); 如上述代码设置了动画的起始X轴位置为50、终止X轴位置为1000、透明度为0、持续时长为2500 ms、启动延时为500 ms和循环次数5次。下面通过start()方法启动动画,代码如下: animator.start(); 其中,AnimatorProperty实例可以重复使用,例如可以使用setTarget()方法改变关联的Component对象,代码如下: animator.setTarget(t); 下面来看一个示例,为了便于展示,新建一个Button组件,并设置LayoutConfig和背景ShapeElement,代码如下: //动画效果布局定义 layoutConfig.setMargins(100,600,100,100); myLayout.setLayoutConfig(layoutConfig); layoutConfig.width=ComponentContainer.LayoutConfig.MATCH_CONTENT; layoutConfig.height = ComponentContainer.LayoutConfig.MATCH_CONTENT; ShapeElement shapeElement = new ShapeElement(); shapeElement.setShape(ShapeElement.RECTANGLE); shapeElement.setCornerRadius(80); shapeElement.setRgbColor(new RgbColor(0,255,255)); Button button = new Button(this); button.setLayoutConfig(layoutConfig); button.setBackground(shapeElement); button.setText("启动动画效果"); button.setTextSize(130); 接下来创建一个属性动画,将这个动画和前面的Button实例关联起来。 设置动画的效果为2.5s内从X轴的50位置,移动到屏幕水平方向上500的位置,然后旋转90°,代码如下: AnimatorProperty animator = button.createAnimatorProperty(); animator.moveFromX(50).moveToX(500).rotate(90).alpha((float) 0.5).setDuration(2500).setDelay(500); animator.setCurveType(Animator.CurveType.BOUNCE); 运行上述代码,效果如图3.34所示。 图3.34属性动画效果 3.3.4动画集合AnimatorGroup 如果需要使用一个组合动画,可以把多个动画对象添加到AnimatorGroup中。AnimatorGroup提供了两种方法: runSerially()和runParallel(),分别表示动画按序列启动和动画并发启动。下面是一个简单的示例。 首先需要声明一个动画集合AnimatorGroup,代码如下: AnimatorGroup animatorGroup = new AnimatorGroup(); 其次,实例化多个动画,并将它们添加到动画集合中。这里实例化了4个动画animator1~animator4,分别配置在4个按钮button1~button4上。动画的配置方法均参考3.3.3节的属性动画,animator1只从左向右移动,animator2从右向左移动并旋转90°,animator3从左向右移动并旋转90°,animator4与animator2相同,代码如下: //动画的实例化与配置 AnimatorProperty animator1 = button1.createAnimatorProperty(); animator1.moveFromX(50).moveToX(500).alpha((float) 0.5).setDuration(2500).setDelay(500); AnimatorProperty animator2 = button2.createAnimatorProperty(); animator2.moveFromX(50).moveToX(500).rotate(90).alpha((float) 0.5).setDuration(2500).setDelay(500); AnimatorProperty animator3 = button3.createAnimatorProperty(); animator3.moveFromX(500).moveToX(50).rotate(90).alpha((float) 0.5).setDuration(2500).setDelay(500); AnimatorProperty animator4 = button4.createAnimatorProperty(); animator4.moveFromX(50).moveToX(500).rotate(90).alpha((float) 0.5).setDuration(2500).setDelay(500); AnimatorGroup animatorGroup = new AnimatorGroup(); //4个动画同时播放 animatorGroup.runParallel(animator1, animator2, animator3, animator4); 设置一个按钮并配置监听事件,用于启动动画集合,代码如下: button5.setClickedListener(new Component.ClickedListener() { @Override public void onClick(Component component) { animatorGroup.start(); } }); 运行上述代码,可以看到动画1至动画4同时播放的效果,如图3.35所示。 图3.35同时播放动画组效果 若将上文中的animatorGroup.runParallel (animator1,animator2,animator3,animator4)更改为animatorGroup.runSerially(animator1,animator2,animator3,animator4),则动画将从并行播放变为序列播放,效果如图3.36所示。 图3.36顺序播放动画组效果 为了更加灵活地处理多个动画的播放,例如一些动画序列播放,而另一些动画并行播放,Java UI 框架提供了更方便的动画 Builder 接口。首先声明AnimatorGroup Builder,使用addAnimators()方法为Builder添加多个动画,同一个addAnimators()内的动画并行播放,不同的addAnimators()中的动画按添加顺序播放。本例中动画的播放效果为首先播放完animator1,随后并行启动animator2和animator3至两者全部播放完毕,最后启动animator4,代码如下: //动画播放 AnimatorGroup animatorGroup = new AnimatorGroup(); AnimatorGroup.Builder animatorGroupBuilder = animatorGroup.build(); animatorGroupBuilder.addAnimators(animator1).addAnimators(animator2, animator3).addAnimators(animator4); //4 个动画的顺序为: animator1 -> animator2/animator3 -> animator4 button5.setClickedListener(new Component.ClickedListener() { @Override public void onClick(Component component) { animatorGroup.start(); } }); 运行代码得到效果如图3.37所示。 图3.37动画组Builder接口效果