接触鸿蒙开发已经有 3 个来月了,最近开始在看鸿蒙卡片开发。因为之前的开发大都是基于 Java UI,但按官方的说法,JS 卡片相比 Java 卡片有更大的优势,故决定写个 JS 卡片的 demo 来练练手。
碰巧,前几天和媳妇儿在散步时捡到 1 元钱,没能交给警察叔叔,媳妇儿就提议“我们把它昧了吧,买张彩票。”
由于不是老 CM,没有关注开奖的习惯,想着要是能把开奖结果放在手机桌面显示就好了,这样就不会错过我一夜暴富的机会了。有了需求就开撸,然后就有了这篇文章!
项目简介
①本项目基于 API 6 开发,demo 运行在 API 6 以下的手机上会出现部分功能不能使用的现象。
②卡片功能上实现了***、福彩 3D 的最近一期开奖结果查询,点击卡片“刷新”按钮,调用接口更新最新数据;点击“查看更多”按钮,跳转至应用主界面。
③应用主界面上实现了***、福彩 3D 近 50 期开奖结果查看。
④以上数据均使用了聚合数据的 https://www.juhe.cn/docs/api/id/300 免费接口。
需申请 key,一个 key 一天可免费调用 100 次,如遇 key使 用次数过多导致接口请求失败情况时,开发者可自行申请 key 并替换 Constants.java 文件下的 JH_KEY 常量值),数据可能会有延迟。
⑤卡片开发部分使用了卡片的 JS UI 框架,但由于系统 PA 与 FA 相互调用的限制问题,卡片的业务逻辑部分仍然采用 Java 代码编写。
PS:最开始的想法是尽量可能的少依赖 Java 代码,故尝试了 JS FA 与 Java PA 相互调用的方式,但当应用进程被干掉时,Java 端无法再调用到 JS 端方法。
这样就导致 JS 只能写 UI 部分,业务逻辑还得 Java 层实现。有知道解决办法的也麻烦告知一声。
⑥卡片业务层使用 Java 开发,采用了简单的 MVP 架构,网络请求和数据处理部分使用了 rxjava3+retrofit 框架。
⑦应用主界面使用了 JS-UI 框架实现。
实现效果
请忽略我粗陋的 UI 设计和 GIF 的渣渣像素。
:
项目代码结构分析
base:
IBasePresenter:MVP 架构中 presenter 基类接口
IBaseView:MVP 架构中 view 基类接口
network:
bean,LotteryBean:彩票详情接口返回对应 model
CachedLotteryDetailUtil:彩票详情接口请求工具类,主要作用是防止重复调用详情接口
LogInterceptor:OKhttp 日志拦截工具类
LotteryAPI:接口请求类,通过 retrofit 注解,将接口返回数据转化为实体类
Services:配置 Retrofit 并提供
presenter:
IMainContract:MVP 中 view 与 presenter 的桥梁
MainPresenter:提供彩票详情接口的请求,并处理接口返回数据为卡片需要的 ZSONObject 对象
utils:
LogUtil:日志打印工具类
widget:
controller:FormController:创建卡片时自动生成,卡片管理器的抽象基类;FormControllerManager:创建卡片时自动生成,管理各个 FormController 的工具类
dltwidget.DltWidgetImpl:大乐透卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
fcsdwidget.FcsdWidgetImpl:福彩 3D 卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
ssqwidget.SsqWidgetImpl:双色球卡片管理类,提供了创建卡片、更新卡片、删除卡片、卡片点击事件等行为的回调方法
Constants:常量工具类。
MainAbility:HAP 的入口 ability,由 DevEco Studio 自动生成。同时也是各个卡片对应的 Ability,用来项各个 FormController 分发事件。
default:
common,component/lottery:应用首页列表 item 组件;images :资源图片
pages,home:应用首页
dlt_widget:
common:资源图片存放目录
pages/index,index.css :大乐透卡片 css 样式;index.hml:大乐透卡片布局文件;index.json:包含页面默认值
fcsd_widget:目录结构同 dlt_widget。
ssq_widget:目录结构同 dlt_widget。
详细实现过程
①创建双色球卡片
在目录 entry 上点击右键,在弹出的菜单中选择 New,然后在弹出的子菜单中点击 Service Widget,如下图所示:
在模板选择界面,选择基本的模板 Grid Pattern,点击按钮 Next,进入到卡片配置界面:
首先配置卡片的名称和描述;然后配置卡片关联的 Page Ability;然后配置卡片的编程语言类型是 JS;接下来配置卡片的 JS 组件名称;最后配置卡片支持的规格,勾选支持 2x4、4x4 规格。
重复上述步骤,创建出大乐透和双色球卡片。运行项目,长按图标打开卡片管理界面,我们能看到刚创建的 3 类卡片,且每类卡片对应 3 种不同样式,如下图所示:
②绘制双色球卡片 UI
我们编写双色球界面:
《!--index.hml--》《div class=“container”》
《div》
《text class=“text”》双色球《/text》
《text class=“text-small” style=“margin-left : 15px;” if=“{{ cardType2x4 || cardType4x4 }}”》每周二、四、日开奖《/text》
《text class=“text” style=“margin-left : 75px;” if=“{{ cardType2x4 || cardType4x4 }}” onclick=“showMore”》 查看更多
《/text》
《/div》
《div style=“margin-top : 10px; align-content : center;”》
《text class=“text-small” style=“margin-right : 10px;”》第{{ lotteryData.lottery_no }}期《/text》
《text class=“text-small” if=“{{ cardType2x4 || cardType4x4 }}”》 开奖日期:《/text》
《text class=“text-small”》 {{ lotteryData.lottery_date }}《/text》
《/div》
《div style=“flex-wrap : wrap; margin-top : 10px;”》
《text class=“ball” for=“{{ lotteryRed }}” tid=“id”》{{ $item }}《/text》
《text class=“ball” style=“background-color : blue;” for=“{{ lotteryBlue }}” tid=“id”》{{ $item }}《/text》
《/div》
《div class=“amount-box” if=“{{ cardType2x4 || cardType4x4 }}”》
《div style=“flex-direction : column;”》
《text class=“text-small”》本期全国销量《/text》
《text class=“text-amount”》{{ lotteryData.lottery_sale_amount }}《/text》
《/div》
《text class=“diver”》《/text》
《div style=“flex-direction : column;”》
《text class=“text-small”》累计奖池《/text》
《text class=“text-amount”》{{ lotteryData.lottery_pool_amount }}《/text》
《/div》
《/div》
《div style=“flex-direction : column; padding-top : 10px; margin-bottom: 20px;” if=“{{ cardType4x4 }}”》
《div style=“background-color : green;”》
《text class=“text-prize”》奖项《/text》
《text class=“text-prize”》中奖条件《/text》
《text class=“text-prize”》中奖注数《/text》
《text class=“text-prize”》单注金额(元)《/text》
《/div》
《div for=“{{ lottery_prize }}”》
《text class=“text-prize”》{{ $item.prize_name }}《/text》
《text class=“text-prize”》{{ $item.prize_require }}《/text》
《text class=“text-prize”》{{ $item.prize_num }}《/text》
《text class=“text-prize”》{{ $item.prize_amount }}《/text》
《/div》
《/div》
《div》
《text class=“text-small” if=“{{ cardType2x4 || cardType4x4 }}”》更新时间:《/text》
《text class=“text-small”》{{ updateTime }}《/text》
《image class=“refresh” src=“/common/image_1.png” onclick=“updateData”》《/image》
《/div》《/div》
《!--index.json--》
{
“data”: {
“cardType2x2”: true,
“cardType2x4”: false,
“cardType4x4”: false,
“lotteryData”: {
“lottery_no”: “21081”,
“lottery_date”: “2021-07-20”,
“lottery_sale_amount”:“344,437,194”,
“lottery_pool_amount”:“997,378,346”
},
“lotteryRed”:[“01”,“03”,“05”,“18”,“22”,“23”],
“lotteryBlue”:[“01”],
“updateTime”: “2021/07/07 1459”,
“lottery_prize”:[
{
“prize_name”:“一等奖”,
“prize_num”:“4”,
“prize_amount”:“10,000,000”,
“prize_require”:“6+1”
},
{
“prize_name”:“二等奖”,
“prize_num”:“135”,
“prize_amount”:“207,725”,
“prize_require”:“6+0”
},
{
“prize_name”:“三等奖”,
“prize_num”:“879”,
“prize_amount”:“3,000”,
“prize_require”:“5+1”
},
{
“prize_name”:“四等奖”,
“prize_num”:“45659”,
“prize_amount”:“200”,
“prize_require”:“5+0,4+1”
},
{
“prize_name”:“五等奖”,
“prize_num”:“1001881”,
“prize_amount”:“10”,
“prize_require”:“4+0,3+1”
},
{
“prize_name”:“六等奖”,
“prize_num”:“6962930”,
“prize_amount”:“5”,
“prize_require”:“2+1,1+1,0+1”
}
]
},
“actions”: {
}
}
《!--index.css--》
.container {
width: 100%;
height: 100%;
padding: 10px;
flex-direction: column;
}
.text {
font-size: 15px;
}
.text-small {
font-size: 11px;
}
.text-amount {
font-size: 14px;
font-weight: bold;
color: darkred;
}
.text-prize {
font-size: 11px;
flex-weight: 25;
min-height: 20px;
text-align: center;
}
.ball {
width: 20px;
height: 20px;
text-align: center;
font-size: 12px;
margin: 4px;
color: #FFFFFF;
border-radius: 20px;
background-color: red;
}
.amount-box {
flex-direction: row;
align-items: center;
height: 30px;
}
.diver {
width: 1px;
height: 20px;
background-color: red;
margin-left: 15px;
margin-right: 15px;
}
.refresh {
width: 22px;
height: 22px;
}
编写完成后,重新运行,不出意外你应该能看到如下效果:
这里提下卡片 JS-UI 框架的坑:
框架提供了原子布局来控制元素在不同尺寸布局上的隐藏和展示,但怎么说呢 ,一句话概括就是:你以为的并不是你以为的。
可以看到我这里放弃了 display-index 的使用,而采用通过 JAVA 端卡片的类型,来适配不同的 UI。
不同于应用开发中的 JS UI 框架,这里的条件渲染不支持表达式。
css 不支持标签选择器。
部分 css 样式不能被继承,例如给父 div 元素设置了 font-size,你会发现 div 中的 text 组件并没继承上述样式。
列表渲染的 for 循环的数组必须是 index.json data 对象的最外层。
不支持双层 for 循环。
api6 不兼容 api5 的设备。
你会发现卡片 JSON 文件中的 data 对象数据结构同接口返回的数据结构有些差异,就是因为上述原因导致的。
③获取网络数据
简单的 MVP 架构,采用 Retrofit+RxJava 作为异步网络请求框架。
build.gradle 中添加 RxJava 和 Retrofit 的依赖:
// RxJava
api ‘io.reactivex.rxjava33.0.3’
implementation ‘io.openharmony.tpc.thirdlib1.0.0’// retrofit
implementation ‘com.squareup.retrofit22.7.0’
implementation “com.squareup.retrofit22.1.0”
implementation ‘com.squareup.retrofit22.9.0’
config.json 文件中添加网络权限:
// config.json“deviceConfig”: {
“default”: {
“network”: {
“cleartextTraffic”: true,
“securityConfig”: {
“domainSettings”: {
“cleartextPermitted”: true,
“domains”: [
{
“subdomains”: true,
“name”: “apis.juhe.cn”
},
{
“subdomains”: true,
“name”: “v.juhe.cn”
}
]
}
}
}
}
},
“module”: {
。..
“reqPermissions”: [
{
“name”: “ohos.permission.GET_NETWORK_INFO”
},
{
“name”: “ohos.permission.SET_NETWORK_INFO”
},
{
“name”: “ohos.permission.INTERNET”
}
],
。..
}
创建接口请求:
// LotteryAPI.javapublic interface LotteryAPI {
@POST(“/lottery/query”)
@FormUrlEncoded
Observable《LotteryBean》 queryDetail(@Field(“lottery_id”) String lottery_id, @Field(“lottery_no”) String lottery_no, @Field(“key”) String key);
}
P 层和 V 层的接口比较简单,不在罗列。编写 P 层业务逻辑:
//MainPresenter.java@Overridepublic void loadLotteryData(long formId, String lotteryId) {
CachedLotteryDetailUtil.getLotteryDetail(lotteryId)
.flatMap((Function《LotteryBean, ObservableSource《ZSONObject》》) res -》 {
if (0 != res.getError_code()) {
Throwable throwable = new Throwable(res.getReason());
return Observable.error(throwable);
}
ZSONObject zsonObject = buildDataByResult(res);
return Observable.just(zsonObject);
})
.subscribeOn(Schedulers.io())
.observeOn(OpenHarmonySchedulers.mainThread())
.subscribe(new Observer《ZSONObject》() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ZSONObject result) {
if (mView != null) {
mView.onLoadDataSuccess(formId, result);
}
}
@Override
public void onError(@NonNull Throwable e) {
if (mView != null) {
mView.onLoadDataFailed(e);
}
}
@Override
public void onComplete() {
}
});
}
/**
* 根据接口返回的数据,组装成JS卡片需要的数据
*
* @param result
* @return
*/private ZSONObject buildDataByResult(LotteryBean result) {
LotteryBean.DetailBean detailBean = result.getResult();
ZSONObject data = new ZSONObject();
ZSONObject lotteryData = new ZSONObject();
lotteryData.put(“lottery_no”, detailBean.getLottery_no());
lotteryData.put(“lottery_date”, detailBean.getLottery_date());
lotteryData.put(“lottery_sale_amount”, detailBean.getLottery_sale_amount());
lotteryData.put(“lottery_pool_amount”, detailBean.getLottery_pool_amount());
data.put(“lotteryData”, lotteryData);
String[] ballArray = detailBean.getLottery_res().split(“,”);
if (“ssq”.equalsIgnoreCase(detailBean.getLottery_id())){
data.put(“lotteryRed”, Arrays.copyOfRange(ballArray, 0, 6));
data.put(“lotteryBlue”, Arrays.copyOfRange(ballArray, 6, 7));
} else if (“dlt”.equalsIgnoreCase(detailBean.getLottery_id())){
data.put(“lotteryRed”, Arrays.copyOfRange(ballArray, 0, 5));
data.put(“lotteryBlue”, Arrays.copyOfRange(ballArray, 5, 7));
} else if (“fcsd”.equalsIgnoreCase(detailBean.getLottery_id())){
data.put(“lotteryRed”, ballArray);
}
data.put(“updateTime”, new SimpleDateFormat(“yyyy-MM-dd HHss”).format(new Date()));
ZSONArray zsonArray = new ZSONArray();
for (LotteryBean.PrizeBean prizeBean : detailBean.getLottery_prize()) {
ZSONObject prize = new ZSONObject();
prize.put(“prize_name”, prizeBean.getPrize_name());
prize.put(“prize_num”, prizeBean.getPrize_num());
prize.put(“prize_amount”, prizeBean.getPrize_amount());
prize.put(“prize_require”, prizeBean.getPrize_require());
zsonArray.add(prize);
}
data.put(“lottery_prize”, zsonArray);
return data;
}
④V 层调用 P 层,更新卡片
这里提下 MainAbility 的 onCreateForm 方法,它在进入卡片管理界面时被调用,内部通过 FormControllerManager 获取了卡片对应的 FormController,并调用了其 bindFormData() 方法。
由于我们卡片展示的数据来源于网络请求,在对应的 FormController 实现类中不太好获得 formId,所以我们稍微改造一下 FormController 的 bindFormData 方法,把 formId 给传进去。
public abstract ProviderFormInfo bindFormData(long formId);
//MainAbility.javapublic class MainAbility extends AceAbility implements IMainContract.View {
private IMainContract.Presenter mPresenter;
@Override
public void onStart(Intent intent) {
。..
}
@Override
public void onStop() {
super.onStop();
}
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
HiLog.info(TAG, “onCreateForm”);
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
HiLog.info(TAG, “onCreateForm: formId=” + formId + “,formName=” + formName);
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
FormController formController = formControllerManager.getController(formId);
formController = (formController == null) ? formControllerManager.createFormController(formId,
formName, dimension) : formController;
if (formController == null) {
HiLog.error(TAG, “Get null controller. formId: ” + formId + “, formName: ” + formName);
return null;
}
return formController.bindFormData(formId);
}
@Override
protected void onUpdateForm(long formId) {
。..
}
@Override
protected void onDeleteForm(long formId) {
。..
}
@Override
protected void onTriggerFormEvent(long formId, String message) {
。..
}
@Override
public void onNewIntent(Intent intent) {
。..
}
private boolean intentFromWidget(Intent intent) {
。..
}
private String getRoutePageSlice(Intent intent) {
。..
}
private IMainContract.Presenter getPresenter() {
if (mPresenter == null) {
mPresenter = new MainPresenter(this);
}
return mPresenter;
}
/**
* 查询彩票开奖详情
*
* @param formId
* @param lotteryId
*/
public void loadData(long formId, String lotteryId) {
getPresenter().loadLotteryData(formId, lotteryId);
}
@Override
public void onLoadDataSuccess(long formId, ZSONObject data) {
try {
// 更新卡片
updateForm(formId, new FormBindingData(data));
} catch (FormException e) {
e.printStackTrace();
}
}
@Override
public void onLoadDataFailed(Throwable exception) {
。..
}
}
//SsqWidgetImpl.javapublic class SsqWidgetImpl extends FormController{
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, SsqWidgetImpl.class.getName());
public SsqWidgetImpl(Context context, String formName, Integer dimension) {
super(context, formName, dimension);
}
@Override
public ProviderFormInfo bindFormData(long formId) {
HiLog.info(TAG, “===== ssq bind form data”);
ZSONObject zsonObject = new ZSONObject();
ProviderFormInfo providerFormInfo = new ProviderFormInfo();
boolean is2x2 = dimension == DEFAULT_DIMENSION_2X2;
boolean is2x4 = dimension == DIMENSION_2X4;
boolean is4x4 = dimension == DIMENSION_4X4;
zsonObject.put(“cardType2x2”, is2x2);
zsonObject.put(“cardType2x4”, is2x4);
zsonObject.put(“cardType4x4”, is4x4);
providerFormInfo.setJsBindingData(new FormBindingData(zsonObject));
// 调用接口更新卡片数据
((MainAbility)context).loadData(formId,Constants.LOTTERY_ID_SSQ);
return providerFormInfo;
}
@Override
public void updateFormData(long formId, Object.。. vars) {
}
@Override
public void onTriggerFormEvent(long formId, String message) {
}
@Override
public Class《? extends AbilitySlice》 getRoutePageSlice(Intent intent) {
HiLog.info(TAG, “get the default page to route when you click card.”);
return null;
}
}
⑤简单的网络优化
由于 onCreateForm(Intent intent)方法会被调用多次,而每个类型的卡片请求的数据一样,聚合 api 一天 100 次免费请求的次数一会儿就用完了,故对此进行一个简单的优化。
优先看内存缓存中是否有,有且为过期(缓存默认 10 分钟有效期)则直接返回,否则阻塞等待接口请求完成,对于并发请求,若请求队列已有相同的请求,则阻塞,否则创建新的请求。
若不关心接口调用的可略过本节:
// CachedLotteryDetailUtil.java/**
* CachedLotteryDetailUtil
* 缓存彩票详情工具类,防止在卡片创建时,重复调用接口
*
* @author:xwg
* @since 2021-07-30
*/public class CachedLotteryDetailUtil {
//缓存的有效时间 默认10分钟
private static final long CACHE_TIME = 10 * 60 * 1000;
// 缓存的彩票结果
private static volatile ConcurrentHashMap《String, LotteryBean》 cacheResMap = new ConcurrentHashMap《》();
//缓存的请求时间
private static volatile ConcurrentHashMap《String, Long》 reqTimeMap = new ConcurrentHashMap《》();
//正在请求 对应的lotteryId
private static volatile List《String》 requestingLotteryId;
/**
* 获取彩票详情
*
* @param lotteryId
* @return
*/
public static Observable《LotteryBean》 getLotteryDetail(String lotteryId) {
if (cacheResMap.get(lotteryId) != null) {
if (!isExpired(lotteryId)) {
return Observable.create(emitter -》 {
if (!emitter.isDisposed()) {
emitter.onNext(cacheResMap.get(lotteryId));
}
});
} else {
cacheResMap.remove(lotteryId);
}
}
if (isRequesting(lotteryId)) {
return waitToRequestEnd(lotteryId);
} else {
return requestDetail(lotteryId);
}
}
/**
* 是否正在请求
*
* @param lotteryId
* @return
*/
private static boolean isRequesting(String lotteryId) {
if (requestingLotteryId == null || lotteryId.isEmpty()) {
return false;
}
return requestingLotteryId.contains(lotteryId);
}
/**
* 阻塞等待请求结果
*
* @param lotteryId
* @return
*/
private static Observable《LotteryBean》 waitToRequestEnd(final String lotteryId) {
return Observable.create(emitter -》 {
while (isRequesting(lotteryId)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
}
if (!emitter.isDisposed()) {
try {
if (cacheResMap != null && cacheResMap.get(lotteryId) != null && !isExpired(lotteryId)) {
LotteryBean lotteryBean = cacheResMap.get(lotteryId);
emitter.onNext(lotteryBean);
if (!emitter.isDisposed()) {
emitter.onComplete();
}
} else {
Throwable throwable = new Throwable(“请求异常,请稍后再试”);
emitter.onError(throwable);
}
} catch (Exception e) {
emitter.onError(e);
}
}
});
}
/**
* 联网请求彩票详情
*
* @param lotteryId
* @return
*/
private static Observable《LotteryBean》 requestDetail(final String lotteryId) {
if (requestingLotteryId == null) {
requestingLotteryId = new LinkedList《》();
}
if (!lotteryId.isEmpty() && !requestingLotteryId.contains(lotteryId)) {
requestingLotteryId.add(lotteryId);
}
return Services.createAPI(LotteryAPI.class).queryDetail(lotteryId, “”, Constants.JH_KEY)
.doOnError(throwable -》 {
requestingLotteryId.remove(lotteryId);
reqTimeMap.remove(lotteryId);
})
.doAfterNext(lotteryBean -》 {
requestingLotteryId.remove(lotteryId);
cacheResMap.put(lotteryId, lotteryBean);
reqTimeMap.put(lotteryId, System.currentTimeMillis());
});
}
/**
* 缓存是否已经过期
*
* @return
*/
private static boolean isExpired(String lotteryId) {
if (reqTimeMap == null || reqTimeMap.get(lotteryId) == null) {
return true;
}
return System.currentTimeMillis() - reqTimeMap.get(lotteryId) 》 CACHE_TIME;
}
}
⑥给卡片增加刷新事件和查看更多事件
给卡片的 index.json 文件添加 actions,定义 showMore 为 router 事件,触发这个事件会跳转到指定的 abilityName 对应的 Ability。
updateData 为 message 事件,触发该事件,会回调 Ability 中的 onTriggerFormEvent(long formId, String message)方法。
// index.json
{
“data”: {
。..
},
“actions”: {
“showMore”: {
“action”: “router”,
“abilityName”: “com.xwg.lotteryquery.MainAbility”,
“params”: {
“message”: “fcsd”
}
},
“updateData”: {
“action”: “message”,
“params”: {
“key”: “fcsd”
}
}
}
}
卡片的布局文件中添加点击事件:
《!--跳转应用首页事件--》《text class=“text” style=“margin-left : 120px;” if=“{{ cardType2x4 || cardType4x4 }}” onclick=“showMore”》 查看更多
《/text》
《!--点击刷新按钮事件--》《image class=“refresh” src=“/common/refresh.png” onclick=“updateData”》《/image》
// SsqWidgetImpl.java@Overridepublic void onTriggerFormEvent(long formId, String message) {
HiLog.info(TAG, “======== ssq handle card click event.”);
((MainAbility)context).loadData(formId,Constants.LOTTERY_ID_SSQ);
}
添加定时刷新:打开 config.json,对于标签“scheduledUpdateTime”设定的时刻,当到达之后,MainAbility 中卡片的回调方法 onUpdateForm() 就会被自动调用。
updateDuration 默认为 1,下面配置表示:双色球卡片允许定时刷新,从 10:30 开始,每隔半小时刷新一次。
// config.json“forms”: [
{
“jsComponentName”: “ssq_widget”,
“isDefault”: true,
“scheduledUpdateTime”: “10:30”,
“defaultDimension”: “2*2”,
“name”: “SsqWidget”,
“description”: “双色球”,
“colorMode”: “auto”,
“type”: “JS”,
“supportDimensions”: [
“2*2”,
“2*4”,
“4*4”
],
“updateEnabled”: true,
“updateDuration”: 1
}
。..
]
FormController 直接调用 MainAbility 的获取数据方法:
// SsqWidgetImpl.java@Overridepublic void updateFormData(long formId, Object.。. vars) {
HiLog.info(TAG, “======== ssq update form data timing, default 30 minutes”);
((MainAbility)context).loadData(formId,Constants.LOTTERY_ID_SSQ);
}
⑦大乐透和福彩 3D 卡片的实现
这两个卡片的实现过程和双色球卡片基本一致,主要是 UI 上有些区别。大乐透卡片 4X4 样式中,由于中奖信息列表较长,引入了 list 和 list-item 组件,让其可在卡片内上下滚动,具体实现此处不再赘述,有兴趣可阅读源码。
⑧历史开奖列表的实现
这个界面也相对简单,使用了 swiper 组件,左右滑动或点击头部标签栏,完成***、福彩 3D 标签页的切换,每个标签页展示对应的近 50 期开奖结果,使用 list 和 list-item 组件渲染。
由于各列表 item 展示 UI 相近,故将其抽成了组件放置于 */common/component/lottery* 目录下。
lottery 子组件部分,lottery.js:通过 props 接收外部传入的 lotteryData 数据。
// lottery.jsexport default {
props: {
lotteryData: {}
}
}
lottery.hml 组件布局代码:
《!--lottery.hml--》《div class=“item”》
《div class=“item-header”》
《text style=“font-weight : bold;”》第{{ lotteryData.lottery_no }}期《/text》
《text class=“remarks”》开奖日期:{{ lotteryData.lottery_date }}《/text》
《/div》
《div style=“margin-left : 10px;”》
《text for=“{{ lotteryData.ballList }}”
class=“ball”
style=“background-color : {{ $idx 》= lotteryData.redBallCount ? ‘#6666FF’ : ‘#FF0033’ }};”
》{{ $item }}《/text》
《/div》
《div class=“amount-box”》
《div style=“flex-direction : column;”》
《text class=“text-small”》本期全国销量《/text》
《text class=“text-amount”》{{ lotteryData.lottery_sale_amount }}《/text》
《/div》
《text class=“diver”》《/text》
《div style=“flex-direction : column;”》
《text class=“text-small”》累计奖池《/text》
《text class=“text-amount”》{{ lotteryData.lottery_pool_amount || ‘0’ }}《/text》
《/div》
《/div》《/div》
css 比较简单,这里不再给出。
父组件实现:
《!--home.hml--》《element name=‘lottery’ src=‘。./。./common/component/lottery/lottery.hml’》《/element》《div class=“container”》
《div class=“title-container”》
《text class=“title {{ currentIdx == 0 ? tabSelected : tabUnSelected }}” onclick=“onTabClick(0)”》双色球《/text》
《text class=“title {{ currentIdx == 1 ? tabSelected : tabUnSelected }}” onclick=“onTabClick(1)”》大乐透《/text》
《text class=“title {{ currentIdx == 2 ? tabSelected : tabUnSelected }}” onclick=“onTabClick(2)”》福彩3D《/text》
《/div》
《swiper class=“swiper” id=“swiper” index=“0” indicator=“false” loop=“true”
digital=“false” on:change=“onChange”》
《div class=“swiperContent”》
《list class=“”》
《list-item for=“{{ ssqResList }}” class=“lottery-item”》
《lottery lottery-data = “{{$item}}”》《/lottery》
《/list-item》
《/list》
《/div》
《div class=“swiperContent”》
《list class=“”》
《list-item for=“{{ dltResList }}” class=“lottery-item”》
《lottery lottery-data = “{{$item}}”》《/lottery》
《/list-item》
《/list》
《/div》
《div class=“swiperContent”》
《list class=“”》
《list-item for=“{{ fcsdResList }}” class=“lottery-item”》
《lottery lottery-data = “{{$item}}”》《/lottery》
《/list-item》
《/list》
《/div》
《/swiper》《/div》
《!--home.js--》
import http from ‘@ohos.net.http’;
const JH_URL = ‘http://apis.juhe.cn’const JH_KEY = ‘4931b786dd99c28e7e9990fb75c39fad’export default {
data: {
currentIdx: 0,
tabSelected: ‘tabSelected’,
tabUnSelected: ‘tabUnSelected’,
ssqResList: null, // 双色球近期开奖结果列表
dltResList: null, // 大乐透近期开奖结果列表
fcsdResList: null, // 3D球近期开奖结果列表
},
onInit() {
this.querySsqHis();
this.queryDltHis();
this.queryFcsdHis();
},
// 查询双色球历史开奖
querySsqHis() {
console.info(‘xwg== querySsqHis ----------’);
let _t = this
let httpRequest = http.createHttp();
let url = JH_URL + ‘/history’ + ‘?lottery_id=ssq&page_size=50&page=&key=’ + JH_KEY
httpRequest.request(url, (err, data) =》 {
if (err == null) {
let lotteryResList = JSON.parse(data.result).result.lotteryResList
_t.ssqResList = lotteryResList.map(item =》 {
item.ballList = item.lottery_res.split(“,”)
item.redBallCount = 6
return item
})
console.info(‘xwg== ssqResList:’ + JSON.stringify(_t.ssqResList));
} else {
console.info(‘xwg== error:’ + JSON.stringify(err));
}
});
},
// 查询大乐透历史开奖
queryDltHis() {
let _t = this
let httpRequest = http.createHttp();
let url = JH_URL + ‘/history’ + ‘?lottery_id=ssq&page_size=50&page=&key=’ + JH_KEY
httpRequest.request(url, (err, data) =》 {
if (err == null) {
let dltRes = JSON.parse(data.result).result.lotteryResList
_t.dltResList = dltRes.map(item =》 {
item.ballList = item.lottery_res.split(“,”)
item.redBallCount = 5
return item
})
} else {
console.info(‘xwg== error:’ + JSON.stringify(err));
}
});
},
// 查询福彩3D历史开奖
queryFcsdHis() {
let _t = this
let httpRequest = http.createHttp();
let url = JH_URL + ‘/history’ + ‘?lottery_id=ssq&page_size=50&page=&key=’ + JH_KEY
httpRequest.request(url, (err, data) =》 {
if (err == null) {
let fcsdRes = JSON.parse(data.result).result.lotteryResList
_t.fcsdResList = fcsdRes.map(item =》 {
item.ballList = item.lottery_res.split(“,”)
item.redBallCount = 3
return item
})
} else {
console.info(‘xwg== error:’ + JSON.stringify(err));
}
});
},
// swiper滑动监听
onChange(value) {
this.currentIdx = value.index
},
// tab click事件
onTabClick(idx) {
this.currentIdx = idx
this.$element(‘swiper’).swipeTo({
index: idx
});
},
computed: {}
}
尚未解决的问题:这里引入了 http 组件进行网络请求,但在请求聚合接口时,失败率很高,但尝试请求别的网站的 api 时没有此现象,目前尚不知原因。
总结
由于这也是我第一次使用 JS UI 框架进行卡片开发的项目,水平有限,难免会对官方部分 API 理解不到位甚至理解有误的地方,希望大家也多多指正,共同进步。
这一路上虽然磕磕巴巴,也有很多吐槽,但我们从卡片 JS-UI API 5 到 API 6 功能上逐渐靠拢应用 JS-UI 上也能看出来鸿蒙的努力,给它点时间,相信它功能上会变得更强大、完善;对于开发也会变得更快捷、简单。
最后附上项目地址:lottery-query,请自行下载资源,欢迎交流学习。
https://gitee.com/chinasoft6_ohos/lottery-query
责任编辑:haq
全部0条评论
快来发表一下你的评论吧 !