第5章 方舟开发框架(ArkUI)—— 基于JS扩展的类Web开发范式 方舟开发框架是一种跨设备的高性能UI开发框架,支持声明式编程和跨设备多态UI,适用于手机、平板、智慧屏和智能穿戴应用开发。方舟开发框架包括基于JS扩展的类Web开发范式和TS扩展的声明式开发范式。本章对基于JS扩展的类Web开发范式进行描述。 视频讲解 5.1开发概述 基于JS扩展的类Web开发基础功能如下。 1. 类Web范式编程 采用类HTML和CSS Web编程语言作为页面布局和页面样式的开发语言,页面业务逻辑则支持ECMAScript规范的JavaScript语言。方舟开发框架提供的类Web编程范式,可以让开发者避免编写UI状态切换的代码,视图配置信息更加直观。 2. 跨设备 开发框架架构上支持UI跨设备显示能力,运行时自动映射到不同设备类型,开发者无感知,降低多设备适配成本。 3. 高性能 开发框架包含许多核心的控件,例如列表、图片和各类容器组件等,针对声明式语法进行了渲染流程的优化。 使用基于JS扩展的类Web开发范式的方舟开发框架,包括应用层(Application)、前端框架层(Framework)、引擎层(Engine)和平台适配层(Porting Layer),如图51所示。 图51方舟开发框架 (1) 应用层。 应用层表示开发的FA应用,这里的FA特指JS FA应用。 (2) 前端框架层。 前端框架层主要完成前端页面解析,以及提供MVVM(ModelViewViewModel)开发模式、页面路由机制和自定义组件等功能。 (3) 引擎层。 引擎层主要提供动画解析、DOM(Document Object Model)树构建、布局计算、渲染命令构建与绘制、事件管理等功能。 (4) 平台适配层。 平台适配层主要完成对平台层进行抽象,提供抽象接口,可以对接到系统平台。例如,事件对接、渲染管线对接和系统生命周期对接等。 视频讲解 5.2JS FA初步应用 基于JS扩展的类Web开发范式支持纯JavaScript、JavaScript和Java混合语言开发。JS FA是基于JavaScript、JavaScript与Java混合开发的FA。 5.2.1JS FA概述 JS FA在HarmonyOS上运行时,需要基类AceAbility、加载JS FA主体的方法、JS FA开发目录,具体说明如下。 1. AceAbility AceAbility类是JS FA在HarmonyOS上运行环境的基类,继承自Ability。开发者的应用运行入口类应该从该类派生,相关代码如下。 public class MainAbility extends AceAbility { @Override public void onStart(Intent intent) { super.onStart(intent); } @Override public void onStop() { super.onStop(); } } 2. 加载JS FA JS FA生命周期事件分为应用生命周期和页面生命周期,应用通过AceAbility类中setInstanceName()接口设置该Ability的实例资源,并通过AceAbility窗口进行显示及全局应用生命周期管理。 setInstanceName(String name)的参数name是指实例名称,实例名称与config.json文件中module.js.name的值对应。若开发者未修改实例名称,而使用了默认值default,则无须调用此接口。若已经修改,则需在应用Ability实例的onStart()中调用此接口,并将参数name设置为修改后的实例名称。多实例应用的module.js字段中有多个实例项,使用时选择相应的实例名称。 setInstanceName()接口使用方法: 在MainAbility的onStart()中的super.onStart()前调用此接口。以JSComponentName作为实例名称,需在super.onStart(Intent)前调用此接口,相关代码如下。 public class MainAbility extends AceAbility { @Override public void onStart(Intent intent) { setInstanceName("JSComponentName"); //config.json配置文件中module.js.name的标签值 super.onStart(intent); } } 3. JS FA开发目录 图52新建工程的JS目录 新建工程的JS目录如图52所示。 在工程目录中,i18n下存放多语言的json文件; pages文件夹下存放多个页面,每个页面由HML、CSS和JS文件组成。HML是一套类HTML的标记语言,通过组件、事件构建出页面的内容。页面具备数据绑定、事件绑定、列表渲染、条件渲染等高级能力。 (1) main→js→default→i18n→enUS.json: 此文件定义了在英文模式下页面显示的变量内容。 { "strings": { "hello": "Hello", "world": "World" } } 同理,zhCN.json中定义了中文模式下的页面内容。 (2) main→js→default→pages→index→index.hml: 此文件定义了index页面的布局、用到的组件,以及这些组件的层级关系。例如,index.hml文件中包含了一个text组件,内容为Hello World文本。 <div class="container"> <text class="title"> {{ $t('strings.hello') }} {{title}} </text> </div> (3) main→js→default→pages→index→index.css: 此文件定义了index页面的样式(index.css文件定义了container和title)。 .container { flex-direction: column; justify-content: center; align-items: center; } .title { font-size: 100px; } (4) main→js→default→pages→index→index.js: 此文件定义了index页面的业务逻辑(数据绑定、事件处理等)。示例: 变量title赋值为字符串World。 export default { data: { title: '', }, onInit() { this.title=this.$t('strings.world'); }, } 5.2.2JS FA开发应用 本节主要介绍JS FA开发应用。该应用通过media query同时适配手机和TV,单击或者将焦点移动到食物的缩略图选择不同的食物图片,也可以添加到购物车,如图53和图54所示。 1. 构建页面布局 在index.hml文件中构建页面布局,进行代码开发之前,首先要对页面布局进行分析,将页面分解为不同区,用容器组件承载。根据JS FA应用效果图,此页面共分成三部分: 标题区、展示区和详情区。展示区和详情区在手机和TV上分别是按列排列和按行排列。 标题区较为简单,由两个按列排列的text组件构成。展示区包含4个image组件的swiper,详情区由image和text组件构成。下面以手机效果图为例,展示区和详情区布局如图55所示。 图53手机应用效果 图54TV应用效果 图55展示区和详情区布局 根据布局结构的分析,实现页面基础布局的代码如下(其中4个image组件通过for指令循环创建)。 <!-- index.hml --> <div class="container"> <!-- title area --> <div class="title"> <text class="name">Food</text> <text class="sub-title">Choose What You Like</text> </div> <div class="display-style"> <!-- display area --> <swiper id="swiperImage" class="swiper-style"> <image src="{{$item}}" class="image-mode" focusable="true" for="{{imageList}}"></image> </swiper> <!-- product details area --> <div class="container"> <div class="selection-bar-container"> <div class="selection-bar"> <image src="{{$item}}" class="option-mode" onfocus="swipeToIndex({{$idx}})" onclick="swipeToIndex({{$idx}})" for="{{imageList}}"></image> </div> </div> <div class="description-first-paragraph"> <text class="description">{{descriptionFirstParagraph}}</text> </div> <div class="cart"> <text class="{{cartStyle}}" onclick="addCart" onfocus="getFocus" onblur="lostFocus" focusable="true">{{cartText}}</text> </div> </div> </div> </div> swiper组件里展示的图片需要自行添加图片资源,放置到js→default→common目录下,common目录需自行创建。 2. 构建页面样式 index.css文件通过media query管控手机和TV不同页面样式。此外,该页面样式还采用了CSS伪类的写法,当单击或者焦点移动到image组件上时,image组件由半透明变成不透明,以此实现选中的效果,相关代码请扫描二维码获取。 3. 构建页面逻辑 在index.js文件中构建页面逻辑,主要实现两个功能: 单击或者焦点移动到不同的缩略图时,swiper滑动到相应的图片; 焦点移动到购物车时,Add To Cart背景颜色从浅蓝色变成深蓝色,单击后文字变为Cart+1,背景颜色由深蓝色变成黄色。添加购物车不可重复操作,逻辑页面相关代码如下。 //index.js export default { data: { cartText: 'Add To Cart', cartStyle: 'cart-text', isCartEmpty: true, descriptionFirstParagraph: 'This is a food page containing fresh fruits, snacks and etc. You can pick whatever you like and add it to your cart. Your order will arrive within 48 hours. We guarantee that our food is organic and healthy. Feel free to access our 24h online service for more information about our platform and products.', imageList: ['/common/food_000.JPG', '/common/food_001.JPG', '/common/food_002.JPG', '/common/food_003.JPG'], }, swipeToIndex(index) { this.$element('swiperImage').swipeTo({index: index}); }, addCart() { if (this.isCartEmpty) { this.cartText='Cart + 1'; this.cartStyle='add-cart-text'; this.isCartEmpty=false; } }, getFocus() { if (this.isCartEmpty) { this.cartStyle='cart-text-focus'; } }, lostFocus() { if (this.isCartEmpty) { this.cartStyle='cart-text'; } }, } 4. 配置设备类型 在config.json的deviceType字段中添加手机和TV的设备类型。 { ... "module": { ... "deviceType": [ "phone", "tv" ], ... } } 视频讲解 5.3构建用户界面 本节主要介绍组件、构建布局、添加交互、动画、事件、页面路由和焦点逻辑。 5.3.1组件 组件(Component)是构建页面的核心,每个组件通过对数据和方法的简单封装,实现独立的可视、可交互功能单元。组件之间相互独立,随取随用,既可以在需求相同的地方重复使用,也可以通过组件间合理的搭配定义满足业务需求的新组件,减少开发量,实现自定义开发的组件。组件根据功能,可以分为以下几类,如表51所示。 表51组件类型及组件名称 组 件 类 型 组 件 名 称 容器组件 badge、dialog、div、form、list、listitem、listitemgroup、panel、popup、refresh、stack、stepper、stepperitem、swiper、tabs、tabbar、tabcontent 基础组件 button、chart、divider、image、imageanimator、input、label、marquee、menu、option、picker、pickerview、piece、progress、qrcode、rating、richtext、search、select、slider、span、switch、text、textarea、toolbar、toolbaritem、toggle、web 媒体组件 camera、video 画布组件 canvas 栅格组件 gridcontainer、gridrow、gridcol svg组件 svg、rect、circle、ellipse、path、line、polyline、polygon、text、tspan、textPath、animate、animateMotion、animateTransform 5.3.2构建布局 本节主要对布局说明、添加标题行和文本区域、添加图片区域、添加留言区域、添加容器进行描述。 1. 布局说明 手机和智慧屏的基准宽度为720px(px为逻辑像素,非物理像素),实际显示效果会根据屏幕宽度进行缩放,其换算关系如下。 组件的width设为100px时,在宽度为720物理像素的屏幕上,实际显示为100物理像素; 在宽度为1440物理像素的屏幕上,实际显示为200物理像素。智能穿戴的基准宽度为454px,换算逻辑同理。 一个页面的基本元素包含标题区域、文本区域和图片区域等,每个基本元素内还包含多个子元素,根据需求可以添加按钮、开关、进度条等组件。在构建页面布局时,需要对每个基本元素思考以下几个问题: 元素的尺寸和排列位置、是否有重叠的元素、是否需要设置对齐、内间距或者边界、是否包含子元素及其排列位置、是否需要容器组件及其类型。 将页面中的元素分解之后再对每个基本元素按顺序实现,可以减少多层嵌套造成的视觉混乱和逻辑混乱,提高代码的可读性,方便对页面做后续调整,如图56和图57所示。 图56页面布局分解 图57留言区布局分解 2. 添加标题行和文本区域 实现标题和文本区域常用的是基础组件text。text组件用于展示文本,可以设置不同的属性和样式,文本内容需要写在标签内容区,插入标题和文本区域的相关代码如下。 <!-- xxx.hml --> <div class="container"> <text class="title-text">{{headTitle}}</text> <text class="paragraph-text">{{paragraphFirst}}</text> <text class="paragraph-text">{{paragraphSecond}}</text> </div> /* xxx.css */ .container { flex-direction: column; margin-top: 20px; margin-left: 30px; } .title-text { color: #1a1a1a; font-size: 50px; margin-top: 40px; margin-bottom: 20px; } .paragraph-text { color: #000000; font-size: 35px; line-height: 60px; } //xxx.js export default { data: { headTitle: 'Capture the Beauty in This Moment', paragraphFirst: 'Capture the beauty of light during the transition and fusion of ice and water. At the instant of movement and stillness, softness and rigidity, force and beauty, condensing moving moments.', paragraphSecond: 'Reflecting the purity of nature, the innovative design upgrades your visual entertainment and ergonomic comfort. Effortlessly capture what you see and let it speak for what you feel.', }, } 3. 添加图片区域 添加图片区域通常用image组件实现,使用方法与text组件类似。图片资源建议放在js→default→common目录下,common目录需自行创建,相关代码如下。 <!-- xxx.hml --> <image class="img" src="{{middleImage}}"></image> /*xxx.css*/ .img { margin-top: 30px; margin-bottom: 30px; height: 385px; } //xxx.js export default { data: { middleImage: '/common/ice.png', }, } 4. 添加留言区域 用户输入留言后单击完成,留言区域即显示留言内容。用户单击右侧的“删除”按钮可删除当前留言内容并重新输入,留言区域由div、text、input关联click事件实现。开发者可以使用input组件实现输入留言,通过text组件实现留言完成,使用commentText的状态标记此时显示的组件(通过if属性控制)。在包含文本“完成”和“删除”的text组件中关联click事件,更新commentText状态和inputValue的内容,相关代码如下。 <!-- xxx.hml --> <div class="container"> <text class="comment-title">Comment</text> <div if="{{!commentText}}"> <input class="comment" value="{{inputValue}}" onchange="updateValue()"></input> <text class="comment-key" onclick="update" focusable="true">Done</text> </div> <div if="{{commentText}}"> <text class="comment-text" focusable="true">{{inputValue}}</text> <text class="comment-key" onclick="update" focusable="true">Delete</text> </div> </div> /* xxx.css */ .container { margin-top: 24px; background-color: #ffffff; } .comment-title { font-size: 40px; color: #1a1a1a; font-weight: bold; margin-top: 40px; margin-bottom: 10px; } .comment { width: 550px; height: 100px; background-color: lightgrey; } .comment-key { width: 150px; height: 100px; margin-left: 20px; font-size: 32px; color: #1a1a1a; font-weight: bold; } .comment-key:focus { color: #007dff; } .comment-text { width: 550px; height: 100px; text-align: left; line-height: 35px; font-size: 30px; color: #000000; border-bottom-color: #bcbcbc; border-bottom-width: 0.5px; } //xxx.js export default { data: { inputValue: '', commentText: false, }, update() { this.commentText=!this.commentText; }, updateValue(e) { this.inputValue=e.text; }, } 5. 添加容器 将页面的基本元素组装在一起,需要使用容器组件。在页面布局中常用div、list和tabs 3种容器组件。在页面结构相对简单时,可以直接用div作为容器,因为div作为单纯的布局容器,可以支持多种子组件,使用起来更为方便。 1) list组件 当页面结构较为复杂时,如果使用div循环渲染,容易出现卡顿,因此推荐使用list组件代替div组件实现长列表布局,从而实现更加流畅的列表滚动体验。list仅支持listitem作为子组件,相关代码如下。 <!-- xxx.hml --> <list class="list"> <list-item type="listItem" for="{{textList}}"> <text class="desc-text">{{$item.value}}</text> </list-item> </list> /* xxx.css */ .desc-text { width: 683.3px; font-size: 35.4px; } //xxx.js export default { data: { textList:[{value: 'JS FA'}], }, } 为避免示例代码过长,以上示例的list中只包含一个listitem和一个text组件。在实际应用中可以在list中加入多个listitem,同时listitem下可以包含多个其他子组件。 2) tabs组件 当页面经常需要动态加载时,推荐使用tabs组件。tabs组件支持change事件,在页签切换后触发。tabs组件仅支持一个tabbar和一个tabcontent,相关代码如下。 <!-- xxx.hml --> <tabs> <tab-bar> <text>Home</text> <text>Index</text> <text>Detail</text> </tab-bar> <tab-content> <image src="{{homeImage}}"></image> <image src="{{indexImage}}"></image> <image src="{{detailImage}}"></image> </tab-content> </tabs> //xxx.js export default { data: { homeImage: '/common/home.png', indexImage: '/common/index.png', detailImage: '/common/detail.png', }, } tabcontent组件用来展示页签的内容区,支持scrollable属性,高度默认充满tabs剩余空间。 5.3.3添加交互 添加交互可以通过在组件上实现关联事件。本节介绍如何用div、text和image组件关联click事件,构建一个点赞按钮。点赞按钮通过一个div组件实现关联click事件。div组件包含一个image和一个text组件。 image组件用于显示未点赞和点赞的效果。click事件的函数会交替更新点赞和未点赞图片的路径。text组件用于显示点赞数,点赞数会在click事件的函数中同步更新。 click事件作为一个函数定义在JS文件中,可以更改isPressed的状态,从而更新显示image组件。如果isPressed为真,则点赞数加1。该函数在HML文件中对应的div组件上生效,点赞按钮各子组件的样式设置在CSS文件当中,相关代码如下。 <!-- xxx.hml --> <!--点赞按钮 --> <div> <div class="like" onclick="likeClick"> <image class="like-img" src="{{likeImage}}" focusable="true"></image> <text class="like-num" focusable="true">{{total}}</text> </div> </div> /*xxx.css*/ .like { width: 104px; height: 54px; border: 2px solid #bcbcbc; justify-content: space-between; align-items: center; margin-left: 72px; border-radius: 8px; } .like-img { width: 33px; height: 33px; margin-left: 14px; } .like-num { color: #bcbcbc; font-size: 20px; margin-right: 17px; } //xxx.js export default { data: { likeImage: '/common/unLike.png', isPressed: false, total: 20, }, likeClick() { var temp; if (!this.isPressed) { temp=this.total + 1; this.likeImage='/common/like.png'; } else { temp=this.total - 1; this.likeImage='/common/unLike.png'; } this.total=temp; this.isPressed=!this.isPressed; }, } 除此之外,还有很多表单组件(开关、标签、滑动选择器等)可在页面布局时灵活使用,提高交互性。 5.3.4动画 动画分为静态动画和连续动画。 1. 静态动画 静态动画的核心是transform样式,可以实现以下3种变换类型,一次样式设置只能实现一种类型变换。 translate: 沿水平或垂直方向将指定组件移动所需距离。scale: 横向或纵向将指定组件缩小或放大到所需比例。rotate: 将指定组件沿横轴、纵轴或中心点旋转指定的角度,相关代码如下。 <!-- xxx.hml --> <div class="container"> <text class="translate">hello</text> <text class="rotate">hello</text> <text class="scale">hello</text> </div> /*xxx.css*/ .container { flex-direction: column; align-items: center; } .translate { height: 150px; width: 300px; font-size: 50px; background-color: #008000; transform: translate(200px); } .rotate { height: 150px; width: 300px; font-size: 50px; background-color: #008000; transform-origin: 200px 100px; transform: rotateX(45deg); } .scale { height: 150px; width: 300px; font-size: 50px; background-color: #008000; transform: scaleX(1.5); } 2. 连续动画 静态动画只有开始状态和结束状态,没有中间状态。如果设置中间的过渡状态和转换效果,需要使用连续动画实现。 连续动画的核心是animation样式,它定义了动画的开始状态、结束状态及时间和速度的变化曲线,通过animation样式实现的效果如下。 animationname: 设置动画执行后应用到组件上的背景颜色、透明度、宽高和变换类型。 animationdelay和animationduration: 分别设置动画执行后元素延迟和持续的时间。 animationtimingfunction: 描述动画执行的速度曲线,使动画更加平滑。 animationiterationcount: 定义动画播放的次数。 animationfillmode: 指定动画执行结束后是否恢复初始状态。 animation样式需要在CSS文件中先定义keyframe,然后设置动画的过渡效果,并通过一个样式类型在HML文件中调用,相关代码如下。 <!-- xxx.hml --> <div class="item-container"> <text class="header">animation-name</text> <div class="item {{colorParam}}"> <text class="txt">color</text> </div> <div class="item {{opacityParam}}"> <text class="txt">opacity</text> </div> <input class="button" type="button" name="" value="show" onclick="showAnimation"/> </div> /*xxx.css*/ .item-container { margin-right: 60px; margin-left: 60px; flex-direction: column; } .header { margin-bottom: 20px; } .item { background-color: #f76160; } .txt { text-align: center; width: 200px; height: 100px; } .button { width: 200px; font-size: 30px; background-color: #09ba07; } .color { animation-name: Color; animation-duration: 8000ms; } .opacity { animation-name: Opacity; animation-duration: 8000ms; } @keyframes Color { from { background-color: #f76160; } to { background-color: #09ba07; } } @keyframes Opacity { from { opacity: 0.9; } to { opacity: 0.1; } } //xxx.js export default { data: { colorParam: '', opacityParam: '', }, showAnimation: function () { this.colorParam=''; this.opacityParam=''; this.colorParam='color'; this.opacityParam='opacity'; }, } 5.3.5事件 事件主要为手势事件和按键事件。手势事件主要用于智能穿戴等具有触摸屏的设备,按键事件主要用于智慧屏设备。 1. 手势事件 手势表示由单个或多个事件识别的语义动作(触摸、单击和长按)。一个完整的手势可能由多个事件组成,对应手势的生命周期,支持的事件如下。 1) 触摸 touchstart: 手指触摸动作开始。 touchmove: 手指触摸后移动。 touchcancel: 手指触摸动作被打断(来电提醒、弹窗)。 touchend: 手指触摸动作结束。 2) 单击 click: 用户快速轻敲屏幕。 3) 长按 longpress: 用户在相同位置长时间保持与屏幕接触,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="text-container" onclick="click"> <text class="text-style">{{onClick}}</text> </div> <div class="text-container" ontouchstart="touchStart"> <text class="text-style">{{touchstart}}</text> </div> <div class="text-container" ontouchmove="touchMove"> <text class="text-style">{{touchmove}}</text> </div> <div class="text-container" ontouchend="touchEnd"> <text class="text-style">{{touchend}}</text> </div> <div class="text-container" ontouchcancel="touchCancel"> <text class="text-style">{{touchcancel}}</text> </div> <div class="text-container" onlongpress="longPress"> <text class="text-style">{{onLongPress}}</text> </div> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; } .text-container { margin-top: 10px; flex-direction: column; width: 750px; height: 50px; background-color: #09ba07; } .text-style { width: 100%; line-height: 50px; text-align: center; font-size: 24px; color: #ffffff; } //xxx.js export default { data: { touchstart: 'touchstart', touchmove: 'touchmove', touchend: 'touchend', touchcancel: 'touchcancel', onClick: 'onclick', onLongPress: 'onlongpress', }, touchCancel: function (event) { this.touchcancel='canceled'; }, touchEnd: function(event) { this.touchend='ended'; }, touchMove: function(event) { this.touchmove='moved'; }, touchStart: function(event) { this.touchstart='touched'; }, longPress: function() { this.onLongPress='longpressed'; }, click: function() { this.onClick='clicked'; }, } 2. 按键事件 按键事件是智慧屏上特有的手势事件,当用户操作遥控器按键时触发。用户单击一个遥控器按键,通常会触发两次key事件: 先触发action为0,再触发action为1,即先触发按下事件,再触发抬起事件。action为2的场景比较少见,一般为用户按下按键且不松开,此时repeatCount将返回次数。每个物理按键对应各自的按键值(keycode)实现不同的功能,相关代码如下。 <!-- xxx.hml --> <div class="card-box"> <div class="content-box"> <text class="content-text" onkey="keyUp" onfocus="focusUp" onblur="blurUp">{{up}}</text> </div> <div class="content-box"> <text class="content-text" onkey="keyDown" onfocus="focusDown" onblur="blurDown">{{down}}</text> </div> </div> /*xxx.css*/ .card-box { flex-direction: column; justify-content: center; } .content-box { align-items: center; height: 200px; flex-direction: column; margin-left: 200px; margin-right: 200px; } .content-text { font-size: 40px; text-align: center; } //xxx.js export default { data: { up: 'up', down: 'down', }, focusUp: function() { this.up='up focused'; }, blurUp: function() { this.up='up'; }, keyUp: function() { this.up='up keyed'; }, focusDown: function() { this.down='down focused'; }, blurDown: function() { this.down='down'; }, keyDown: function() { this.down='down keyed'; }, } 按键事件通过获焦事件向下分发,因此示例中使用了focus事件和blur事件明确当前焦点的位置。单击上下键选中up或down按键,即相应的focused状态,失去焦点的按键恢复正常的up或down按键文本,按确认键后该按键变为keyed状态。 5.3.6页面路由 很多应用由多个页面组成,例如,用户可以从音乐列表页面单击歌曲,跳转到该歌曲的播放界面。此过程通过页面路由进行串联,按需实现跳转。 页面路由router根据页面的URI找到目标页面,从而实现跳转。以最基础的两个页面之间的跳转为例,具体实现步骤如下: 在Project窗口,打开entry→src→main→js→default,右击pages文件夹,选择New→JS Page,创建详情页; 调用router.push()路由到详情页,调用router.back()回到首页。 1. 构建页面布局 index和detail页面均包含一个text组件和button组件。text组件指明当前页面,button组件实现两个页面之间的相互跳转,HML文件相关代码如下。 <!-- index.hml --> <div class="container"> <text class="title">This is the index page.</text> <button type="capsule" value="Go to the second page" class="button" onclick="launch"></button> </div> <!-- detail.hml --> <div class="container"> <text class="title">This is the detail page.</text> <button type="capsule" value="Go back" class="button" onclick="launch"></button> </div> 2. 构建页面样式 构建index和detail页面样式,text组件和button组件居中显示,两个组件间距为50px。CSS相关代码如下(两个页面样式代码一致)。 /*index.css*/ /*detail.css*/ .container { flex-direction: column; justify-content: center; align-items: center; } .title { font-size: 50px; margin-bottom: 50px; } 3. 实现跳转 为了使button组件的launch方法生效,需要在页面的JS文件中实现跳转逻辑。调用router.push()接口,将URI指定的页面添加到路由栈中,即跳转到URI指定的页面。在调用router方法之前,需要导入router模块,相关代码如下。 图58页面路由效果 //index.js import router from '@system.router'; export default { launch() { router.push ({ uri: 'pages/detail/detail', }); }, } //detail.js import router from '@system.router'; export default { launch() { router.back(); }, } 运行效果如图58所示。 5.3.7焦点逻辑 焦点移动是智慧屏的主要交互方式,本节介绍焦点逻辑的相关规则。 1. 容器组件焦点分发逻辑 容器组件在第一次获焦时焦点一般落在第一个可获焦的子组件上,再次获焦时焦点落在上一次失去焦点时获焦的子组件上。容器组件一般都有特定的焦点分发逻辑,常用容器组件的焦点分发逻辑说明如下。 (1) div组件通过按键移动获焦时,焦点会移动到在移动方向上与当前获焦组件布局中心距离最近的可获焦叶子节点上。如图59所示,焦点在上方横向div的第二个子组件上,当单击Down按键时,焦点要移动到下方的横向div中,这时下方横向div中的子组件会与当前焦点所在的子组件进行布局中心距离的计算,其中距离最近的子组件获焦。 图59div焦点移动时距离计算示例 (2) list组件包含listitem与listitemgroup,list组件每次获焦时会使第一个可获焦的item获焦。listitemgroup为特殊的listitem,且两者都与div的焦点逻辑相同。 (3) stack组件只能由自顶而下的第一个可获焦的子组件获焦。 (4) swiper的每个页面和refresh页面焦点逻辑都与div的相同。 (5) tabs组件包含tabbar与tabcontent,tabbar中的子组件默认均能获焦,与是否有可获焦的叶子节点无关。tabbar与tabcontent的每个页面都与div的焦点逻辑相同。 (6) dialog的button可获焦,若有多个button,默认初始焦点落在第二个button上。 (7) popup无法获焦。 2. focusable属性使用 通用属性focusable主要用于控制组件能否获焦,本身不支持焦点的组件在设置此属性后可以拥有获取焦点的能力。例如,text组件本身不能获焦,焦点无法移动到它上面,设置text的focusable属性为true后,text组件便可以获焦。如果在未使用focusable属性的情况下,使用了focus、blur或key事件,会默认添加focusable属性为true。 容器组件能否可获焦依赖于是否拥有可获焦的子组件。如果容器组件内没有可以获焦的子组件,即使设置了focusable为true,依然不能获焦。若容器组件focusable属性设置为false,则它本身和所包含的所有组件都不可获焦。 视频讲解 5.4常见组件开发 常见组件包括Text、Input、Button、List、Picker、Dialog、Form、Stepper、Tabs、Image。下面分别介绍功能及其相关代码。 5.4.1Text Text是文本组件,用于呈现一段文本信息。 1. 创建Text组件 在pages/index目录下的HML文件中创建一个Text组件,相关代码如下。 <!-- xxx.hml --> <div class="container" style="text-align: center;justify-content: center; align-items: center;"> <text> Hello World </text> </div> /*xxx.css*/ .container { flex-direction: column; align-items: center; justify-content: center; background-color: #F1F3F5; } 2. 设置Text组件样式和属性 设置Text组件样式和属性步骤如下。 1) 添加文本样式 设置color、fontsize和allowscale属性分别为文本添加颜色、大小和缩放,相关代码如下。 <!-- xxx.hml --> <div class="container" style="background-color:#F1F3F5;justify-content: center; align-items: center;"> <text style="color: blueviolet; font-size: 40px; allow-scale:true"> This is a passage </text> </div> 2) 添加画线 设置textdecoration属性为文本添加画线,相关代码如下。 <!-- xxx.hml --> <div class="container" style="background-color:#F1F3F5;"> <text style="text-decoration:underline"> This is a passage </text> <text style="text-decoration:line-through"> This is a passage </text> </div> /*xxx.css*/ .container { flex-direction: column; align-items: center; justify-content: center; } text{ font-size: 50px; } 3) 隐藏文本内容 当文本内容过多而显示不全时,添加textoverflow属性将隐藏内容以省略号的形式展现,相关代码如下。 <!-- xxx.hml --> <div class="container"> <text class="text"> This is a passage </text> </div> /*xxx.css*/ .container { flex-direction: column; align-items: center; background-color: #F1F3F5; justify-content: center; } .text{ width: 200px; max-lines: 1; text-overflow:ellipsis; } textoverflow样式需要与maxlines样式配套使用,maxlines属性设置文本最多可以展示的行数,在设置最大行数的情况下方可生效。 4) 设置文本折行 设置wordbreak属性对文本内容做断行处理,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="content"> <text class="text1"> Welcome to the world </text> <text class="text2"> Welcome to the world </text> </div> </div> /*xxx.css*/ .container { background-color: #F1F3F5; flex-direction: column; align-items: center; justify-content: center; } .content{ width: 50%; flex-direction: column; align-items: center; justify-content: center; } .text1{ height: 200px; border:1px solid #1a1919; margin-bottom: 50px; text-align: center; word-break: break-word; font-size: 40px; } .text2{ height: 200px; border:1px solid #0931e8; text-align: center; word-break: break-all; font-size: 40px; } 5) Text组件支持Span子组件 相关代码如下。 <!-- xxx.hml --> <div class="container" style="justify-content: center; align-items: center;flex-direction: column;background-color: #F1F3F5;"> <text style="font-size: 45px;"> This is a passage </text> <text style="font-size: 45px;"> <span style="color: aqua;">This </span> <span style="color: #F1F3F5;"> 1 </span> <span style="color: blue;"> is a </span> <span style="color: #F1F3F5;"> 1 </span> <span style="color: red;">passage </span> </text> </div> 当使用Span子组件组成文本段落时,如果Span属性样式异常(fontweight设置为1000),将导致文本段落显示异常。在使用Span子组件时,Text组件内不能存在文本内容; 如果存在,只会显示子组件Span中的内容。 3. 场景示例 Text组件通过数据绑定展示文本内容,Span组件通过设置show属性实现文本内容的隐藏和显示,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div style="align-items: center;justify-content: center;"> <text class="title"> {{ content }} </text> <switch checked="true" onchange="test"></switch> </div> <text class="span-container" style="color: #ff00ff;"> <span show="{{isShow}}">{{ content}}</span> <span style="color: white;"> 1 </span> <span style="color: #f76160">Hide clip </span> </text> </div> /*xxx.css*/ .container { align-items: center; flex-direction: column; justify-content: center; background-color: #F1F3F5; } .title { font-size: 26px; text-align:center; width: 200px; height: 200px; } //xxx.js export default { data: { isShow:true, content: 'Hello World' }, onInit(){}, test(e) { this.isShow=e.checked } } 5.4.2Input Input是交互式组件,用于接收用户数据,其类型可设置为日期、多选框和按钮等。 1. 创建Input组件 在pages/index目录下的HML文件中创建Input组件,相关代码如下。 <!-- xxx.hml --> <div class="container"> <input type="text"> Please enter the content </input> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; background-color: #F1F3F5; } 2. 设置Input类型 通过设置type属性定义Input类型,将Input设置为button、date等,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="div-button"> <dialog class="dialogClass" id="dialogId"> <div class="content"> <text>this is a dialog</text> </div> </dialog> <input class="button" type="button" value="click" onclick="btnclick"></input> </div> <div class="content"> <input onchange="checkboxOnChange" checked="true" type="checkbox"></input> </div> <div class="content"> <input type="date" class="flex" placeholder="Enter data"></input> </div> </div> /*xxx.css*/ .container { align-items: center; flex-direction: column; justify-content: center; background-color: #F1F3F5 ; } .div-button { flex-direction: column; align-items: center; } .dialogClass{ width:80%; height: 200px; } .button { margin-top: 30px; width: 50%; } .content{ width: 90%; height: 150px; align-items: center; justify-content: center; } .flex { width: 80%; margin-bottom:40px; } //xxx.js export default { btnclick(){ this.$element('dialogId').show() }, } 智能穿戴将Input类型设置为button、radio和checkbox。当Input类型为checkbox和radio时,当前组件选中的属性为checked才生效,默认值为false。 3. 事件绑定 向Input组件添加search和translate事件,相关代码如下。 <!-- xxx.hml --> <div class="content"> <text style="margin-left: -7px;"> <span>Enter text and then touch and hold what you've entered</span> </text> <input class="input" type="text" onsearch="search" placeholder="search"> </input> <input class="input" type="text" ontranslate="translate" placeholder="translate"> </input> </div> /*xxx.css*/ .content { width: 100%; flex-direction: column; align-items: center; justify-content: center; background-color: #F1F3F5; } .input { margin-top: 50px; width: 60%; placeholder-color: gray; } text{ width:100%; font-size:25px; text-align:center; } // xxx.js import prompt from '@system.prompt' export default { search(e){ prompt.showToast({ message:e.value, duration: 3000, }); }, translate(e){ prompt.showToast({ message:e.value, duration: 3000, }); } } 4. 设置输入提示 通过对Input组件添加showError方法提示输入的错误原因,相关代码如下。 <!-- xxx.hml --> <div class="content"> <input id="input" class="input" type="text"maxlength="20" placeholder="Please input text" onchange="change"> </input> <input class="button" type="button" value="Submit" onclick="buttonClick"></input> </div> /*xxx.css*/ .content { width: 100%; flex-direction: column; align-items: center; justify-content: center; background-color: #F1F3F5; } .input { width: 80%; placeholder-color: gray; } .button { width: 30%; margin-top: 50px; } //xxx.js import prompt from '@system.prompt' export default { data:{ value:'', }, change(e){ this.value=e.value; prompt.showToast({ message: "value: " + this.value, duration: 3000, }); }, buttonClick(e){ if(this.value.length > 6){ this.$element("input").showError({ error:'Up to 6 characters are allowed.' }); }else if(this.value.length == 0){ this.$element("input").showError({ error:this.value + 'This field cannot be left empty.' }); }else{ prompt.showToast({ message: "success " }); } }, } 该方法在Input类型为text、email、date、time、number和password时生效。 5. 场景示例 根据场景选择不同类型的Input输入框,完成信息录入,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="label-item"> <label>memorandum</label> </div> <div class="label-item"> <label class="lab" target="input1">content:</label> <input class="flex" id="input1" placeholder="Enter content" /> </div> <div class="label-item"> <label class="lab" target="input3">date:</label> <input class="flex"id="input3" type="date" placeholder="Enter data" /> </div> <div class="label-item"> <label class="lab" target="input4">time:</label> <input class="flex" id="input4" type="time" placeholder="Enter time" /> </div> <div class="label-item"> <label class="lab" target="checkbox1">Complete:</label> <input class="flex" type="checkbox" id="checkbox1" style="width: 100px;height: 100px;" /> </div> <div class="label-item"> <input class="flex" type="button" id="button" value="save" onclick="btnclick"/> </div> </div> /*xxx.css*/ .container { flex-direction: column; background-color: #F1F3F5; } .label-item { align-items: center; border-bottom-width: 1px;border-color: #dddddd; } .lab { width: 400px;} label { padding: 30px; font-size: 30px; width: 320px; font-family: serif; color: #9370d8; font-weight: bold; } .flex { flex: 1; } .textareaPadding { padding-left: 100px; } //xxx.js import prompt from '@system.prompt'; export default { data: { }, onInit() { }, btnclick(e) { prompt.showToast({ message:'Saved successfully!' }) } } 5.4.3Button Button是按钮组件,其类型包括胶囊按钮、圆形按钮、文本按钮、弧形按钮、下载按钮。 1. 创建Button组件 在pages/index目录下的hml文件中创建Button组件,相关代码如下。 <!-- xxx.hml --> <div class="container"> <buttontype="capsule" value="Capsule button"></button> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; background-color: #F1F3F5; } 2. 设置Button类型 通过设置Button的type属性选择按钮类型,例如定义Button为圆形按钮、文本按钮等,相关代码如下。 <!-- xxx.hml --> <div class="container"> <button class="circle" type="circle" >+</button> <button class="text" type="text"> button</button> </div> /*xxx.css*/ .container { background-color: #F1F3F5; flex-direction: column; align-items: center; justify-content: center; } .circle { font-size: 120px; background-color: blue; radius: 72px; } .text { margin-top: 30px; text-color: white; font-size: 30px; font-style: normal; background-color: blue; width: 50%; height: 100px; } 胶囊按钮(type=capsule)不支持border相关样式。圆形按钮(type=circle)不支持文本相关样式。文本按钮(type=text)自适应文本大小,不支持尺寸样式设置(radius、width和height),背景透明不支持backgroundcolor样式。Button组件使用的icon图标如果来自云端路径,需要添加网络访问权限 ohos.permission.INTERNET,在resources文件夹下的config.json文件中进行权限配置,相关代码如下。 <!-- config.json --> "module": { "reqPermissions": [{ "name": "ohos.permission.INTERNET" }], } 3. 显示下载进度 为Button组件添加progress方法,实时显示下载的进度,相关代码如下。 <!-- xxx.hml --> <div class="container"> <button class="button download" type="download" id="download-btn" onclick="setProgress">{{downloadText}}</button> </div> /*xxx.css*/ .container { background-color: #F1F3F5; flex-direction: column; align-items: center; justify-content: center; } .download { width: 280px; text-color: white; background-color: #007dff; } //xxx.js import prompt from '@system.prompt'; export default { data: { percent: 0, downloadText: "Download", isPaused: true, intervalId : null, }, star(){ this.intervalId=setInterval(()=>{ if(this.percent <100){ this.percent += 1; this.downloadText=this.percent+ "%"; } else{ prompt.showToast({ message: "Download succeeded." }) this.paused() this.downloadText="Download"; this.percent=0; this.isPaused=true; } },100) }, paused(){ clearInterval(this.intervalId); this.intervalId=null; }, setProgress(e) { if(this.isPaused){ prompt.showToast({ message: "Download started" }) this.star(); this.isPaused=false; }else{ prompt.showToast({ message: "Paused." }) this.paused(); this.isPaused=true; } } } setProgress方法只支持Button的类型为download。 4. 场景示例 在本场景中,可根据输入的文本内容进行Button类型切换,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="input-item"> <input class="input-text" id="change" type="{{mytype}}"placeholder="{{myholder}}" style="background-color:{{mystyle1}}; placeholder-color:{{mystyle2}};flex-grow:{{myflex}};"name="{{myname}}" value="{{myvalue}}"></input> </div> <div class="input-item"> <div class="doc-row"> <input type="button" class="select-button color-3" value="text" onclick="changetype3"></input> <input type="button" class="select-button color-3" value="data" onclick="changetype4"></input> </div> </div> </div> /*xxx.css*/ .container { flex-direction: column; align-items: center; background-color: #F1F3F5; } .input-item { margin-bottom: 80px; flex-direction: column; } .doc-row { justify-content: center; margin-left: 30px; margin-right: 30px; justify-content: space-around; } .input-text { height: 80px; line-height: 80px; padding-left: 30px; padding-right: 30px; margin-left: 30px; margin-right: 30px; margin-top:100px; border: 3px solid; border-color: #999999; font-size: 30px; background-color: #ffffff; font-weight: 400; } .select-button { width: 35%; text-align: center; height: 70px; padding-top: 10px; padding-bottom: 10px; margin-top: 30px; font-size: 30px; color: #ffffff; } .color-3 { background-color: #0598db;; } //xxx.js export default { data: { myflex: '', myholder: 'Enter text.', myname: '', mystyle1: "#ffffff", mystyle2: "#ff0000", mytype: 'text', myvalue: '', }, onInit() { }, changetype3() { this.myflex=''; this.myholder='Enter text.'; this.myname=''; this.mystyle1="#ffffff"; this.mystyle2="#FF0000"; this.mytype='text'; this.myvalue=''; }, changetype4() { this.myflex=''; this.myholder='Enter a date.'; this.myname=''; this.mystyle1="#ffffff"; this.mystyle2="#FF0000"; this.mytype='date'; this.myvalue=''; }, } 其他组件介绍,请扫描二维码获取。 视频讲解 5.5动效开发 本节主要介绍CSS和JS动画。其中,CSS动画包括属性样式、transform样式、backgroundposition样式动画。JS动画包括组件和插值器动画。 5.5.1CSS动画开发 本部分包括属性样式动画、transform样式动画和backgroundposition样式动画。 1. 属性样式动画 在关键帧(Keyframes)中动态设置父组件的width和height值实现变大缩小,子组件设置scale属性实现父子组件同时缩放,再设置opacity实现父子组件的显示与隐藏,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="fade"> <text>fading away</text> </div> <div class="bigger"> <text>getting bigger</text> </div> </div> /*xxx.css*/ .container { background-color:#F1F3F5; display: flex; justify-content: center; align-items: center; flex-direction: column; } .fade{ width: 30%; height: 200px; left: 35%; top: 25%; position: absolute; animation: 2s change infinite friction; } .bigger{ width: 20%; height: 100px; background-color: blue; animation: 2s change1 infinite linear-out-slow-in; } text{ width: 100%; height: 100%; text-align: center; color: white; font-size: 35px; animation: 2s change2 infinite linear-out-slow-in; } /*颜色变化*/ @keyframes change{ from { background-color: #f76160; opacity: 1; } to { background-color: #09ba07; opacity: 0; } } /*父组件大小变化*/ @keyframes change1{ 0% { width: 20%; height: 100px; } 100% { width: 80%; height: 200px; } } /*子组件文字缩放*/ @keyframes change2{ 0%{ transform: scale(0); } 100% { transform: scale(1.5); } } animation取值不区分先后,duration(动画执行时间)/delay(动画延迟执行时间)按照出现的先后顺序解析。必须设置animationduration样式,否则时长为0则不会有动画效果。当设置animationfillmode属性为forwards时,组件直接展示最后一帧的样式。 2. transform样式动画 设置transform属性对组件进行旋转、缩放、移动和倾斜。 1) 设置静态动画 创建一个正方形并旋转90°变成菱形,用下方的长方形将菱形下半部分遮盖形成屋顶。首先,设置长方形translate属性值为(150px,-150px),确定坐标位置形成门; 其次,使用position属性将横纵线跟随父组件(正方形)移动到指定坐标位置; 再次,设置scale属性使父子组件一起变大形成窗户大小; 最后,使用skewX属性使组件倾斜后设置坐标translate(200px,-830px)得到烟囱,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="top"></div> <div class="content"></div> <div class="door"></div> <!--窗户 --> <div class="window"> <div class="horizontal"></div> <div class="vertical"></div> </div> <div class="chimney"></div> </div> /*xxx.css*/ .container { background-color:#F1F3F5; align-items: center; flex-direction: column; } .top{ z-index: -1; position: absolute; width: 428px; height: 428px; background-color: #860303; transform: rotate(45deg); margin-top: 230px; margin-left: 266px; } .content{ margin-top: 500px; width: 600px; height: 400px; background-color: white; border:1px solid black; } .door{ width: 100px; height: 150px; background-color: #1033d9; transform: translate(150px,-150px); } .window{ z-index: 1; position: relative; width: 100px; height: 100px; background-color: white; border: 1px solid black; transform: translate(-150px,-400px) scale(1.5); } /*窗户的横轴*/ .horizontal{ position: absolute; top: 50%; width: 100px; height: 5px; background-color: black; } /*窗户的纵轴*/ .vertical{ position: absolute; left: 50%; width: 5px; height: 100px; background-color: black; } .chimney{ z-index: -2; width: 40px; height: 100px; border-radius: 15px; background-color: #9a7404; transform: translate(200px,-830px) skewX(-5deg); } 2) 设置平移动画 小球下降动画,改变小球的Y轴坐标实现小球下落,在下一段时间内减小Y轴坐标实现小球回弹,让每次回弹的高度逐次减小直至回弹高度为0,即模拟出小球下降的动画,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="circle"></div> <div class="flower"></div> </div> /*xxx.css*/ .container { background-color:#F1F3F5; display: flex; justify-content: center; } .circle{ width: 100px; height: 100px; border-radius: 50px; background-color: red; /*forwards停在动画的最后一帧*/ animation: down 3s fast-out-linear-in forwards; } .flower{ position: fixed; width: 80%; margin-left: 10%; height: 5px; background-color: black; top: 1000px; } @keyframes down { 0%{ transform: translate(0px,0px); } /*下落*/ 15%{ transform: translate(10px,900px); } /*开始回弹*/ 25%{ transform: translate(20px,500px); } /*下落*/ 35%{ transform: translate(30px,900px); } /*回弹*/ 45%{ transform: translate(40px,700px); } 55%{ transform: translate(50px,900px); } 65%{ transform: translate(60px,800px); } 80%{ transform: translate(70px,900px); } 90%{ transform: translate(80px,850px); } /*停止*/ 100%{ transform: translate(90px,900px); } } 3) 设置旋转动画 设置不同的原点位置(transformorigin)改变元素围绕的旋转中心。rotate3d属性前三个参数值分别为X轴、Y轴、Z轴的旋转向量,第四个值为旋转角度,旋转角度可为负值,负值代表旋转方向为逆时针,相关代码请扫描二维码获取。 transformorigin变换对象的原点位置,如果仅设置一个值,另一个值为50%,若设置两个值,则第一个值表示X轴的位置,第二个值表示Y轴的位置。 4) 设置缩放动画 设置scale样式属性实现涟漪动画。首先,使用定位确定元素的位置,确定坐标后创建多个组件实现重合效果; 其次,设置opacity属性改变组件不透明度实现组件隐藏与显示,同时设置scale值,使组件可以一边放大一边隐藏; 最后,设置两个组件不同的动画执行时间,实现扩散效果。设置sacle3d中X轴、Y轴、Z轴的缩放参数实现动画,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="circle"> <text>ripple</text> </div> <div class="ripple"></div> <div class="ripple ripple2"></div> <!-- 3d --> <div class="content"> <text>spring</text> </div> </div> /*xxx.css*/ .container { flex-direction: column; background-color:#F1F3F5; width: 100%; position: relative; } .circle{ margin-top: 400px; margin-left: 40%; width: 100px; height: 100px; border-radius: 50px; background:linear-gradient(#dcaec1, #d3a8e3); z-index: 1; position: absolute; } .ripple{ margin-top: 400px; margin-left: 40%; position: absolute; z-index: 0; width: 100px; height: 100px; border-radius: 50px; background:linear-gradient(#dcaec1,#d3a8e3); animation: ripple 5s infinite; } /*设置不同的动画时间*/ .ripple2{ animation-duration: 2.5s; } @keyframes ripple{ 0%{ transform: scale(1); opacity: 0.5; } 50%{ transform: scale(3); opacity: 0; } 100%{ transform: scale(1); opacity: 0.5; } } text{ color: white; text-align: center; height: 100%; width: 100%; } .content { margin-top: 700px; margin-left: 33%; width: 200px; height: 100px; animation:rubberBand 1s infinite; /*设置渐变色*/ background:linear-gradient(#e276aa,#ec0d66); position: absolute; } @keyframes rubberBand { 0% { transform: scale3d(1, 1, 1); } 30% { transform: scale3d(1.25, 0.75, 1.1); } 40% { transform: scale3d(0.75, 1.25, 1.2); } 50% { transform: scale3d(1.15, 0.85, 1.3); } 65% { transform: scale3d(.95, 1.05, 1.2); } 75% { transform: scale3d(1.05, .95, 1.1); } 100%{ transform: scale3d(1, 1, 1); } } 设置transform属性值后,子元素会随父元素一起改变,若只改变父元素其他属性值(如height、width),则子元素不会改变。 5) 设置matrix属性 matrix是一个入参为6个值的矩阵,6个值分别代表scaleX、skewY、skewX、scaleY、translateX和translateY。下面示例中设置了matrix属性为matrix(1,0,0,1,0,200),使组件移动和倾斜,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="rect"> </div> </div> /*xxx.css*/ .container{ background-color:#F1F3F5; display: flex; justify-content: center; } .rect{ width: 100px; height: 100px; background-color: red; animation: down 3s infinite forwards; } @keyframes down{ 0%{ transform: matrix(1,0,0,1,0,0); } 10%{ transform: matrix(1,0,0,1,0,200); } 60%{ transform: matrix(2,1.5,1.5,2,0,700); } 100%{ transform: matrix(1,0,0,1,0,0); } } 6) 整合transform属性 transform可以设置多个值并且多个值可同时设置,下面示例中展示同时设置缩放(scale)、平移(translate)和旋转(rotate)属性时的动画效果。 <!-- xxx.hml --> <div class="container"> <div class="rect1"></div> <div class="rect2"></div> <div class="rect3"></div> <div class="rect4"></div> <div class="rect5"></div> </div> /*xxx.css*/ .container{ flex-direction:column; background-color:#F1F3F5; padding:50px; } .rect1{ width: 100px; height: 100px; background:linear-gradient(#e77070,#ee0202); animation: change1 3s infinite forwards; } .rect2{ margin-top: 50px; width: 100px; height: 100px; background:linear-gradient(#95a6e8, #2739de); animation: change2 3s infinite forwards; } .rect3{ margin-top: 50px; width: 100px; height: 100px; background:linear-gradient(#142ee2, #8cb1e5); animation: change3 3s infinite; } .rect4{ align-self: center; margin-left: 50px; margin-top: 200px; width: 100px; height: 100px; background:linear-gradient(#e2a8df, #9c67d4,#8245d9,#e251c3); animation: change4 3s infinite; } .rect5{ margin-top: 300px; width: 100px; height: 100px; background:linear-gradient(#e7ded7, #486ccd, #94b4d2); animation: change5 3s infinite; } /*change1 change2对比*/ @keyframes change1{ 0%{ transform: translate(0,0); transform: rotate(0deg) } 100%{ transform: translate(0,500px); transform: rotate(360deg) } } /*change2 change3 对比属性顺序不同的动画效果*/ @keyframes change2{ 0%{ transform:translate(0,0) rotate(0deg) ; } 100%{ transform: translate(300px,0) rotate(360deg); } } @keyframes change3{ 0%{ transform:rotate(0deg) translate(0,0); } 100%{ transform:rotate(360deg)translate(300px,0); } } /*属性值不对应的情况*/ @keyframes change4{ 0%{ transform: scale(0.5); } 100%{ transform:scale(2) rotate(45deg); } } /*多属性的写法*/ @keyframes change5{ 0%{ transform:scale(0) translate(0,0) rotate(0); } 100%{ transform: scale(1.5) rotate(360deg) translate(200px,0); } } 当设置多个transform时,后续的transform值会将前面的覆盖。若想同时使用多个动画样式可用复合写法,例如transform: scale(1) rotate(0) translate(0,0)。transform进行复合写法时,变化样式内多个样式值顺序的不同会呈现不同的动画效果。transform属性设置的样式值要一一对应,若前后不一致,则该动画不生效。若设置多个样式值,则只会呈现出已对应值的动画效果。 3. backgroundposition样式动画 通过改变backgroundposition属性(第一个值为X轴的位置,第二个值为Y轴的位置)移动背景图片位置,若背景图位置超出组件,则超出部分的背景图不显示,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div class="content"></div> <div class="content1"></div> </div> /*xxx.css*/ .container { background-color:#F1F3F5; display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; } .content{ width: 400px; height: 400px; background-image: url('common/images/bg-tv.jpg'); background-size: 100%; background-repeat: no-repeat; animation: change 3s infinite; border: 1px solid black; } .content1{ margin-top:50px; width: 400px; height: 400px; background-image: url('common/images/bg-tv.jpg'); background-size: 50%; background-repeat: no-repeat; animation: change1 5s infinite; border: 1px solid black; } /*背景图片移动出组件*/ @keyframes change{ 0%{ background-position:0px top; } 25%{ background-position:400px top; } 50%{ background-position:0px top; } 75%{ background-position:0px bottom; } 100%{ background-position:0px top; } } /*背景图片在组件内移动*/ @keyframes change1{ 0%{ background-position:left top; } 25%{ background-position:50% 50%; } 50%{ background-position:right bottom; } 100%{ background-position:left top;; } } backgroundposition仅支持背景图片的移动,不支持背景颜色(backgroundcolor)。 5.5.2JS动画 本部分介绍组件动画、插值器动画和动画帧。 1. 组件动画 在组件上创建和运行动画的快捷方式步骤如下。 1) 获取动画对象 通过调用animate方法获得animation对象,animation对象支持动画属性、动画方法和动画事件,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div id="content" class="box" onclick="Show"></div> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; width: 100%; } .box{ width: 200px; height: 200px; background-color: #ff0000; margin-top: 30px; } /*xxx.js*/ export default { data: { animation: '', }, onInit() { }, onShow() { var options={ duration: 1500, }; var frames=[ { width:200,height:200, }, { width:300,height:300, } ]; this.animation=this.$element('content').animate(frames, options); //获取动画对象 }, Show() { this.animation.play(); } } 使用animate方法时必须传入Keyframes和Options参数。多次调用animate方法时,采用replace策略,即最后一次调用时传入的参数生效。 2) 设置动画参数 在获取动画对象后,通过设置参数Keyframes设置动画在组件上的样式,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div id="content" class="box" onclick="Show"></div> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; width: 100%; } .box{ width: 200px; height: 200px; background-color: #ff0000; margin-top: 30px; } /*xxx.js*/ export default { data: { animation: '', keyframes:{}, options:{} }, onInit() { this.options={ duration: 4000, }; this.keyframes=[ { transform: { translate: '-120px -0px', scale: 1, rotate: 0 }, transformOrigin: '100px 100px', offset: 0.0, width: 200, height: 200 }, { transform: { translate: '120px 0px', scale: 1.5, rotate: 90 }, transformOrigin: '100px 100px', offset: 1.0, width: 300, height: 300 } ]; }, Show() { this.animation=this.$element('content').animate(this.keyframes, this.options); this.animation.play(); } } translate、scale和totate的先后顺序会影响动画效果。transformOrigin只对scale和totate起作用。在获取动画对象后,通过设置参数Options设置动画的属性,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div id="content" class="box" onclick="Show"></div> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; width: 100%; } .box{ width: 200px; height: 200px; background-color: #ff0000; margin-top: 30px; } /*xxx.js*/ export default { data: { animation: '', }, onInit() { }, onShow() { var options={ duration: 1500, easing: 'ease-in', delay: 5, iterations: 2, direction: 'normal', }; var frames=[ { transform: { translate: '-150px -0px' } }, { transform: { translate: '150px 0px' } } ]; this.animation=this.$element('content').animate(frames, options); }, Show() { this.animation.play(); } } direction: 指定动画的播放模式。normal: 动画正向循环播放。reverse: 动画反向循环播放。alternate: 动画交替循环播放,奇数次正向播放,偶数次反向播放。alternatereverse: 动画反向交替循环播放,奇数次反向播放,偶数次正向播放。 3) 添加事件和调用方法 animation对象支持动画事件和动画方法。可以通过添加开始和取消事件,调用播放、暂停、倒放和结束方法实现预期动画,相关代码请扫描二维码获取。 2. 插值器动画 通过设置插值器实现动画效果,从API Version 6 开始支持。 (1) 创建动画对象。通过createAnimator创建一个动画对象,通过设置参数options设置动画的属性,相关代码如下。 <!-- xxx.hml --> <div class="container"> <div style="width: 300px;height: 300px;margin-top: 100px;background: linear-gradient(pink, purple);transform: translate({{translateVal}});"> </div> <div class="row"> <button type="capsule" value="play" onclick="playAnimation"></button> </div> </div> /*xxx.css*/ .container { flex-direction: column; align-items: center; justify-content: center; } button{ width: 200px; } .row{ width: 65%; height: 100px; align-items: center; justify-content: space-between; margin-top: 50px; margin-left: 260px; } /*xxx.js*/ import animator from '@ohos.animator'; export default { data: { translateVal: 0, animation: null }, onInit() {}, onShow(){ var options={ duration: 3000, easing:"friction", delay:"1000", fill: 'forwards', direction:'alternate', iterations: 2, begin: 0, end: 180 }; //设置参数 this.animation=animator.createAnimator(options)//创建动画 }, playAnimation() { var _this=this; this.animation.onframe=function(value) { _this.translateVal= value }; this.animation.play(); } } 使用createAnimator创建动画对象时必须传入options参数。begin为插值起点,不设置时默认为0; end为插值终点,不设置时默认为1。 (2) 添加动画事件和调用接口。animator支持事件和接口,可以通过添加frame、cancel、repeat、finish事件和调用update、play、pause、cancel、reverse、finish接口自定义动画效果,相关代码请扫描二维码获取。 3. 动画帧 本部分包括请求动画帧和取消动画帧。 1) 请求动画帧 通过requestAnimationFrame函数逐帧回调,在调用该函数时传入一个回调函数。 runframe在调用requestAnimationFrame时传入带有timestamp参数的回调函数step,将step中的timestamp赋予起始的startTime。当timestamp与startTime的差值小于规定的时间时将再次调用requestAnimationFrame,最终动画将会停止,相关代码如下。 <!-- xxx.hml --> <div class="container"> <tabs onchange="changecontent"> <tab-content> <div class="container"> <stack style="width: 300px;height: 300px;margin-top: 100px;margin-bottom: 100px;"> <canvas id="mycanvas" style="width: 100%;height: 100%;background-color: coral;"> </canvas> <div style="width: 50px;height: 50px;border-radius: 25px;background-color: indigo;position: absolute;left: {{left}};top: {{top}};"> </div> </stack> <button type="capsule" value="play" onclick="runframe"></button> </div> </tab-content> </tabs> </div> /*xxx.css*/ .container { flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; } button{ width: 300px; } /*xxx.js*/ export default { data: { timer: null, left: 0, top: 0, flag: true, animation: null, startTime: 0, }, onShow() { var test=this.$element("mycanvas"); var ctx=test.getContext("2d"); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(300, 300); ctx.lineWidth=5; ctx.strokeStyle="red"; ctx.stroke(); }, runframe() { this.left=0; this.top=0; this.flag=true; this.animation=requestAnimationFrame(this.step); }, step(timestamp) { if (this.flag) { this.left += 5; this.top += 5; if (this.startTime == 0) { this.startTime=timestamp; } var elapsed=timestamp - this.startTime; if (elapsed < 500) { console.log('callback step timestamp: ' + timestamp); this.animation=requestAnimationFrame(this.step); } } else { this.left -= 5; this.top -= 5; this.animation=requestAnimationFrame(this.step); } if (this.left == 250 || this.left == 0) { this.flag=!this.flag } }, onDestroy() { cancelAnimationFrame(this.animation); } } requestAnimationFrame函数调用回调函数时,在第一个参数位置传入timestamp时间戳,表示requestAnimationFrame开始执行回调函数的时刻。 2) 取消动画帧 通过cancelAnimationFrame函数取消逐帧回调,在调用cancelAnimationFrame函数时取消requestAnimationFrame函数的请求,相关代码如下。 <!-- xxx.hml --> <div class="container"> <tabs onchange="changecontent"> <tab-content> <div class="container"> <stack style="width: 300px;height: 300px;margin-top: 100px;margin-bottom: 100px;"> <canvas id="mycanvas" style="width: 100%;height: 100%;background-color: coral;"> </canvas> <div style="width: 50px;height: 50px;border-radius: 25px;background-color: indigo;position: absolute;left: {{left}};top: {{top}};"> </div> </stack> <button type="capsule" value="play" onclick="runframe"></button> </div> </tab-content> </tabs> </div> /* xxx.css */ .container { flex-direction: column; justify-content: center; align-items: center; width: 100%; height: 100%; } button{ width: 300px; } /*xxx.js*/ export default { data: { timer: null, left: 0, top: 0, flag: true, animation: null }, onShow() { var test=this.$element("mycanvas"); var ctx=test.getContext("2d"); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(300, 300); ctx.lineWidth=5; ctx.strokeStyle="red"; ctx.stroke(); }, runframe() { this.left=0; this.top=0; this.flag=true; this.animation=requestAnimationFrame(this.step); }, step(timestamp) { if (this.flag) { this.left += 5; this.top += 5; this.animation=requestAnimationFrame(this.step); } else { this.left -= 5; this.top -= 5; this.animation=requestAnimationFrame(this.step); } if (this.left == 250 || this.left == 0) { this.flag=!this.flag } }, onDestroy() { cancelAnimationFrame(this.animation); } } 在调用该函数时需传入一个具有标识ID的参数。 视频讲解 5.6自定义组件 使用基于JS扩展的类Web开发范式的方舟开发框架支持自定义组件,用户可根据业务需求将已有的组件进行扩展,增加自定义的私有属性和事件,封装成新的组件,方便在工程中多次调用,提高页面布局代码的可读性,具体封装方法如下。 1. 构建自定义组件 相关代码如下。 <!-- comp.hml --> <div class="item"> <text class="title-style">{{title}}</text> <text class="text-style" onclick="childClicked" focusable="true">单击这里查看隐藏文本</text> <text class="text-style" if="{{showObj}}">hello world</text> </div> /*comp.css*/ .item { width: 700px; flex-direction: column; height: 300px; align-items: center; margin-top: 100px; } .text-style { width: 100%; text-align: center; font-weight: 500; font-family: Courier; font-size: 36px; } .title-style { font-weight: 500; font-family: Courier; font-size: 50px; color: #483d8b; } //comp.js export default { props: { title: { default: 'title', }, showObject: {}, }, data() { return { showObj: this.showObject, }; }, childClicked () { this.$emit('eventType1', {text: '收到子组件参数'}); this.showObj=!this.showObj; }, } 2. 引入自定义组件 相关代码如下。 <!-- xxx.hml --> <element name='comp' src='../../common/component/comp.hml'></element> <div class="container"> <text>父组件:{{text}}</text> <comp title="自定义组件" show-object="{{isShow}}" @event-type1="textClicked"></comp> </div> /*xxx.css*/ .container { background-color: #f8f8ff; flex: 1; flex-direction: column; align-content: center; } //xxx.js export default { data: { text: '开始', isShow: false, }, textClicked (e) { this.text=e.detail.text; }, } 本示例中父组件通过添加自定义属性向子组件传递了名称为title的参数,子组件在props中接收。同时子组件也通过事件绑定向上传递了参数text,接收时通过e.detail获取。如绑定子组件事件,父组件事件命名必须遵循事件绑定规则,自定义组件效果如图510所示。 图510自定义组件效果 视频讲解 5.7JS FA调用PA 基于JS扩展的类Web开发范式的方舟开发框架,提供了JS FA(Feature Ability)调用Java PA(Particle Ability)的机制,该机制提供了一种通道传递方法,调用、处理数据返回,上报订阅事件。 当前提供Ability和Internal Ability两种方式,开发者可以根据业务场景选择合适的调用方式进行开发。 Ability: 拥有独立的Ability生命周期,FA使用远端进程通信拉起并请求PA服务,适用于基本服务供多FA调用或者服务在后台独立运行的场景。 Internal Ability: 与FA共进程,采用内部函数调用的方式和FA进行通信,适用于对服务响应时延要求较高的场景,该方式下PA不支持其他FA访问调用。 对于Internal Ability调用方式的开发,可以使用js2javacodegen工具自动生成代码,提高开发效率。 JS端与Java端通过bundleName和abilityName进行关联。在系统收到JS调用请求后,JS接口中设置的参数选择对应的处理方式。开发者在onRemoteRequest()中实现PA提供的业务逻辑。 1. FA调用PA接口 本部分包括FA端和PA端提供的接口。 1) FA端提供以下三个JS接口 FeatureAbility.callAbility(OBJECT): 调用PA。 FeatureAbility.subscribeAbilityEvent(OBJECT,Function): 订阅PA。 FeatureAbility.unsubscribeAbilityEvent(OBJECT): 取消订阅PA。 2) PA端提供以下两类接口 IRemoteObject.onRemoteRequest(int,MessageParcel,MessageParcel,MessageOption): Ability调用方式,FA使用远端进程通信拉起并请求PA服务。 AceInternalAbility.AceInternalAbilityHandler.onRemoteRequest(int,MessageParcel,MessageParcel,MessageOption): Internal Ability调用方式,采用内部函数调用的方式和FA进行通信。 2. FA调用PA常见问题 callAbility返回报错: Internal ability not register。返回该错误说明JS接口调用请求未在系统中找到对应的InternalAbilityHandler进行处理,因此需要检查以下几点是否正确执行。 (1) 在AceAbility继承类中对AceInternalAbility继承类执行了register方法。 (2) JS侧填写的bundleName和abilityName与AceInternalAbility继承类构造函数中填写的名称保持相同,大小写敏感。 (3) 检查JS端填写的abilityType(0: Ability; 1: Internal Ability),确保没有将AbilityType缺省或误填写为Ability方式。 Ability和Internal Ability是两种不同的FA调用PA的方式。Ability和InternalAbility差异项如表52所示,避免开发时将两者混淆使用。 表52Ability和InternalAbility差异项 差异项 Ability InternalAbility JS端(abilityType) 0 1 是否需要在config.json的abilities中为PA添加声明 需要(有独立的生命周期) 不需要(和FA共生命周期) 是否需要在FA中注册 不需要 需要 继承的类 ohos.aafwk.ability.Ability ohos.ace.ability.AceInternalAbility 是否允许被其他FA访问调用 是 否 FeatureAbility.callAbility中syncOption参数说明如下。 JS FA侧返回的结果都是Promise对象,因此无论该参数取何值,都采用异步方式等待PA侧响应。对于JAVA PA侧,在Internal Ability方式下收到FA的请求后,根据该参数的取值选择通过同步的方式获取结果后返回,或者异步执行PA逻辑,获取结果后使用remoteObject.sendRequest的方式将结果返回FA。 使用await方式调用时IDE编译报错,需引入babelruntime/regenerator。 3. 示例参考 JS端调用FeatureAbility接口,传入两个Number参数,Java端接收后返回两个数的和。JS FA应用的JS模块(entry/src/main)典型开发目录结构如图511所示。 图511JS模块典型开发目录结构 1) FA JavaScript端 使用Internal Ability方式时,需要将对应的action.abilityType值改为ABILITY_TYPE_INTERNAL,相关代码如下。 //abilityType: 0-Ability; 1-Internal Ability const ABILITY_TYPE_EXTERNAL=0; const ABILITY_TYPE_INTERNAL=1; //syncOption(Optional, default sync): 0-Sync; 1-Async const ACTION_SYNC=0; const ACTION_ASYNC=1; const ACTION_MESSAGE_CODE_PLUS=1001; export default { plus: async function() { var actionData={}; actionData.firstNum=1024; actionData.secondNum=2048; var action={}; action.bundleName='com.example.hiaceservice'; action.abilityName='com.example.hiaceservice.ComputeServiceAbility'; action.messageCode=ACTION_MESSAGE_CODE_PLUS; action.data=actionData; action.abilityType=ABILITY_TYPE_EXTERNAL; action.syncOption=ACTION_SYNC; var result=await FeatureAbility.callAbility(action); var ret=JSON.parse(result); if (ret.code == 0) { console.info('plus result is:'+JSON.stringify(ret.abilityResult)); } else { console.error('plus error code:' + JSON.stringify(ret.code)); } } } 2) PA端(Ability方式) 功能代码实现: 在Java目录下新建一个Service Ability,文件命名为ComputeServiceAbility.java,相关代码如下。 package com.example.hiaceservice; //ohos相关接口包 import ohos.aafwk.ability.Ability; import ohos.aafwk.content.Intent; import ohos.hiviewdfx.HiLog; import ohos.hiviewdfx.HiLogLabel; import ohos.rpc.IRemoteBroker; import ohos.rpc.IRemoteObject; import ohos.rpc.RemoteObject; import ohos.rpc.MessageParcel; import ohos.rpc.MessageOption; import ohos.utils.zson.ZSONObject; import java.util.HashMap; import java.util.Map; public class ComputeServiceAbility extends Ability { //定义日志标签 private static final HiLogLabel LABEL=new HiLogLabel(HiLog.LOG_APP, 0, "MY_TAG"); private MyRemote remote=new MyRemote(); //FA在请求PA服务时会调用Ability.connectAbility连接PA,连接成功后,需要在onConnect返 //回一个remote对象,供FA向PA发送消息 @Override protected IRemoteObject onConnect(Intent intent) { super.onConnect(intent); return remote.asObject(); } class MyRemote extends RemoteObject implements IRemoteBroker { private static final int SUCCESS=0; private static final int ERROR=1; private static final int PLUS=1001; MyRemote() { super("MyService_MyRemote"); } @Override public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) { switch (code) { case PLUS: { String dataStr=data.readString(); RequestParam param=new RequestParam(); try { param=ZSONObject.stringToClass(dataStr, RequestParam.class); } catch (RuntimeException e) { HiLog.error(LABEL, "convert failed."); } //返回结果当前仅支持String,对于复杂结构可以序列化为Zson字符串上报 Map<String, Object> result=new HashMap<String, Object>(); result.put("code", SUCCESS); result.put("abilityResult", param.getFirstNum() + param.getSecondNum()); reply.writeString(ZSONObject.toZSONString(result)); break; } default: { Map<String, Object> result=new HashMap<String, Object>(); result.put("abilityError", ERROR); reply.writeString(ZSONObject.toZSONString(result)); return false; } } return true; } @Override public IRemoteObject asObject() { return this; } } } 请求参数代码如下。 RequestParam.java public class RequestParam { private int firstNum; private int secondNum; public int getFirstNum() { return firstNum; } public void setFirstNum(int firstNum) { this.firstNum=firstNum; } public int getSecondNum() { return secondNum; } public void setSecondNum(int secondNum) { this.secondNum=secondNum; } } 3) PA端(Internal Ability方式) 功能代码实现可以使用js2javacodegen工具自动生成: 在Java目录下新建一个Service Ability,文件命名为ComputeInternalAbility.java,相关代码请扫描二维码获取。 视频讲解 5.8使用工具自动生成JS FA调用PA代码 JS FA调用PA是基于JS扩展的类Web开发范式的方舟开发框架所提供的一种跨语言能力调用的机制,用于建立JS能力与Java能力之间传递方法调用、处理数据返回及订阅事件上报的通道。开发者可以使用FA调用PA机制进行应用开发,但直接使用需要手动撰写大量模板代码,且模板代码可能与业务代码相互耦合,使代码可维护性和可读性较差。 为提升开发效率,快速完成FA调用PA应用,可以在DevEco Studio环境中借助js2javacodegen工具自动生成JS FA调用PA代码(目前仅支持InternalAbility调用方式)。开发者只需添加简单的配置与标注即可利用该工具完成大部分FA调用PA模板代码的编写,同时也有效地将业务代码与模板代码相互分离。 1. js2javacodegen工具简介 js2javacodegen是工具链提供的自动生成JS FA调用PA代码的辅助开发工具。它可以根据用户源码生成FA调用PA所需的、与用户编写的业务代码相互分离的模板代码。 js2javacodegen工具所支持的FA调用PA实现方式为InternalAbility类型,目前尚不支持Ability类型。开发者完成设置后只需编写包含实际业务逻辑的InternalAbility类和需要注册的Ability类,并在InternalAbility类中加上对应注解,js2javacodegen即可在编译过程中完成FA调用PA通道的建立。之后,只需在JS侧调用由js2javacodegen工具生成的JS接口即可调用Java一侧的能力。 js2javacodegen工具所生成的模板代码包含Java代码和JS代码。其中,Java代码会被直接编译成字节码文件,并且对应Ability类中会被自动添加注册与反注册语句,开发者无须关注; 而JS代码则需要用户手动调用,因此需要在编译前设置好JS代码的输出路径。 注解使用说明如下: js2javacodegen工具通过注解获取信息并生成开发者所需的代码。因此,用户如果使用该工具辅助开发,则需要了解以下三种用法。 1) @InternalAbility注解 @InternalAbility注解为类注解,用于InternalAbility、包含实际业务代码的类(简称InternalAbility类),只支持文件中public的顶层类,不支持接口类和注解类,包含一个参数registerTo,值为需要注册到Ability类的全名。示例如下,Service类是一个InternalAbility类,注册到位于com.example包中的、名为Ability的Ability类。 @InternalAbility(registerTo="com.example.Ability") public class Service{} 2) @ExportIgnore注解 @ExportIgnore注解为方法注解,用于InternalAbility类中的某些方法,表示该方法不暴露给JS侧调用,仅对public方法有效。示例如下,service方法不会被暴露给JS侧。 @ExportIgnore public int service(int input) { return input; } 3) @ContextInject注解 @ContextInject注解用于AbilityContext上的注解。该类由HarmonyOS的Java API提供,可通过它获取API中提供的信息。 可以借助abilityContext对象获取API中提供的信息,示例如下。 @ContextInject AbilityContext abilityContext; 2. 新建工程 体验工具生成模板代码的功能,可使用DevEco Studio新建一个包含JS前端的简单手机项目,并用其开发一个简单的FA调用PA应用。 3. 工具开关与编译设置 快速验证功能可选择修改entry模块的build.gradle,通过entry模块进行验证。 编译参数位于ohos→defaultConfig中,只需添加如下设置即可。开发者需在此处设置JS模板代码生成路径,即jsOutputDir对应的值。 //在文件头部定义JS模板代码生成路径 def jsOutputDir=project.file("src/main/js/default/generated").toString() //在ohos→defaultConfig中设置JS模板代码生成路径 javaCompileOptions { annotationProcessorOptions { arguments =["jsOutputDir": jsOutputDir] //JS模板代码生成赋值 } } 工具开关位于ohos中,只需添加如下设置即可。值设为true则启用工具,值设为false则不进行配置,不启用工具。 compileOptions { f2pautogenEnabled true //此处为启用js2java-codegen工具的开关 } 4. Java侧代码编写 模板代码的生成需要提供用于FA调用的PA,因此需要编写InternalAbility类,在类上加@InternalAbility注解,registerTo参数设为将要注册到的Ability类的全称。Ability类可使用项目中已有的MainAbility类,或创建新的Ability类。 注意,InternalAbility类中需要暴露给FA调用的方法只能是public类型的非静态非void方法,若不是则不会被暴露。 一个简单的InternalAbility类实现如下,文件名为Service.java,与MainAbility类同包,用注解注册到MainAbility类。类中包含一个ADD方法作为暴露给JS FA调用的能力,实现两数相加的功能,入参为两个int参数,返回值为两数之和,示例如下。 package com.example.myapplication; import ohos.annotation.f2pautogen.InternalAbility; @InternalAbility(registerTo="com.example.myapplication.MainAbility") //此处registerTo的参数为项目中MainAbility类的全称 public class Service { public int add(int num1, int num2) { return num1 + num2; } } 5. 编译 js2javacodegen工具在编译过程中会自动被调用、生成模板代码并完成整个通道建立的过程。 单击菜单栏中的Build→Build HAP(s)/APP(s)→Build HAP(s),即可完成项目编译,同时js2javacodegen工具会在编译过程中完成FA调用PA通道的建立。 编译过程会生成Java和JS的模板代码。其中,JS的模板代码位于编译设置中的路径,名称与InternalAbility类相对应; 而Java的模板代码位于entry→build→generated→source→annotation→debug→InternalAbility类同名包→InternalAbility类名+Stub.java,该类的调用语句会被注入MainAbility类的字节码当中,生成的模板如图512和图513所示。 图512Java模板代码示例 图513JS模板代码示例 6. JS侧代码编写 为了简易直观地检验工具生成代码的可用性,开发者可以通过修改entry→src→main→js→default→pages→index→index.js调用Java侧的能力,并在前端页面展示效果。 可通过import方式引入JS侧FA接口,例如import Service from '../../generated/Service.js'; (from后的值需要与编译设置中的路径进行统一,生成的JS代码文件名及类名与InternalAbility类名相同)。 一个简单的index.js页面实现如下,调用JS侧接口,传入1和10两个参数,并把返回的结果打印在title中,这样只要运行该应用就可以验证FA调用PA是否成功。 import Service from '../../generated/Service.js'; //此处FA路径和类名对应之前的jsOutput路径及InternalAbility的名称 export default { data: { title: "Result:" }, onInit() { const echo=new Service(); //此处新建FA实例 echo.add(1,10) .then((data) => { this.title += data["abilityResult"]; //获取运算结果,并加到title之后 }); } } 为了方便结果展示,这里对同目录下的index.hml也做一些修改,使页面中只显示title的内容。 <div class="container"> <text class="title"> {{ title }} </text> </div> 7. 结果验证 启动手机模拟器,成功后运行,看到显示界面则说明js2javacodegen工具生成了有效的模板代码,建立了FA调用PA的通道。