[文章]完整服务卡片项目开发,为Bilibili添加服务卡片

阅读量0
0
0


BilibiliCards
项目预览视频播放地址
介绍
这是一款纯鸿蒙版的哔哩哔哩服务卡片应用。

6月2日鸿蒙发布,今年的六月已经被鸿蒙刷屏了。从安卓到鸿蒙,最直观的变化应该就是服务卡片了。我也是在学习鸿蒙的同时,实际体验一下服务卡片的开发。给大家看看最终的效果。
1演示640×295帧率10.gif
2演示640×295帧率10.gif
3演示640×295帧率10.gif

接下来分享下我的制作过程,我使用的开发环境是IDE:DevEco Studio 2.1 ReleaseSDK:API Version 5
软件安装和项目建立的部分就跳过了,相信大家都比较熟悉了。直奔主题服务卡片的制作。

一、服务卡片设计
首先要先了解服务卡片,都有哪些尺寸,支持哪些组件,使用什么语言。然后规划好要实现哪些功能。
1.尺寸规格
服务卡片有4种尺寸,分别是微卡片、小卡片、中卡片、大卡片。官方提供了4种基础模板,12种高级模板。可以选择。基础模板如下图
服务卡片布局.png

2.功能设计
服务卡片设计的初衷就是信息显示、服务直达。依照这个原则,我找了几个Bilibili中我比较常用的功能,来制作服务卡片,比如追番列表。
App功能截取_2.gif

3.开发语言
看下表就一目了然了,就是推荐JS。表格来源:
场景Java卡片JS卡片支持的版本
实时刷新(类似时钟)Java使用ComponentProvider做实时刷新代价比较大JS可以做到端侧刷新,但是需要定制化组件HarmonyOS 2.0及以上
开发方式Java UI在卡片提供方需要同时对数据和组件进行处理,生成ComponentProvider远端渲染JS卡片在使用方加载渲染,提供方只要处理数据、组件和逻辑分离HarmonyOS 2.0及以上
组件支持Text、Image、DirectionalLayout、PositionLayout、DependentLayoutdiv、list、list-item、swiper、stack、image、text、span、progress、button(定制:chart 、clock、calendar)HarmonyOS 2.0及以上
卡片内动效不支持暂不开放HarmonyOS 2.0及以上
阴影模糊不支持支持HarmonyOS 2.0及以上
动态适应布局不支持支持HarmonyOS 2.0及以上
自定义卡片跳转页面不支持支持HarmonyOS 2.0及以上

二、界面实现
本着学习的目的,卡片界面就不使用模板了。不过我们还是要通过IDE>>File>>New>>Service Widget来添加服务卡片,这样添加IDE会自动添加配置和管理相关文件。然后服务卡片的界面重新编写。服务卡片常用的的容器组件有div、list、stack、swiper等。我使用了4种尺寸的卡片,并尽可能的使用到所有的容器组件。
image-20210617100003900.png

div:基础容器组件
就是用来划分区域的。比较常用。比如追番服务卡片。效果如图,代码如下
image-20210703173854044.png

  1. <div class="div_root"  ><!--在服务卡片设置一个 根div 横向布局-->

  2.     <div class="div_container"><!--在根div 横向放置4个div,每个div内部从上往下排列-->
  3.             <image class="item_image" src="{{ src1 }}"></image>
  4.             <text class="item_title">{{ itemTitle1 }}</text>
  5.             <text class="item_content">{{ itemContent1 }}</text>
  6.     </div>

  7.     <div class="div_container"><!--第二列-->
  8.         <image class="item_image" src="{{ src2 }}"></image>
  9.         <text class="item_title">{{ itemTitle2 }}</text>
  10.         <text class="item_content">{{ itemContent2 }}</text>
  11.     </div>

  12.     <div class="div_container"><!--第三列-->
  13.         <image class="item_image" src="{{ src3 }}"></image>
  14.         <text class="item_title">{{ itemTitle3 }}</text>
  15.         <text class="item_content">{{ itemContent3 }}</text>
  16.     </div>

  17.     <div class="div_container"><!--第四列-->
  18.         <image class="item_image" src="{{ src4 }}"></image>
  19.         <text class="item_title">{{ itemTitle4 }}</text>
  20.         <text class="item_content">{{ itemContent4 }}</text>
  21.     </div>

  22. </div>
复制代码
  1. .div_root {
  2.     flex-direction: row;        /*flex容器主轴方向,row:水平方向从左到右。*/
  3.     justify-content: center;    /*flex容器当前行的主轴对齐格式,center:项目位于容器的中心。*/
  4.     margin:6px;                 /*外边距属性:只有一个值时,这个值会被指定给全部的四个边。*/
  5.     border-radius: 10px;                /*设置元素的外边框圆角半径。*/
  6. }

  7. .div_container {
  8.     flex-direction: column;         /*flex容器主轴方向,column:垂直方向从上到下。*/
  9.     justify-content: flex-start;    /*flex容器当前行的主轴对齐格式,flex-start:项目位于容器的开头。*/
  10.     margin:6px;
  11. }

  12. .item_image {
  13.     height: 60%;                                        /*卡片在不同设备,尺寸会发生变化,所以最好使用百分比进行标注。*/
  14.     border-radius: 10px;
  15.     background-color: #F1F3F5;      /*设置背景颜色。*/
  16. }
  17. @media (dark-mode: true) {          /*当前系统为深色模式时,使用这里的配置,如果没有颜色设置,可以不设置*/
  18.     .item_image {
  19.         height: 60%;
  20.         border-radius: 10px;
  21.         background-color: #202224;
  22.     }
  23. }

  24. .item_title {
  25.     margin-top: 10px;           /*设置上边距。*/
  26.     font-size: 12px;            /*设置文本的尺寸。*/
  27.     font-weight: bold;          /*设置文本的字体粗细。取值[100, 900],默认为400。*/
  28.     max-lines:1;                /*设置文本的最大行数。*/
  29.     text-overflow: ellipsis;    /*根据父容器大小显示,显示不下的文本用省略号代替。需配合max-lines使用。*/
  30.     color: #e5000000;           /*设置文本的颜色。*/
  31. }

  32. .item_content {
  33.     margin-top: 5px;
  34.     font-size: 9px;
  35.     font-weight: bold;
  36.     text-overflow: ellipsis;
  37.     max-lines:1;
  38.     color: #99000000;
  39. }
复制代码
其实这个服务卡片的布局,每一列的内容都是相同的,是应该使用list组件的。
list:列表容器组件
就如上面所说的连续相同的部分,可以使用这个组件,List不但可以显示更多的内容,而且代码更少。效果图如下
2×4追番列表滚动展示.gif

  1. <list class="list">
  2.     <list-item for="{{cards}}" class="list-item">
  3.         <div class="div">
  4.             <image class="item_image" src="{{ $item.pic }}"></image>
  5.             <text class="item_name">{{ $item.name }}</text>
  6.             <text class="item_title">{{ $item.title }}</text>
  7.         </div>
  8.     </list-item>
  9. </list>
复制代码
  1. list{
  2.     align-items:center; /*list每一列交叉轴上的对齐格式:元素在交叉轴居中*/
  3. }
  4. .list-item{
  5.     border-radius: 15px;
  6.     background-color: #f2f2f2;
  7.     margin-bottom: 5px;
  8. }
  9. .div{
  10.     flex-direction: column;
  11. }

  12. .item_image {
  13.     border-top-right-radius: 15px;
  14.     border-top-left-radius: 15px;
  15. }

  16. .item_name {
  17.     margin:5px 8px 0px;
  18.     font-size: 12px;
  19.     color: #262626;
  20. }

  21. .item_title{
  22.     margin:3px 8px 8px;
  23.     font-size: 10px;
  24.     color: #AAAAAA;
  25.     max-lines: 2;
  26.     text-overflow: ellipsis;    /* 省略号 */
  27. }
复制代码
stack:堆叠容器组件
简单来说就是可以在一张图片上堆叠显示另一张图片,例如下图蓝框的图片覆盖在红框图片的上面。
image-20210703204427507.png
  1. <stack class="stack-parent">
  2.     <image src="{{src}}" class="image_src"></image>
  3.     <image src="{{vip}}" class="image_vip"></image>
  4. </stack>
复制代码
swiper:滑动容器组件
正常情况下swiper是可以实现上下、左右滑动操作的。但是放置在桌面上的服务卡片,在左右滑动操作的时候,会使系统分不清楚用户是要左右滑动屏幕,还是左右滑动卡片。所以目前服务卡片的swiper容器是不支持手势滑动切换子组件的。下图是通过点击图片侧面的控制条实现上下滑动的。但是我个人觉得上下滑动其实还是挺好用的,毕竟在list组件上是可以上下滑动的,只可惜目前还不支持。
滑动容器展示.gif
  1. <swiper class="card_root_layout" indicator="true" autoplay="true" interval="10" loop="true" vertical="true">
  2.     <stack class="stack-parent">
  3.         <image src="{{src0}}" class="item_image"></image>
  4.         <text class="item_title">{{title0}}</text>
  5.     </stack>
  6.     <stack class="stack-parent">
  7.         <image src="{{src1}}" class="item_image">></image>
  8.         <text class="item_title">{{title1}}</text>
  9.     </stack>
  10.     <stack class="stack-parent">
  11.         <image src="{{src2}}" class="item_image">></image>
  12.         <text class="item_title">{{title2}}</text>
  13.     </stack>
  14.     <stack class="stack-parent">
  15.         <image src="{{src3}}" class="item_image">></image>
  16.         <text class="item_title">{{title3}}</text>
  17.     </stack>
  18. </swiper>
复制代码
总结:服务卡片的设计比较简单,零基础也没关系,官方还贴心的准备了模板。只要挑选模板,设置变量也能快速构建。

三、API数据请求
卡片设计好之后,就需要通过Bilibili的API来获取数据了。主要就是给权限添加依赖,然后发送网络请求,通过API获取JSON的返回值,然后解析JSON得到我们需要的数据。
1.添加联网权限
要在config.json配置文件的module中添加:"reqPermissions": [{"name":"ohos.permission.INTERNET"}],
  1. {
  2.   ... ...
  3.   "module": {
  4.           ... ...
  5.           "reqPermissions": [{"name":"ohos.permission.INTERNET"}]
  6.   }
  7. }
复制代码
2.添加依赖包
找到entry/build.gradle文件,在dependencies下添加
  1. xxxxxxxxxx dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar', '*.har'])    testImplementation 'junit:junit:4.13'    ohosTestImplementation 'com.huawei.ohos.testkit:runner:1.0.0.100'​    // ZZRHttp 可以单独一个进程进行http请求    implementation 'com.zzrv5.zzrhttp:ZZRHttp:1.0.1'      
复制代码
3.http请求
以获取粉丝数为例。如果在浏览器中输入 https://api.bilibili.com/x/relation/stat?vmid=383565952 (其中vmid:是要查询的用户ID)
image-20210703231027298.png

follower的值就是粉丝数。
网络访问我们可以使用HttpURLConnection,或者okhttp等依赖包,但是需要开启子线程、处理异常等操作,所以这里使用的是ZZR老师封装好的ZZRHttp
代码实现:
  1. //获取Bilibili粉丝数,这里就要用到第二步我们添加的ZZRHttp
  2. String url = "https://api.bilibili.com/x/relation/stat?vmid=383565952";
  3. ZZRHttp.get(url, new ZZRCallBack.CallBackString() {
  4.     @Override
  5.     public void onFailure(int i, String s) {
  6.         HiLog.info(TAG, "API返回失败");
  7.     }
  8.     @Override
  9.     public void onResponse(String s) {
  10.         HiLog.info(TAG, "API返回成功");
  11.         // 如果返回成功,返回的结果就会保存在 String s 中。
  12.         // s = {"code":0,"message":"0","ttl":1,"data":{"mid":383565952,"following":70,"whisper":0,"black":0,"follower":5384}}
  13.     }
  14. });
复制代码
4.解析JSON
得到的是JSON格式的返回值,要得到follower的值,还需要对JSON进行数据解析。
先按照JSON的内容,生成JAVA类。代码如下。可以自己写,也可以百度搜 ”JSON生成Java实体类“,可直接生成。
  1. public class BilibiliFollower {
  2.     public static class Data{
  3.         private int follower;
  4.         public int getFollower() {
  5.             return follower;
  6.         }
  7.         public void setFollower(int follower) {
  8.             this.follower = follower;
  9.         }
  10.     }
  11.     private BilibiliFollower.Data data;
  12.     public BilibiliFollower.Data getData() {
  13.         return data;
  14.     }
  15.     public void setData(BilibiliFollower.Data data) {
  16.         this.data = data;
  17.     }
  18. }
复制代码
  1. //解析JSON,使用第二步我们添加的fastjson包
  2. try {
  3.     //1.调用fastjson解析,结果保存在JSON对应的类
  4.     BilibiliFollower bilibiliFollower = JSON.parseObject(s,BilibiliFollower.class);
  5.     //2.get方法获取解析内容
  6.     BilibiliFollower.Data data= bilibiliFollower.getData();
  7.     System.out.println("解析成功"+data.getFollower());

  8. } catch (Exception e) {
  9.     HiLog.info(TAG, "解析失败");
  10. }
复制代码
总结:一定要添加联网权限不然是获取不到数据的。添加了2个依赖包,可以很方便的提取数据。获取其他的卡片数据的方式同理,不过代码比较多,就不一一展示了,感兴趣可以下载全量代码看。
四、数据更新要想将数据更新到服务卡片,得先了解服务卡片的运作机制。如果是通过IDE>>File>>New>>Service Widget添加的服务卡片,那么在MainAbility中会添加卡片的生命周期回调方法,参考下面的代码
  1. public class MainAbility extends Ability {
  2.    
  3.     ... ...

  4.     protected ProviderFormInfo onCreateForm(Intent intent) {...}//在服务卡片上右击>>服务卡片(或上滑)时,通知接口

  5.     protected void onUpdateForm(long formId) {...}//在服务卡片请求更新,定时更新时,通知接口

  6.     protected void onDeleteForm(long formId) {..}//在服务卡片被删除时,通知接口

  7.     protected void onTriggerFormEvent(long formId, String message) {...}//JS服务卡片click时,通知接口
  8. }
复制代码
1.定时更新
按照上述分析,我们只需要在config.json中开启服务卡片的周期性更新,在onUpdateForm(long formId)方法下执行数据获取更新。
config.json文件“abilities”的forms模块配置细节如下
  1. "forms": [
  2.   {
  3.       "jsComponentName": "widget2",
  4.       "isDefault": true,
  5.       "scheduledUpdateTime": "10:30",//定点刷新的时刻,采用24小时制,精确到分钟。"updateDuration": 0时,才会生效。
  6.       "defaultDimension": "1*2",
  7.       "name": "widget2",
  8.       "description": "This is a service widget",
  9.       "colorMode": "auto",
  10.       "type": "JS",
  11.       "supportDimensions": [
  12.           "1*2"
  13.       ],
  14.       "updateEnabled": true,        //表示卡片是否支持周期性刷新
  15.       "updateDuration": 1                //卡片定时刷新的更新周期,1为30分钟,2为60分钟,N为30*N分钟
  16.   }
  17. ]
复制代码
这样结合我们在上一步获取API数据,解析JSON,开启服务卡片的周期性更新,就可以在updateFormData()实现服务卡片的数据更新了。截取follower数据更新的部分代码如下
  1. public void updateFormData(long formId, Object... vars) {
  2.     HiLog.info(TAG, "update form data: formId" + formId);

  3.     //这部分用来获取粉丝数
  4.     String url = "https://api.bilibili.com/x/relation/stat?vmid=383565952";
  5.     ZZRHttp.get(url, new ZZRCallBack.CallBackString() {
  6.         @Override
  7.         public void onFailure(int i, String s) {HiLog.info(TAG, "API返回失败");}
  8.         @Override
  9.         public void onResponse(String s) {
  10.             HiLog.info(TAG, "API返回成功");
  11.             try {
  12.                 //1.调用fastjson解析,结果保存在JSON对应的类
  13.                 BilibiliFollower bilibiliFollower = JSON.parseObject(s,BilibiliFollower.class);
  14.                 //2.get方法获取解析内容
  15.                 BilibiliFollower.Data data= bilibiliFollower.getData();
  16.                 System.out.println("解析成功"+data.getFollower());

  17.                 //这部分用来更新卡片信息
  18.                 ZSONObject zsonObject = new ZSONObject(); //1.将要刷新的数据存放在一个ZSONObject实例中
  19.                 zsonObject.put("follower",data.getFollower()); //2.更新数据,data.getFollower()就是在API数据请求中获取的粉丝数。
  20.                 FormBindingData formBindingData = new FormBindingData(zsonObject); //3.将其封装在一个FormBindingData的实例中
  21.                 try {
  22.                     ((MainAbility)context).updateForm(formId,formBindingData); //4.调用MainAbility的方法updateForm(),并将formBindingData作为第二个实参
  23.                 } catch (FormException e) {
  24.                     e.printStackTrace();
  25.                     HiLog.info(TAG, "更新卡片失败");
  26.                 }
  27.             } catch (Exception e) {
  28.                 HiLog.info(TAG, "解析失败");
  29.             }
  30.         }
  31.     });
  32. }
复制代码
2.手动更新
正常来说这样就可以正常更新数据了,但是会有个问题。就是在服务卡片首次创建添加到桌面的时候,在添加完的至少30分钟里,数据是不会更新的。此时如果在index.json中设置初始信息,那么在添加完成的前30分钟数据都是写死在data中的。如果不设置初始信息那么卡片就是空白的。
空白卡片.gif
所以按照前面服务卡片的运作机制的分析,我们还需要在卡片初始化onCreateForm()的时候进行一次更新。这个非常简单用onCreateForm()调用onUpdateForm(formId)即可。

  1. @Override
  2. protected ProviderFormInfo onCreateForm(Intent intent) {
  3.     ... ...

  4.         //初始化时先在线更新一下卡片
  5.     onUpdateForm(formId);

  6.     return formController.bindFormData();
  7. }
复制代码
总结:这里的onUpdateForm(formId)中API的网络请求一定要新开一个子线程,不然会影响页面加载。这也是前面说的用ZZRhttp的原因。不过现在也遇到一个问题,当卡片数量变多时,同时在线更新这么多的卡片会变得非常缓慢,这个问题还有待解决。
五、功能直达
目前服务卡片仅支持click通用事件,事件类型:跳转事件(router)和消息事件(message)。详细说明参考官方文档https://developer.harmonyos.com/cn/docs/documentation/doc-references/js-service-widget-syntax-hml-0000001152828575
1.跳转事件
接下来实现与服务卡片的交互,当点击服务卡片时,会跳转到相应的页面,所以这里使用跳转事件。以番剧更新的卡片为例
追番列表跳转.gif
1.首先我们要先添加一个要跳转的页面。如下图所示添加一个Page Ability,比如:VideoSlice
image-20210701213924644.png

2.新建完成之后会增加VideoSlice和 slice/VideoSliceSlice 两个文件,和base/layout/ability_bilibili_page.xml页面文件

  1. @Override
  2. public void onStart(Intent intent) {
  3.     super.onStart(intent);
  4.     super.setUIContent(ResourceTable.Layout_ability_video);

  5.     Text text = (Text) findComponentById(ResourceTable.Id_text);
  6.     text.setText("页面跳转中");

  7.     // 随机图片数组
  8.     int[] resource = {ResourceTable.Media_36e,ResourceTable.Media_36g,ResourceTable.Media_36h,ResourceTable.Media_38p};
  9.     Component component = findComponentById(ResourceTable.Id_image);
  10.     if (component instanceof Image) {
  11.         Image image = (Image) component;
  12.         image.setPixelMap(resource[(int)(Math.random()*3)]);//随机显示一张图片
  13.     }

  14.     String url = "https://m.bilibili.com";

  15.     String param = intent.getStringParam("params");//从intent中获取 跳转事件定义的params字段的值
  16.     if(param !=null){
  17.         ZSONObject data = ZSONObject.stringToZSON(param);
  18.         url = data.getString("url");
  19.     }

  20.     webview(url);
  21. }
  22. //启动webview
  23. public void webview(String url){
  24.     WebView webView = (WebView) findComponentById(ResourceTable.Id_webview);
  25.     webView.getWebConfig().setJavaScriptPermit(true);  // 如果网页需要使用JavaScript,增加此行;如何使用JavaScript下文有详细介绍
  26.     webView.load(url);
  27. }
复制代码
3.增加webview,将页面默认的Text控件修改为webview
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <DirectionalLayout
  3.     xmlns:ohos="http://schemas.huawei.com/res/ohos"
  4.     ohos:height="match_parent"
  5.     ohos:width="match_parent"
  6.     ohos:alignment="center"
  7.     ohos:orientation="vertical">
  8.     <ohos.agp.components.webengine.WebView
  9.         ohos:id="$+id:webview"
  10.         ohos:height="match_parent"
  11.         ohos:width="match_parent">
  12.     </ohos.agp.components.webengine.WebView>

  13. </DirectionalLayout>
复制代码
4.在index.hml中给要触发的控件上添加onclick,比如:onclick="routerEvent1"

  1. xxxxxxxxxx <div class="div_root"  ><!--在服务卡片设置一个 根div 横向布局-->    <div class="div_container"><!--在根div 横向放置4个div,每个div内部从上往下排列-->            <image class="item_image" src="{{ src1 }}" onclick="routerEvent1"></image>            <text class="item_title">{{ itemTitle1 }}</text>            <text class="item_content">{{ itemContent1 }}</text>    </div>    ... ...</div>
复制代码
5.在index.json中,添加对应的actions,跳转事件要多加一个参数"abilityName",指定要跳转的页面,并且携带参数url。

  1. {
  2.   "data": {
  3.   },
  4.   "actions": {
  5.         "routerEvent1": {
  6.             "action": "router",
  7.             "bundleName": "com.liangzili.servicewidget",
  8.             "abilityName": "com.liangzili.servicewidget.VideoSlice",
  9.             "params": {
  10.                 "url": "{{url1}}"
  11.             }
  12.         },
  13.       "routerEvent2": {
  14.       ... ...   
  15. }
复制代码
2.消息事件
这里使用视频动态服务卡片,做一个消息事件的测试,效果如下图,点击左右边,实现服务卡片的滑动。在小卡片上这样的操作体验不好。所以消息事件中的例子,只是为了测试,并没有加到项目里。
siwper组件.gif
1.在index.hml中给要触发的控件上添加onclick,比如:onclick="sendMessageEvent"
  1. <-- 为了方便测试,直接将onclick添加在左右两侧的div组件上 -->
  2. <div class="div">
  3.     <image class="item_image" src="{{ src0 }}"></image>
  4.     <text class="item_title">{{ itemTitle0 }}</text>
  5.     <text class="item_content">{{ itemContent0 }}</text>
  6. </div>
  7. <div class="div">
  8.     <image class="item_image" src="{{ src1 }}"></image>
  9.     <text class="item_title">{{ itemTitle1 }}</text>
  10.     <text class="item_content">{{ itemContent1 }}</text>
  11. </div>
复制代码
2.在index.json中,添加对应的actions
  1. {
  2.   "data": {
  3.   },
  4.     "actions": {
  5.         "sendMessageEvent0": {
  6.             "action": "message",
  7.             "params": {
  8.                 "p1": "left",
  9.                 "index": "{{index}}"
  10.             }
  11.         },
  12.         "sendMessageEvent1": {
  13.             "action": "message",
  14.             "params": {
  15.                 "p1": "right",
  16.                 "index": "{{index}}"
  17.             }
  18.         }
  19.     }
  20. }
复制代码
3.如果是消息事件(message)当点击带有onclick的控件时,会触发MainAbility下的这个函数

  1. @Override
  2. protected void onTriggerFormEvent(long formId, String message) {
  3.     HiLog.info(TAG, "onTriggerFormEvent: " + message); //params的内容就通过message传递过来
  4.     super.onTriggerFormEvent(formId, message);
  5.     FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
  6.     FormController formController = formControllerManager.getController(formId);//通过formId得到卡片控制器
  7.     formController.onTriggerFormEvent(formId, message);//接着再调用,对应的控制器 WidgetImpl
  8. }
复制代码
4.最后调用卡片控制器 WidgetImpl 中的onTriggerFormEvent()
  1. public void onTriggerFormEvent(long formId, String message) {
  2.     HiLog.info(TAG, "onTriggerFormEvent."+message);
  3.    
  4.     //先获取message中的参数
  5.     ZSONObject data = ZSONObject.stringToZSON(message);
  6.     String p1 = data.getString("p1");
  7.     Integer index = data.getIntValue("index");

  8.     ZSONObject zsonObject = new ZSONObject();         //将要刷新的数据存放在一个ZSONObject实例中
  9.     Integer indexMax = 2;                           //有N个滑块组件就设置N-1
  10.     if(p1.equals("right")){                         //判断点击方向,如果是右侧
  11.         if(index == indexMax){index = -1;}          //实现循环滚动
  12.         index = index+1;
  13.         zsonObject.put("index",index);
  14.     }else {                                         //判断点击方向,如果是左侧
  15.         if(index == 0){index = indexMax+1;}         //实现循环滚动
  16.         index = index-1;
  17.         zsonObject.put("index",index);
  18.     }

  19.     FormBindingData formBindingData = new FormBindingData(zsonObject);
  20.     try {
  21.         ((MainAbility)context).updateForm(formId,formBindingData);
  22.     } catch (FormException e) {
  23.         e.printStackTrace();
  24.         HiLog.info(TAG, "更新卡片失败");
  25.     }
  26. }
复制代码
3.list跳转事件
list组件只能添加一个onclick,而且在点击的同时还需要获取点击的是list列表中的哪一项,这个比较特殊。
  1. <list class="list" else>
  2.     <list-item for="{{list}}" class="list-item">
  3.         <div class="div">
  4.                         ... ...
  5.         </div>
  6.     </list-item>
  7. </list>
复制代码
这个坑折磨了我好久,最终我发现在index.json中,可以使用$item,$idx获取到hml页面list的元素变量和索引。但是在官方文档并没有找到相关的内容,尝试了很久才解决这个问题。之后的部分就和跳转事件一样了,使用Video页面解析url进行播放就可以了。
  1.   "actions": {
  2.     "sendRouteEvent": {
  3.       "action": "router",
  4.       "bundleName": "com.liangzili.demos",
  5.       "abilityName": "com.liangzili.demos.Video",
  6.       "params": {
  7.         "url": "{{$item.short_url}}",
  8.         "index": "{{$idx}}"
  9.       }
  10.     }
  11.   }
复制代码
总结:解决了list的点击事件之后,才发现这个控件真是好用。能用list还是list方便。六、加载页面,保存Cookie
启动之后的页面主要是为了登录账号,因为大部分的API是需要登录之后才可以获取到的。
1.webview加载页面
在base/layout/ability_main.xml中添加webview组件,代码如下
  1. <ohos.agp.components.webengine.WebView
  2.         ohos:id="$+id:webview"
  3.         ohos:height="match_parent"
  4.         ohos:width="match_parent">
  5.     </ohos.agp.components.webengine.WebView>
复制代码
然后在启动页面执行加载操作。但其实加载前需要先从数据库中提取cookie信息,这个接下来说。

  1. String url = "https://m.bilibili.com";
  2. WebView webView = (WebView) findComponentById(ResourceTable.Id_webview);
  3. webView.getWebConfig().setJavaScriptPermit(true);  // 如果网页需要使用JavaScript,增加此行;如何使用JavaScript下文有详细介绍
  4. webView.load(url);
复制代码
2.Cookie的读取和保存类
com/liangzili/demos/utils/CookieUtils.java
  1. public class CookieUtils {
  2.     private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG,0x0,CookieUtils.class.getName());

  3.     /**
  4.      * 使用关系型数据库[读取]Cookie
  5.      * @param preferences
  6.      * @param url
  7.      */
  8.     public static void ExtarctCookie(Preferences preferences, String url){
  9.         Map<String, ?> map = new HashMap<>();
  10.         //先从数据库中取出cookie
  11.         map = PreferenceDataBase.GetCookieMap(preferences);
  12.         //然后写入到cookieStore
  13.         CookieStore cookieStore = CookieStore.getInstance();//1.获取一个CookieStore的示例
  14.         for (Map.Entry<String, ?> entry : map.entrySet()) {
  15.             HiLog.info(TAG,entry.getKey()+"="+entry.getValue().toString());
  16.             cookieStore.setCookie(url,entry.getKey()+"="+entry.getValue().toString());//2.写入数据,只能一条一条写
  17.         }
  18.     }
  19.    
  20.     /**
  21.      * 使用关系型数据库[保存]Cookie
  22.      * @param preferences  数据库的Preferences实例
  23.      * @param url  指定Cookie对应的域名
  24.      */
  25.     public static void SaveCookie(Preferences preferences,String url){
  26.         //先取出要保存的cookie
  27.         CookieStore cookieStore = CookieStore.getInstance();
  28.         String cookieStr = cookieStore.getCookie(url);
  29.         HiLog.info(TAG,"saveCookie(String url)"+url+cookieStr);

  30.         //然后将cooke转成map
  31.         Map<String,String> cookieMap = cookieToMap(cookieStr);

  32.         //最后将map写入数据库
  33.         PreferenceDataBase.SaveMap(preferences,cookieMap);
  34.     }
  35.     // cookieToMap
  36.     public static Map<String,String> cookieToMap(String value) {
  37.         Map<String, String> map = new HashMap<String, String>();
  38.         value = value.replace(" ", "");
  39.         if (value.contains(";")) {
  40.             String values[] = value.split(";");
  41.             for (String val : values) {
  42.                 String vals[] = val.split("=");
  43.                 map.put(vals[0], vals[1]);
  44.             }
  45.         } else {
  46.             String values[] = value.split("=");
  47.             map.put(values[0], values[1]);
  48.         }
  49.         return map;
  50.     }
  51. }
复制代码
七、偏好型数据库
数据库的操作主要是com/liangzili/demos/database/PreferenceDataBase.java 这个类。使用轻量级偏好型数据库,更符合我们这里的需求。
1.获取Preferences实例
  1. public class PreferenceDataBase {
  2.     private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG,0x0,PreferenceDataBase.class.getName());

  3.     /**
  4.      * 获取Preferences实例
  5.      * @param context  数据库文件将存储在由context上下文指定的目录里。
  6.      * @param name  fileName表示文件名,其取值不能为空,也不能包含路径
  7.      * @return  //返回对应数据库的Preferences实例
  8.      */
  9.     public static Preferences register(Context context,String name) {
  10.         DatabaseHelper databaseHelper = new DatabaseHelper(context);
  11.         Preferences preferences = databaseHelper.getPreferences(name);
  12.         return preferences;
  13.     }
  14.     ... ...
  15. }
复制代码
2.从数据库中保存和读取Map
  1.   /**
  2.      * Map[保存]到偏好型数据库
  3.      * @param preferences  数据库的Preferences实例
  4.      * @param map  要保存的map
  5.      */
  6.     public static void SaveMap(Preferences preferences,Map<String,String> map){
  7.         // 遍历map
  8.         for (Map.Entry<String, String> entry : map.entrySet()) {
  9.             HiLog.info(TAG,entry.getKey() + "=" + entry.getValue());
  10.             preferences.putString(entry.getKey(),entry.getValue());//3.将数据写入Preferences实例,
  11.         }
  12.         preferences.flushSync();//4.通过flush()或者flushSync()将Preferences实例持久化。
  13.     }

  14.     /**
  15.      *  从偏好型数据库[读取]Map
  16.      * @param preferences  数据库的Preferences实例
  17.      * @return  要读取的map
  18.      */
  19.     public static Map<String,?> GetCookieMap(Preferences preferences){
  20.         Map<String, ?> map = new HashMap<>();
  21.         map = preferences.getAll();//3.读取数据
  22.         return map;
  23.     }
复制代码
3.提取某些Cookie的值

  1. /**
  2.      * 获取Cookie中的SESSDATA值
  3.      * @param context 上下文用来指定数据文件存储路径
  4.      * @return  Cookie中的SESSDATA值
  5.      */
  6.     public static String getSessData(Context context){
  7.         // 开启数据库
  8.         DatabaseHelper databaseHelper = new DatabaseHelper(context);//1.创建数据库使用数据库操作的辅助类
  9.         Preferences preferences = databaseHelper.getPreferences("bilibili");//2.获取到对应文件名的Preferences实例,filename是String类型
  10.         String SESSDATA = preferences.getString("SESSDATA","");    //3.读取数据
  11.         return SESSDATA;
  12.     }

  13.     /**
  14.      * 获取Cookie中的Vmid值
  15.      * @param context
  16.      * @return Cookie中的Vmid值
  17.      */
  18.     public static String getVmid(Context context){
  19.         // 开启数据库
  20.         DatabaseHelper databaseHelper = new DatabaseHelper(context);//1.创建数据库使用数据库操作的辅助类
  21.         Preferences preferences = databaseHelper.getPreferences("bilibili");//2.获取到对应文件名的Preferences实例,filename是String类型
  22.         String DedeUserID = preferences.getString("DedeUserID","");    //3.读取数据
  23.         return DedeUserID;
  24.     }
复制代码
八、卡片联动数据更新
这部分使用的是对象关系映射数据库。当点击A卡片对应热区,B卡片同步更新相应的内容。效果如下图
联动卡片.gif
1.创建对象关系映射数据库
数据库用来存储卡片的id、卡片名、卡片的规格信息。首先创建一个MyOrmDatebase的类
  1. import ohos.data.orm.OrmDatabase;
  2. import ohos.data.orm.annotation.Database;

  3. @Database(entities = {Form.class},version = 1)
  4. public abstract class MyOrmDatabase extends OrmDatabase {
  5. }
复制代码
接着建立一个存储卡片信息的数据表对象Form
  1. @Entity(tableName = "form")
  2. public class Form extends OrmObject {
  3.     @PrimaryKey()
  4.     // 卡片id
  5.     private Long formId;

  6.     // 卡片名称
  7.     private String formName;

  8.     // 卡片名称
  9.     private int dimension;

  10.     /**
  11.      * 有参构造
  12.      *
  13.      * @param formId 卡片id
  14.      * @param formName 卡片名
  15.      * @param dimension 卡片规格
  16.      */
  17.     public Form(Long formId, String formName, int dimension) {
  18.         this.formId = formId;
  19.         this.formName = formName;
  20.         this.dimension = dimension;
  21.     }

  22.     /**
  23.      * 无参构造
  24.      */
  25.     public Form() {super();}
  26.     public Long getFormId() {return formId;}
  27.     public void setFormId(Long formId) {this.formId = formId;}
  28.     public String getFormName() {return formName;}
  29.     public void setFormName(String formName) {this.formName = formName;}
  30.     public int getDimension() {return dimension;}
  31.     public void setDimension(int dimension) {this.dimension = dimension;}
  32. }
复制代码
2.在数据卡片设置点击事件
为了方便展示,仅展示部分代码。将数据卡片分为8个区域。
src/main/js/stat/pages/index/index.hml
  1. <div class="column" style="border-color: {{color1}}">
  2.     <text class="title">视频播放</text>
  3.     <text class="data">{{data.total_click}}</text>
  4.     <div>
  5.         <text class="title">昨日</text>
  6.         <text class="data">▲</text>
  7.         <text class="incre_data">{{data.incr_click}}</text>
  8.     </div>
  9. </div>
复制代码
然后每个区域触发点击事件时,将携带区域号params
src/main/js/stat/pages/index/index.json
  1. "actions": {
  2.     "sendMessageEvent1": {
  3.         "action": "message",
  4.         "params": 1
  5.     },
复制代码
3.处理对应的点击事件
当点击A数据卡片时,会触发卡片的StatImpl.java。然后在这个卡片的处理函数中,实现B图标卡片的数据获取和更新
src/main/java/com/liangzili/demos/widget/stat/StatImpl.java
首先,当点击对应选区是,实现一个框选的效果。
  1. ZSONObject zsonObject = new ZSONObject(); //首先,将要刷新的数据存放在一个ZSONObject实例中
  2. for (int i=1;i<9;i++){
  3.     zsonObject.put("color"+i,"");
  4. }
  5. zsonObject.put("color"+message,"#8dd5ed");      //更新data
  6. try {
  7.     ((MainAbility)context).updateForm(formId,new FormBindingData(zsonObject));//调用MainAbility的方法updateForm(),并将formBindingData作为第二个实参
  8. } catch (FormException e) {
  9.     e.printStackTrace();
  10. }
复制代码
然后,通过数据库中保存的卡片信息,提取卡片名为pandect的卡片ID
  1. connect = helper.getOrmContext("FormDatabase", "FormDatabase.db", MyOrmDatabase.class);
  2. OrmPredicates ormPredicates = new OrmPredicates(Form.class);
  3. List<Form> forms = connect.query(ormPredicates); //查询

  4. if (forms.size() <= 0) {
  5.     return;
  6. }
  7. for (Form form : forms) {
  8.     if(form.getFormName().equals("pandect")){
  9.         ZSONObject zsonObject1 = new ZSONObject();
  10.         updatePandect(form.getFormId(),message);
  11.     };
  12. }
复制代码
4.更新图表卡片曲线
调用updatePandect函数,实现图表卡片B的数据获取,这部分的代码可以参考第四部分,数据的更新。
不同的地方在于,图片卡片需要计算坐标系Y的值,来实习一个坐标系,显示不同数据大小的图表。
  1. List<ChartPoint> chartPoints = new ArrayList<>(1);

  2. int[] ValList = new int[30];//保存所有数据的数组
  3. for (int i = 0; i < 30; i++) {
  4.     ValList[i] = pandectEntity.getData().get(i).getTotal_inc();
  5. }
  6. int maxVal = Arrays.stream(ValList).max().getAsInt();//求数据数组中的最大值
  7. int digit = String.valueOf(maxVal).length();//最大值的位数
  8. maxVal = (int)Math.pow(10,digit);//Y轴的最大值
  9. int yVal = (int)Math.pow(10,digit-1);//Y轴坐标值 除数

  10. zsonObject.put("xAxis1",maxVal);
  11. zsonObject.put("xAxis2",maxVal*0.8);
  12. zsonObject.put("xAxis3",maxVal*0.6);
  13. zsonObject.put("xAxis4",maxVal*0.4);
  14. zsonObject.put("xAxis5",maxVal*0.2);

  15. //更新卡片数据
  16. for (int i = 30; i > 0; i--) {
  17.     ChartPoint chartPoint =new ChartPoint();;
  18.     // 绘制点的Y轴坐标 百分比
  19.     chartPoints.add(chartPoint);
  20. }
复制代码
九、分布式播放页面
为七夕节增加一个隐藏的活动页面,利用鸿蒙的分布式拉起能力,实现活动视频的播放效果。
1.添加一个播放页
比如PlayerSlice,这个页面用来实现视频的播放。
image-20210813120305525.png
2.为头像卡片添加点击事件
当点击卡片上的头像时实现页面跳转,代码如下
src/main/js/fans/pages/index/index.hml
  1. <div class="card_root_layout" else>
  2.     <div class="div_left_container">
  3.         <stack class="stack-parent">
  4.             <image src="{{src}}" class="image_src"></image>
  5.             <image src="{{vip}}" class="image_vip"></image>
  6.         </stack>
  7.     </div>
  8.     <text class="item_title">{{follower}}</text>
  9. </div>
复制代码
actions中设置跳转到刚才新建的播放页面。
src/main/js/fans/pages/index/index.json
  1.   "actions": {
  2.     "sendRouterEvent": {
  3.       "action": "router",
  4.       "abilityName": "com.liangzili.demos.Player",
  5.       "params": true
  6.     }
  7.   }
复制代码
3.在播放页判断拉起方式
从intent中提取参数params,如果播放页是服务卡片拉起的,得到true。如果是分布式拉起的得到false。
  1. params = intent.getStringParam("params");//从intent中获取 跳转事件定义的params字段的值
  2. if(params.equals("true")){
  3.     Intent intent0 = new Intent();
  4.     Operation op = new Intent.OperationBuilder()
  5.         .withDeviceId(DistributedUtils.getDeviceId())//参数1.是否跨设备,空,不跨设备
  6.         .withBundleName("com.liangzili.demos")//参数2.在config.json中的bundleName
  7.         .withAbilityName("com.liangzili.demos.Player")//参数3.要跳转的ability名
  8.         .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
  9.         .build();
  10.     intent0.setOperation(op);
  11.     intent0.setParam("params","false");
  12.     startAbility(intent0);
  13.     videoSource = "resources/base/media/right.mp4";
  14. }else{
  15.     videoSource = "resources/base/media/left.mp4";
  16. }
复制代码
4.申请分布式拉起页面权限
如果params就调用分布式拉起页面,得提前为应用获取权限。
[td]
权限名说明
ohos.permission.DISTRIBUTED_DATASYNC必选(分布式数据管理权限,允许不同设备间的数据交换)
ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE必选(允许获取分布式组网内设备的状态变化)
ohos.permission.GET_DISTRIBUTED_DEVICE_INFO必选(允许获取分布式组网内的设备列表和设备信息)
ohos.permission.GET_BUNDLE_INFO必选(查询其他应用信息的权限)
在app首次启动时提醒用户获取分布式权限。
src/main/java/com/liangzili/demos/MainAbility.java
  1. requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"},0);
复制代码
5.获取远端设备ID
要拉起远端设备上的页面,得先获取设备的ID。
  1. public class DistributedUtils {
  2.     public static String getDeviceId(){
  3.         //获取在线设备列表,getDeviceList拿到的设备不包含本机。
  4.         List<DeviceInfo> deviceList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
  5.         if(deviceList.isEmpty()){
  6.             return null;
  7.         }
  8.         int deviceNum = deviceList.size();
  9.         List<String> deviceIds = new ArrayList<>(deviceNum);    //提取设备Id
  10.         List<String> deviceNames = new ArrayList<>(deviceNum);  //提取设备名
  11.         deviceList.forEach((device)->{
  12.             deviceIds.add(device.getDeviceId());
  13. harmonyodeviceNames.add(device.getDeviceName());
  14.         });

  15.         String devcieIdStr = deviceIds.get(0);
  16.         return devcieIdStr;
  17.     }
  18. }
复制代码
6.获取资源地址播放视频
视频播放参考的是软通动力HarmonyOS学院的拜年视频代码,
文章链接
https://harmonyos.51cto.com/posts/3129
  1. //设置沉浸式状态栏
  2. getWindow().addFlags(WindowManager.LayoutConfig.MARK_TRANSLUCENT_STATUS);
  3. initPlayer();

  4. //需要重写两个回调:VideoSurfaceCallback 、VideoPlayerCallback
  5. private void initPlayer() {
  6.     sfProvider=(SurfaceProvider) findComponentById(ResourceTable.Id_surfaceProvider);
  7.     //        image=(Image) findComponentById(ResourceTable.Id_img);
  8.     sfProvider.getSurfaceOps().get().addCallback(new VideoSurfaceCallback());
  9.     // sfProvider.pinToZTop(boolean)--如果设置为true, 视频控件会在最上层展示,但是设置为false时,虽然不在最上层展示,却出现黑屏,
  10.     // 需加上一行代码:WindowManager.getInstance().getTopWindow().get().setTransparent(true);
  11.     sfProvider.pinToZTop(true);
  12.     //WindowManager.getInstance().getTopWindow().get().setTransparent(true);
  13.     player=new Player(getContext());
  14.     //sfProvider添加监听事件
  15.     sfProvider.setClickedListener(new Component.ClickedListener() {
  16.         @Override
  17.         public void onClick(Component component) {
  18.             if(player.isNowPlaying()){
  19.                 //如果正在播放,就暂停
  20.                 player.pause();
  21.                 //播放按钮可见
  22.                 image.setVisibility(Component.VISIBLE);
  23.             }else {
  24.                 //如果暂停,点击继续播放
  25.                 player.play();
  26.                 //播放按钮隐藏
  27.                 image.setVisibility(Component.HIDE);
  28.             }
  29.         }
  30.     });
  31. }
  32. private class VideoSurfaceCallback implements SurfaceOps.Callback {
  33.     @Override
  34.     public void surfaceCreated(SurfaceOps surfaceOps) {
  35.         HiLog.info(logLabel,"surfaceCreated() called.");
  36.         if (sfProvider.getSurfaceOps().isPresent()) {
  37.             Surface surface = sfProvider.getSurfaceOps().get().getSurface();
  38.             playLocalFile(surface);
  39.         }
  40.     }
  41.     @Override
  42.     public void surfaceChanged(SurfaceOps surfaceOps, int i, int i1, int i2) {
  43.         HiLog.info(logLabel,"surfaceChanged() called.");
  44.     }
  45.     @Override
  46.     public void surfaceDestroyed(SurfaceOps surfaceOps) {
  47.         HiLog.info(logLabel,"surfaceDestroyed() called.");
  48.     }
  49. }
  50. private void playLocalFile(Surface surface) {
  51.     try {
  52.         RawFileDescriptor filDescriptor = getResourceManager().getRawFileEntry(videoSource).openRawFileDescriptor();
  53.         Source source = new Source(filDescriptor.getFileDescriptor(),filDescriptor.getStartPosition(),filDescriptor.getFileSize());
  54.         player.setSource(source);
  55.         player.setVideoSurface(surface);
  56.         player.setPlayerCallback(new VideoPlayerCallback());
  57.         player.prepare();
  58.         sfProvider.setTop(0);
  59.         player.play();
  60.     } catch (Exception e) {
  61.         HiLog.info(logLabel,"playUrl Exception:" + e.getMessage());
  62.     }
  63. }
复制代码






回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友