HarmonyOS实战开发-如何实现一个简单的健康生活应用(上)

介绍

本篇Codelab介绍了如何实现一个简单的健康生活应用,主要功能包括:

  1. 用户可以创建最多6个健康生活任务(早起,喝水,吃苹果,每日微笑,刷牙,早睡),并设置任务目标、是否开启提醒、提醒时间、每周任务频率。
  2. 用户可以在主页面对设置的健康生活任务进行打卡,其中早起、每日微笑、刷牙和早睡只需打卡一次即可完成任务,喝水、吃苹果需要根据任务目标量多次打卡完成。
  3. 主页可显示当天的健康生活任务完成进度,当天所有任务都打卡完成后,进度为100%,并且用户的连续打卡天数加一。
  4. 当用户连续打卡天数达到3、7、30、50、73、99天时,可以获得相应的成就。成就在获得时会以动画形式弹出,并可以在“成就”页面查看。
  5. 用户可以查看以前的健康生活任务完成情况。

本应用的运行效果如下图所示:

相关概念

  • @Observed 和 @ObjectLink:@Observed适用于类,表示类中的数据变化由UI页面管理;@ObjectLink应用于被@Observed装饰类的对象。
  • @Consume 和 @Provide:@Provide作为数据提供者,可以更新子节点的数据,触发页面渲染。@Consume检测到@Provide数据更新后,会发起当前视图的重新渲染。
  • Flex:一个功能强大的容器组件,支持横向布局,竖向布局,子组件均分和流式换行布局。
  • List:List是很常用的滚动类容器组件之一,它按照水平或者竖直方向线性排列子组件, List的子组件必须是ListItem,它的宽度默认充满List的宽度。
  • TimePicker:TimePicker是选择时间的滑动选择器组件,默认以00:00至23:59的时间区创建滑动选择器。
  • Toggle:组件提供勾选框样式、状态按钮样式及开关样式。
  • 后台代理提醒:使用后台代理提醒能力后,应用可以被冻结或退出,计时和弹出提醒的功能将被后台系统服务代理。
  • 关系型数据库(Relational Database,RDB):一种基于关系模型来管理数据的数据库。

环境搭建

软件要求

  • DevEco Studio版本:DevEco Studio 3.1 Release。
  • OpenHarmony SDK版本:API version 9。

硬件要求

  • 开发板类型:润和RK3568开发板。
  • OpenHarmony系统:3.2 Release。

环境搭建

完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:

  1. 获取OpenHarmony系统版本:标准系统解决方案(二进制)。以3.2 Release版本为例:

2.搭建烧录环境。

  1. 完成DevEco Device Tool的安装
  2. 完成RK3568开发板的烧录

3.搭建开发环境。

  1. 开始前请参考工具准备,完成DevEco Studio的安装和开发环境配置。
  2. 开发环境配置完成后,请参考使用工程向导创建工程(模板选择“Empty Ability”),选择JS或者eTS语言开发。

  3. 工程创建完成后,选择使用真机进行调测。

代码结构解读

本篇Codelab只对核心代码进行讲解。

├─entry/src/main/ets                 // 代码区
│  ├─common
│  │  ├─constants
│  │  │  └─CommonConstants.ets       // 公共常量
│  │  ├─database
│  │  │  ├─rdb                       // 数据库
│  │  │  │  ├─RdbHelper.ets
│  │  │  │  ├─RdbHelperImp.ets
│  │  │  │  ├─RdbUtil.ets
│  │  │  │  └─TableHelper.ets
│  │  │  └─tables                    // 数据库接口
│  │  │     ├─DayInfoApi.ets
│  │  │     ├─GlobalInfoApi.ets
│  │  │     └─TaskInfoApi.ets
│  │  └─utils
│  │     ├─BroadCast.ets             // 通知
│  │     ├─GlobalContext.ets         // 全局上下文
│  │     ├─HealthDataSrcMgr.ets      // 数据管理单例
│  │     ├─Logger.ets                // 日志类
│  │     └─Utils.ets                 // 工具类
│  ├─entryability
│  │  └─EntryAbility.ets             // 程序入口类
│  ├─model                           // model
│  │  ├─AchieveModel.ets
│  │  ├─DatabaseModel.ets            // 数据库model
│  │  ├─Mine.ets
│  │  ├─NavItemModel.ets             // 菜单栏model
│  │  ├─RdbColumnModel.ets           // 数据库表数据
│  │  ├─TaskInitList.ets
│  │  └─WeekCalendarModel.ets        // 日历model
│  ├─pages
│  │  ├─AdvertisingPage.ets          // 广告页
│  │  ├─MainPage.ets                 // 应用主页面
│  │  ├─MinePage.ets                 // 我的页面
│  │  ├─SplashPage.ets               // 启动页
│  │  ├─TaskEditPage.ets             // 任务编辑页面
│  │  └─TaskListPage.ets             // 任务列表页面
│  ├─service
│  │  └─ReminderAgent.ets            // 后台提醒
│  ├─view
│  │  ├─dialog                       // 弹窗组件
│  │  │  ├─AchievementDialog.ets     // 成就弹窗
│  │  │  ├─CustomDialogView.ets      // 自定义弹窗
│  │  │  ├─TaskDetailDialog.ets      // 打卡弹窗
│  │  │  ├─TaskDialogView.ets        // 任务对话框
│  │  │  ├─TaskSettingDialog.ets     // 任务编辑相关弹窗
│  │  │  └─UserPrivacyDialog.ets
│  │  ├─home                         // 主页面相关组件
│  │  │  ├─AddBtnComponent.ets       // 添加任务按钮组件
│  │  │  ├─HomeTopComponent.ets      // 首页顶部组件
│  │  │  ├─TaskCardComponent.ets     // 任务item组件件
│  │  │  └─WeekCalendarComponent.ets // 日历组件
│  │  ├─task                         // 任务相关组件
│  │  │  ├─TaskDetailComponent.ets   // 任务编辑详情组件
│  │  │  ├─TaskEditListItem.ets      // 任务编辑行内容
│  │  │  └─TaskListComponent.ets     // 任务列表组件
│  │  ├─AchievementComponent.ets     // 成就页面
│  │  ├─BadgeCardComponent.ets       // 勋章卡片组件
│  │  ├─BadgePanelComponent.ets      // 勋章面板组件
│  │  ├─HealthTextComponent.ets      // 自定义text组件
│  │  ├─HomeComponent.ets            // 首页页面
│  │  ├─ListInfo.ets                 // 用户信息列表
│  │  ├─TitleBarComponent.ets        // 成就标题组件
│  │  └─UserBaseInfo.ets             // 用户基本信息
│  └─viewmodel                       // viewmodel
│     ├─AchievementInfo.ets          // 成就信息
│     ├─AchievementMapInfo.ets       // 成就map信息
│     ├─AchievementViewModel.ets     // 成就相关模块
│     ├─BroadCastCallBackInfo.ets    // 通知回调信息
│     ├─CalendarViewModel.ets        // 日历相关模块
│     ├─CardInfo.ets                 // 成就卡片信息
│     ├─ColumnInfo.ets               // 数据库表结构
│     ├─CommonConstantsInfo.ets      // 公共常量信息
│     ├─DayInfo.ets                  // 每日信息
│     ├─GlobalInfo.ets               // 全局信息
│     ├─HomeViewModel.ets            // 首页相关模块
│     ├─PublishReminderInfo.ets      // 发布提醒信息
│     ├─ReminderInfo.ets             // 提醒信息
│     ├─TaskInfo.ets                 // 任务信息
│     ├─TaskViewModel.ets            // 任务设置相关模块
│     ├─WeekCalendarInfo.ets         // 日历信息
│     └─WeekCalendarMethodInfo.ets   // 日历方法信息
└─entry/src/main/resources           // 资源文件夹

应用架构分析

本应用的基本架构如下图所示,数据库为其他服务提供基础的用户数据,主要业务包括:用户可以查看和编辑自己的健康任务并进行打卡、查看成就。UI层提供了承载上述业务的UI界面。

应用主页面

本节将介绍如何给应用添加一个启动页,设计应用的主界面,以及首页的界面开发和数据展示。

启动页

首先我们需要给应用添加一个启动页,启动页里我们需要用到一个定时器来实现启动页展示固定时间后跳转应用主页的功能,效果图如下:

打开应用时会进入此页面,具体实现逻辑是:

通过修改/entry/src/main/ets/entryability里的loadContent路径可以改变应用的入口文件,我们需要把入口文件改为我们写的SplashPage启动页面。

// EntryAbility.ets
windowStage.loadContent('pages/SplashPage', (err, data) => {    
  if (err.code) {...}    
  Logger.info('windowStage','Succeeded in loading the content. Data: ' + JSON.stringify(data));
});

在SplashPage启动页的文件里通过首选项来实现是否需要弹“权限管理”的弹窗,如果需要弹窗的情况下,用户点击同意权限后通过首选项对用户的操作做持久化保存。相关代码如下:

// SplashPage.ets
import data_preferences from '@ohos.data.preferences';
onConfirm() {
  let preferences = data_preferences.getPreferences(this.context, H_STORE);
  preferences.then((res) => {
    res.put(IS_PRIVACY, true).then(() => {
      res.flush();
      Logger.info('SplashPage','isPrivacy is put success');
    }).catch((err: Error) => {
      Logger.info('SplashPage','isPrivacy put failed. Cause:' + err);
    });
  })
  this.jumpAdPage();
}
exitApp() {
  this.context.terminateSelf();
}
jumpAdPage() {
  setTimeout(() => {
    router.replaceUrl({ url: 'pages/AdvertisingPage' });
  }, Const.LAUNCHER_DELAY_TIME);
}
aboutToAppear() {
  let preferences = data_preferences.getPreferences(this.context, H_STORE);
  preferences.then((res) => {
    res.get(IS_PRIVACY, false).then((isPrivate) => {
      if (isPrivate === true) {
        this.jumpAdPage();
      } else {
        this.dialogController.open();
      }
    });
  });
}

APP功能入口

我们需要给APP添加底部菜单栏,用于切换不同的应用模块,由于各个模块之间属于完全独立的情况,并且不需要每次切换都进行界面的刷新,所以我们用到了Tabs,TabContent组件。

本应用一共有首页(HomeIndex),成就(AchievementIndex)和我的(MineIndex)三个模块,分别对应Tabs组件的三个子组件TabContent。

// MainPage.ets
TabContent() {
  HomeIndex({ homeStore: $homeStore, editedTaskInfo: $editedTaskInfo, editedTaskID: $editedTaskID })
    .borderWidth({ bottom: 1 })
    .borderColor($r('app.color.primaryBgColor'))
}
.tabBar(this.TabBuilder(TabId.HOME))
.align(Alignment.Start)
TabContent() {
  AchievementIndex()
}
.tabBar(this.TabBuilder(TabId.ACHIEVEMENT))
TabContent() {
  MineIndex()
    .borderWidth({ bottom: 1 })
    .borderColor($r('app.color.primaryBgColor'))
}
.tabBar(this.TabBuilder(TabId.MINE))

首页

首页包含了任务信息的所有入口,包含任务列表的展示,任务的编辑和新增,上下滚动的过程中顶部导航栏的渐变,日期的切换以及随着日期切换界面任务列表跟着同步的功能,效果图如下:

具体代码实现我们将在下边分模块进行说明:

  1. 导航栏背景渐变

Scroll滚动的过程中,在它的onScroll方法里我们通过计算它Y轴的偏移量来改变当前界面的@State修饰的naviAlpha变量值,进而改变顶部标题的背景色,代码实现如下:

// HomeComponent.ets
// 视图滚动的过程中处理导航栏的透明度
onScrollAction() {  
  this.yOffset = this.scroller.currentOffset().yOffset;  
  if (this.yOffset > Const.DEFAULT_56) {    
    this.naviAlpha = 1; 
  } else {    
    this.naviAlpha = this.yOffset / Const.DEFAULT_56;
  }
}

2.日历组件

日历组件主要用到的是一个横向滑动的Scroll组件。

// WeekCalendarComponent.ets
   build() {    
     Row() {      
       Column() {        
         Row() {...}             
         Scroll(this.scroller) {          
           Row() {            
             ForEach(this.homeStore.dateArr, (item: WeekDateModel, index?: number) => {              
               Column() {                
                 Text(item.weekTitle)                  
                   .fontColor(sameDate(item.date, this.homeStore.showDate) ? $r('app.color.blueColor') : $r('app.color.titleColor'))                                 
                 Divider()
                   .color(sameDate(item.date, this.homeStore.showDate) ? $r('app.color.blueColor') : $r('app.color.white'))                
                 Image(this.getProgressImg(item))                               
               } 
               .onClick(() => WeekCalendarMethods.calenderItemClickAction(item, index, this.homeStore))            
             })          
           }       
         }
         ...               
         .onScrollEdge((event) => this.onScrollEdgeAction(event))      
       }
       ...       
     }
     ...    
   }

手动滑动页面时,我们通过在onScrollEnd方法里计算Scroll的偏移量来实现分页的效果,同时Scroll有提供scrollPage()方法可供我们点击左右按钮的时候来进行页面切换。

// WeekCalendarComponent.ets
import display from '@ohos.display';
...
// scroll滚动停止时通过判断偏移量进行分页处理
onScrollEndAction() {
  if (this.isPageScroll === false) {
    let page = Math.round(this.scroller.currentOffset().xOffset / this.scrollWidth);
    page = (this.isLoadMore === true) ? page + 1 : page;
    if (this.scroller.currentOffset().xOffset % this.scrollWidth != 0 || this.isLoadMore === true) {
      let xOffset = page * this.scrollWidth;
      this.scroller.scrollTo({ xOffset, yOffset: 0 } as ScrollTo);
      this.isLoadMore = false;
    }
    this.currentPage = this.homeStore.dateArr.length / Const.WEEK_DAY_NUM - page - 1;
    Logger.info('HomeIndex', 'onScrollEnd: page ' + page + ', listLength ' + this.homeStore.dateArr.length);
    let dayModel: WeekDateModel = this.homeStore.dateArr[Const.WEEK_DAY_NUM * page+this.homeStore.selectedDay];
    Logger.info('HomeIndex', 'currentItem: ' + JSON.stringify(dayModel) + ', selectedDay  ' + this.homeStore.selectedDay);
    this.homeStore!.setSelectedShowDate(dayModel!.date!.getTime());
  }
  this.isPageScroll = false;
}

我们在需要在Scroll滑动到左边边缘的时候去请求更多的历史数据以便Scroll能一直滑动,通过Scroll的onScrollEdge方法我们可以判断它是否已滑到边缘位置。

// WeekCalendarComponent.ets
onScrollEdgeAction(side: Edge) {
  if (side === Edge.Top && this.isPageScroll === false) {
    Logger.info('HomeIndex', 'onScrollEdge: currentPage ' + this.currentPage);
    if ((this.currentPage + 2) * Const.WEEK_DAY_NUM >= this.homeStore.dateArr.length) {
      Logger.info('HomeIndex', 'onScrollEdge: load more data');
      let date: Date = new Date(this.homeStore.showDate);
      date.setDate(date.getDate() - Const.WEEK_DAY_NUM);
      this.homeStore.getPreWeekData(date, () => {});
      this.isLoadMore = true;
    }
  }
}

homeStore主要是请求数据库的数据并对数据进行处理进而渲染到界面上。

// HomeViewModel.ets
public getPreWeekData(date: Date, callback: Function) {
  let weekCalendarInfo: WeekCalendarInfo = getPreviousWeek(date);
    
  // 请求数据库数据
  DayInfoApi.queryList(weekCalendarInfo.strArr, (res: DayInfo[]) => {

    // 数据处理
    ...  
    this.dateArr = weekCalendarInfo.arr.concat(...this.dateArr);
  })
}

同时我们还需要知道怎么根据当天的日期计算出本周内的所有日期数据。

// WeekCalendarModel.ets
export function getPreviousWeek(showDate: Date): WeekCalendarInfo {
  Logger.debug('WeekCalendarModel', 'get week date by date: ' + showDate.toDateString());
  let weekCalendarInfo: WeekCalendarInfo = new WeekCalendarInfo();
  let arr: Array<WeekDateModel> = [];
  let strArr: Array<string> = [];
  let currentDay = showDate.getDay() - 1;
  // 由于date的getDay()方法返回的是0-6代表周日到周六,我们界面上展示的周一-周日为一周,所以这里要将getDay()数据偏移一天
  let currentDay = showDate.getDay() - 1;
  if (showDate.getDay() === 0) {
    currentDay = 6;
  }
  // 将日期设置为当前周第一天的数据(周一)
  showDate.setDate(showDate.getDate() - currentDay);
  for (let index = WEEK_DAY_NUM; index > 0; index--) {
    let tempDate = new Date(showDate);
    tempDate.setDate(showDate.getDate() - index);
    let dateStr = dateToStr(tempDate);
    strArr.push(dateStr);
    arr.push(new WeekDateModel(WEEK_TITLES[tempDate.getDay()], dateStr, tempDate));
  }
  Logger.debug('WeekCalendarModel', JSON.stringify(arr));
  weekCalendarInfo.arr = arr;
  weekCalendarInfo.strArr = strArr;
  return weekCalendarInfo;
}
  1. 悬浮按钮

由于首页右下角有一个悬浮按钮,所以首页整体我们用了一个Stack组件,将右下角的悬浮按钮和顶部的title放在滚动组件层的上边。

// HomeComponent.ets
build() {  
  Stack() {    
    Scroll(this.scroller) {      
      Column() {     
        ...   // 上部界面组件
        Column() {          
          ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) => {            
            TaskCard({
              taskInfoStr: JSON.stringify(item),
              clickAction: (isClick: boolean) => this.taskItemAction(item, isClick)
            })
            ...
          }, (item: TaskInfo) => JSON.stringify(item))} 
      }   
    }
  }
  .onScroll(() => {
    this.onScrollAction()
  })
  // 悬浮按钮
  AddBtn({ clickAction: () => {
    this.editTaskAction()
  } }) 
  // 顶部title 
  Row() {
    Text($r('app.string.EntryAbility_label'))
      .titleTextStyle()
      .fontSize($r('app.float.default_24'))
      .padding({ left: Const.THOUSANDTH_66 })
  }
  .width(Const.THOUSANDTH_1000)
  .height(Const.DEFAULT_56)
  .position({ x: 0, y: 0 })
  .backgroundColor(`rgba(${WHITE_COLOR_0X},${WHITE_COLOR_0X},${WHITE_COLOR_0X},${this.naviAlpha})`)
  CustomDialogView()
} 
.allSize() 
.backgroundColor($r('app.color.primaryBgColor'))

4.界面跳转及传参

首页任务列表长按时需要跳转到对应的任务编辑界面,同时点击悬浮按钮时需要跳转到任务列表页面。

页面跳转需要在头部引入router。

// HomeComponent.ets import router from '@ohos.router';
  1. 任务item的点击事件代码如下
  2. // HomeComponent.ets taskItemAction(item: TaskInfo, isClick: boolean): void { if (!this.homeStore.checkCurrentDay()) { return; } if (isClick) { // 点击任务打卡 let callback: CustomDialogCallback = { confirmCallback: (taskTemp: TaskInfo) => { this.onConfirm(taskTemp) }, cancelCallback: () => { } }; this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]); } else { // 长按编辑任务 let editTaskStr: string = JSON.stringify(TaskMapById[item.taskID - 1]); let editTask: ITaskItem = JSON.parse(editTaskStr); ... router.pushUrl({ url: 'pages/TaskEditPage', params: { params: JSON.stringify(editTask) } }); } }

任务创建与编辑

本节将介绍如何创建和编辑健康生活任务。

功能概述

用户点击悬浮按钮进入任务列表页,点击任务列表可进入对应任务编辑的页面中,对任务进行详细的设置,之后点击完成按钮编辑任务后将返回首页。实现效果如下图:

任务列表与编辑任务

这里主要为大家介绍添加任务列表页的实现、任务编辑的实现、以及具体弹窗设置和编辑完成功能的逻辑实现。

任务列表页

任务列表页由包括上部分的标题、返回按钮以及正中间的任务列表组成。实现效果如图:

使用Navigation以及List组件构成元素,ForEach遍历生成具体列表。这里是Navigation构成页面导航:

// TaskListPage.ets
Navigation() {
  Column() {
    // 页面中间的列表
    TaskList() 
  }
  .width(Const.THOUSANDTH_1000)
  .justifyContent(FlexAlign.Center)
}
.size({ width: Const.THOUSANDTH_1000, height: Const.THOUSANDTH_1000 })
.title(Const.ADD_TASK_TITLE)
.titleMode(NavigationTitleMode.Mini)

列表右侧有一个判断是否开启的文字标识,点击某个列表需要跳转到对应的任务编辑页里。具体的列表实现如下:

// TaskListComponent.ets

@Component
export default struct TaskList {
  ...
  build() {
    List({ space: Const.LIST_ITEM_SPACE }) {
      ForEach(this.taskList, (item: ITaskItem) => {
        ListItem() {
          Row() {
            Row() {
              Image(item?.icon)
                ...
              Text(item?.taskName).fontSize(Const.DEFAULT_20).fontColor($r('app.color.titleColor'))
            }.width(Const.THOUSANDTH_500)

            Blank()
              ...
			// 状态改变
            if (item?.isOpen) {
              Text($r('app.string.already_open'))
                ...
            }
            Image($r('app.media.ic_right_grey'))
              ...
          }
          ...
        }
        ...
        .onClick(() => {
          router.pushUrl({
            url: 'pages/TaskEditPage',
            params: {
              params: formatParams(item),
            }
          })
        })
        ...
      }, (item: ITaskItem) => JSON.stringify(item))
    }
  ...
  }
}

任务编辑页

任务编辑页由上方的“编辑任务”标题以及返回按钮,主体内容的List配置项和下方的完成按钮组成,实现效果如图:

由于每一个配置项功能不相同,且逻辑复杂,故将其拆分为五个独立的组件。

这是任务编辑页面,由Navigation和一个自定义组件TaskDetail构成:

// TaskEditPage.ets
Navigation() {
  Column() {
    TaskDetail()
  }
  .width(Const.THOUSANDTH_1000)
  .height(Const.THOUSANDTH_1000)
}
.size({ width: Const.THOUSANDTH_1000, height: Const.THOUSANDTH_1000 })
.title(Const.EDIT_TASK_TITLE)
.titleMode(NavigationTitleMode.Mini)

自定义组件由List以及其子组件ListItem构成:

// TaskDetailComponent.ets
List({ space: Const.LIST_ITEM_SPACE }) {
  ListItem() {
    TaskChooseItem()
  }
  ...
  ListItem() {
    TargetSetItem()
  }
  ...
  ListItem() {
    OpenRemindItem()
  }
  ...
  ListItem() {
    RemindTimeItem()
  }
  ...
  ListItem() {
    FrequencyItem()
  }
  ...
}
.width(Const.THOUSANDTH_940)

其中做了禁用判断,需要任务打开才可以点击编辑:

// TaskDetailComponent.ets
.enabled(
  this.settingParams?.isOpen
)

一些特殊情况的禁用,如每日微笑、每日刷牙的目标设置不可编辑:

// TaskDetailComponent.ets
.enabled(
  this.settingParams?.isOpen
    && this.settingParams?.taskID !== taskType.smile
    && this.settingParams?.taskID !== taskType.brushTeeth
)

提醒时间在开启提醒打开之后才可以编辑:

// TaskDetailComponent.ets
.enabled(this.settingParams?.isOpen && this.settingParams?.isAlarm)

设置完成之后,点击完成按钮,会向数据库更新现在进行改变的状态信息,并执行之后的逻辑判断:

// TaskDetailComponent.ets
addTask(taskInfo, context).then((res: number) => {
  GlobalContext.getContext().setObject('taskListChange', true);
  // 成功的状态,成功后跳转首页
  router.back({
    url: 'pages/MainPage', 
    params: {
      editTask: this.backIndexParams(),
    }
  })
  Logger.info('addTaskFinished', JSON.stringify(res));
}).catch((error: Error) => {
  // 失败的状态,失败后弹出提示,并打印错误日志
  prompt.showToast({
    message: Const.SETTING_FINISH_FAILED_MESSAGE
  })
  Logger.error('addTaskFailed', JSON.stringify(error));
})

任务编辑弹窗

弹窗由封装的自定义组件CustomDialogView注册事件,并在点击对应的编辑项时进行触发,从而打开弹窗。

CustomDialogView引入实例并注册事件:

// TaskDialogView.ets
targetSettingDialog: CustomDialogController = new CustomDialogController({ 
  builder: TargetSettingDialog(),
  autoCancel: true,
  alignment: DialogAlignment.Bottom,
  offset: { dx: Const.ZERO, dy: Const.MINUS_20 }
});
...

// 注册事件
this.broadCast.on(BroadCastType.SHOW_TARGET_SETTING_DIALOG, () => {
  this.targetSettingDialog.open();
})

点击对应的编辑项进行触发:

// TaskDetailComponent.ets
.onClick(() => {
  this.broadCast.emit(
    BroadCastType.SHOW_TARGET_SETTING_DIALOG);
})

自定义弹窗的实现:

任务目标设置的弹窗较为特殊,故单独拿出来说明。

因为任务目标设置有三种类型:

  • 早睡早起的时间
  • 喝水的量度
  • 吃苹果的个数

如下图所示:

故根据任务的ID进行区分,将同一弹窗复用:

// TaskSettingDialog.ets
if ([taskType.getup, taskType.sleepEarly].indexOf(this.settingParams?.taskID) > Const.HAS_NO_INDEX) {
  TimePicker({
    selected: new Date(`${new Date().toDateString()} 8:00:00`),
  })
    .height(Const.THOUSANDTH_800)
    .useMilitaryTime(true)
    .onChange((value: TimePickerResult) => {
      this.currentTime = formatTime(value);
    })
} else {
  TextPicker({ range: this.settingParams?.taskID === taskType.drinkWater ? this.drinkRange : this.appleRange })
    .width(Const.THOUSANDTH_900,)
    .height(Const.THOUSANDTH_800,)
    .onChange((value) => {
      this.currentValue = value?.split(' ')[0];
    })
}

弹窗确认的时候将修改好的值赋予该项设置,如不符合规则,将弹出提示:

// TaskSettingDialog.ets
// 校验规则
compareTime(startTime: string, endTime: string) {
  if (returnTimeStamp(this.currentTime) < returnTimeStamp(startTime) ||
    returnTimeStamp(this.currentTime) > returnTimeStamp(endTime)) {
    prompt.showToast({
      message: Const.CHOOSE_TIME_OUT_RANGE
    })
    return false;
  }
  return true;
}
// 设置修改项
setTargetValue() {
  if (this.settingParams?.taskID === taskType.getup) {
    if (!this.compareTime(Const.GET_UP_EARLY_TIME, Const.GET_UP_LATE_TIME)) {
      return;
    }
    this.settingParams.targetValue = this.currentTime;
    return;
  }
  if (this.settingParams?.taskID === taskType.sleepEarly) {
    if (!this.compareTime(Const.SLEEP_EARLY_TIME, Const.SLEEP_LATE_TIME)) {
      return;
    }
    this.settingParams.targetValue = this.currentTime;
    return;
  }
  this.settingParams.targetValue = this.currentValue;
}

其余弹窗实现基本类似,这里不再赘述。

后台代理提醒

健康生活App中提供了任务提醒功能,我们用系统提供的后台代理提醒reminderAgent接口完成相关的开发。

说明: 后台代理提醒接口需要在module.json5中申请ohos.permission.PUBLISH_AGENT_REMINDER权限,代码如下:

// module.json5
"requestPermissions": [
  {
    "name": "ohos.permission.PUBLISH_AGENT_REMINDER"
  }
]

后台代理提醒entry\src\main\ets\service\ReminderAgent.ts文件中提供了发布提醒任务、查询提醒任务、删除提醒任务三个接口供任务编辑页面调用,跟随任务提醒的开关增加、更改、删除相关后台代理提醒,代码如下:

// ReminderAgent.ets
import reminderAgent from '@ohos.reminderAgentManager';
import notification from '@ohos.notificationManager';
import preferences from '@ohos.data.preferences';
import Logger from '../common/utils/Logger';
import { CommonConstants as Const } from '../common/constants/CommonConstants';
import ReminderInfo from '../viewmodel/ReminderInfo';
import PublishReminderInfo from '../viewmodel/PublishReminderInfo';

// 发布提醒
function publishReminder(params: PublishReminderInfo, context: Context) {
  if (!params) {
    Logger.error(Const.REMINDER_AGENT_TAG, 'publishReminder params is empty');
    return;
  }
  let notifyId: string = params.notificationId.toString();
  hasPreferencesValue(context, notifyId, (preferences: preferences.Preferences, hasValue: boolean) => {
    if (hasValue) {
      preferences.get(notifyId, -1, (error: Error, value: preferences.ValueType) => {
        if (typeof value !== 'number') {
          return;
        }
        if (value >= 0) {
          reminderAgent.cancelReminder(value).then(() => {
            processReminderData(params, preferences, notifyId);
          }).catch((err: Error) => {
            Logger.error(Const.REMINDER_AGENT_TAG, `cancelReminder err: ${err}`);
          });
        } else {
          Logger.error(Const.REMINDER_AGENT_TAG, 'preferences get value error ' + JSON.stringify(error));
        }
      });
    } else {
      processReminderData(params, preferences, notifyId);
    }
  });
}

// 取消提醒
function cancelReminder(reminderId: number, context: Context) {
  if (!reminderId) {
    Logger.error(Const.REMINDER_AGENT_TAG, 'cancelReminder reminderId is empty');
    return;
  }
  let reminder: string = reminderId.toString();
  hasPreferencesValue(context, reminder, (preferences: preferences.Preferences, hasValue: boolean) => {
    if (!hasValue) {
      Logger.error(Const.REMINDER_AGENT_TAG, 'cancelReminder preferences value is empty');
      return;
    }
    getPreferencesValue(preferences, reminder);
  });
}

// 可通知ID
function hasNotificationId(params: number) {
  if (!params) {
    Logger.error(Const.REMINDER_AGENT_TAG, 'hasNotificationId params is undefined');
    return;
  }
  return reminderAgent.getValidReminders().then((reminders) => {
    if (!reminders.length) {
      return false;
    }
    let notificationIdList: Array<number> = [];
    for (let i = 0; i < reminders.length; i++) {
      let notificationId = reminders[i].notificationId;
      if (notificationId) {
        notificationIdList.push(notificationId);
      }
    }
    const flag = notificationIdList.indexOf(params);
    return flag === -1 ? false : true;
  });
}

function hasPreferencesValue(context: Context, hasKey: string, callback: Function) {
  let preferencesPromise = preferences.getPreferences(context, Const.H_STORE);
  preferencesPromise.then((preferences: preferences.Preferences) => {
    preferences.has(hasKey).then((hasValue: boolean) => {
      callback(preferences, hasValue);
    });
  });
}

// 进程提醒数据
function processReminderData(params: PublishReminderInfo, preferences: preferences.Preferences, notifyId: string) {
  let timer = fetchData(params);
  reminderAgent.publishReminder(timer).then((reminderId: number) => {
    putPreferencesValue(preferences, notifyId, reminderId);
  }).catch((err: Error) => {
    Logger.error(Const.REMINDER_AGENT_TAG, `publishReminder err: ${err}`);
  });
}

// 获取数据
function fetchData(params: PublishReminderInfo): reminderAgent.ReminderRequestAlarm {
  return {
    reminderType: reminderAgent.ReminderType.REMINDER_TYPE_ALARM,
    hour: params.hour || 0,
    minute: params.minute || 0,
    daysOfWeek: params.daysOfWeek || [],
    wantAgent: {
      pkgName: Const.PACKAGE_NAME,
      abilityName: Const.ENTRY_ABILITY
    },
    title: params.title || '',
    content: params.content || '',
    notificationId: params.notificationId || -1,
    slotType: notification.SlotType.SOCIAL_COMMUNICATION
  }
}

function putPreferencesValue(preferences: preferences.Preferences, putKey: string, putValue: number) {
  preferences.put(putKey, putValue).then(() => {
    preferences.flush();
  }).catch((error: Error) => {
    Logger.error(Const.REMINDER_AGENT_TAG, 'preferences put value error ' + JSON.stringify(error));
  });
}

function getPreferencesValue(preferences: preferences.Preferences, getKey: string) {
  preferences.get(getKey, -1).then((value: preferences.ValueType) => {
    if (typeof value !== 'number') {
      return;
    }
    if (value >= 0) {
      reminderAgent.cancelReminder(value).then(() => {
        Logger.info(Const.REMINDER_AGENT_TAG, 'cancelReminder promise success');
      }).catch((err: Error) => {
        Logger.error(Const.REMINDER_AGENT_TAG, `cancelReminder err: ${err}`);
      });
    }
  }).catch((error: Error) => {
    Logger.error(Const.REMINDER_AGENT_TAG, 'preferences get value error ' + JSON.stringify(error));
  });
}

const reminder = {
  publishReminder: publishReminder,
  cancelReminder: cancelReminder,
  hasNotificationId: hasNotificationId
} as ReminderInfo

export default reminder;

实现打卡功能

首页会展示当前用户已经开启的任务列表,每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作,用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为连续打卡一天,连续打卡多天会获得成就徽章。打卡效果如下图所示:

任务列表

使用List组件展示用户当前已经开启的任务,每条任务对应一个TaskCard组件,clickAction包装了点击和长按事件,用户点击任务卡时会触发弹起打卡弹窗,从而进行打卡操作;长按任务卡时会跳转至任务编辑界面,对相应的任务进行编辑处理。代码如下:

// HomeComponent.ets
// 任务列表
ForEach(this.homeStore.getTaskListOfDay(), (item: TaskInfo) => {
  TaskCard({
    taskInfoStr: JSON.stringify(item),
    clickAction: (isClick: boolean) => this.taskItemAction(item, isClick)
  })
  .margin({ bottom: Const.DEFAULT_12 })
  .height($r('app.float.default_64'))
}, (item: TaskInfo) => JSON.stringify(item))
...
CustomDialogView() // 自定义弹窗中间件

自定义弹窗中间件CustomDialogView

在组件CustomDialogView的aboutToAppear生命周期中注册SHOW_TASK_DETAIL_DIALOG的事件回调方法 ,当通过emit触发此事件时即触发回调方法执行。代码如下:

// CustomDialogView.ets
export class CustomDialogCallback {
  confirmCallback: Function = () => {};
  cancelCallback: Function = () => {};
}

@Component
export struct CustomDialogView {
  @State isShow: boolean = false;
  @Provide achievementLevel: number = 0;
  @Consume broadCast: BroadCast;
  @Provide currentTask: TaskInfo = TaskItem;
  @Provide dialogCallBack: CustomDialogCallback = new CustomDialogCallback();

  // 成就对话框
  achievementDialog: CustomDialogController = new CustomDialogController({
    builder: AchievementDialog(),
    autoCancel: true,
    customStyle: true
  });

  // 任务时钟对话框
  taskDialog: CustomDialogController = new CustomDialogController({
    builder: TaskDetailDialog(),
    autoCancel: true,
    customStyle: true
  });

  aboutToAppear() {
    Logger.debug('CustomDialogView', 'aboutToAppear');
    // 成就对话框
    this.broadCast.on(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, (achievementLevel: number) => {
      Logger.debug('CustomDialogView', 'SHOW_ACHIEVEMENT_DIALOG');
      this.achievementLevel = achievementLevel;
      this.achievementDialog.open();
    });

    // 任务时钟对话框
    this.broadCast.on(BroadCastType.SHOW_TASK_DETAIL_DIALOG,
      (currentTask: TaskInfo, dialogCallBack: CustomDialogCallback) => {
        Logger.debug('CustomDialogView', 'SHOW_TASK_DETAIL_DIALOG');
        this.currentTask = currentTask || TaskItem;
        this.dialogCallBack = dialogCallBack;
        this.taskDialog.open();
    });
  }

  aboutToDisappear() {
    Logger.debug('CustomDialogView', 'aboutToDisappear');
  }

  build() {
  }
}

点击任务卡片

点击任务卡片会emit触发 “SHOW_TASK_DETAIL_DIALOG” 事件,同时把当前任务,以及确认打卡回调方法传递下去。代码如下:

// HomeComponent.ets
// 任务卡片事件
taskItemAction(item: TaskInfo, isClick: boolean): void {
  ...
  if (isClick) {
    // 点击任务打卡
     let callback: CustomDialogCallback = { confirmCallback: (taskTemp: TaskInfo) => {
        this.onConfirm(taskTemp)
     }, cancelCallback: () => {
     } };
    // 触发弹出打卡弹窗事件  并透传当前任务参数(item) 以及确认打卡回调
     this.broadCast.emit(BroadCastType.SHOW_TASK_DETAIL_DIALOG, [item, callback]);
  } else {
    // 长按编辑任务
    ...
  }
}
// 确认打卡
onConfirm(task) {
  this.homeStore.taskClock(task).then((res: AchievementInfo) => {
    // 打卡成功后 根据连续打卡情况判断是否 弹出成就勋章  以及成就勋章级别
    if (res.showAchievement) {
      // 触发弹出成就勋章SHOW_ACHIEVEMENT_DIALOG 事件, 并透传勋章类型级别
      let achievementLevel = res.achievementLevel;
      if (achievementLevel) {
        this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG, achievementLevel);
      } else {
        this.broadCast.emit(BroadCastType.SHOW_ACHIEVEMENT_DIALOG);
      }
    }
  })
}

打卡弹窗组件TaskDetailDialog

打卡弹窗组件根据当前任务的ID获取任务名称以及弹窗背景图片资源。

打卡弹窗组件由两个小组件构成,代码如下:

// TaskDetailDialog.ets
Column() {
  // 展示任务的基本信息
  TaskBaseInfo({
    taskName: TaskMapById[this.currentTask?.taskID - 1].taskName  // 根据当前任务ID获取任务名称
  });
  // 打卡功能组件 (任务打卡、关闭弹窗)
  TaskClock({
    confirm: () => {
      this.dialogCallBack.confirmCallback(this.currentTask);
      this.controller.close();
    },
    cancel: () => {
      this.controller.close();
    },
    showButton: this.showButton
  })
}
...

TaskBaseInfo组件代码如下:

// TaskDetailDialog.ets
@Component
struct TaskBaseInfo {
  taskName: string | Resource = '';

  build() {
    Column({ space: Const.DEFAULT_8 }) {
      Text(this.taskName)
        .fontSize($r('app.float.default_22'))
        .fontWeight(FontWeight.Bold)
        .fontFamily($r('app.string.HarmonyHeiTi_Bold'))
        .taskTextStyle()
        .margin({left: $r('app.float.default_12')})
    }
    .position({ y: $r('app.float.default_267') })
  }
}

TaskClock组件代码如下:

// TaskDetailDialog.ets
@Component
struct TaskClock {
  confirm: Function = () => {};
  cancel: Function = () => {};
  showButton: boolean = false;

  build() {
    Column({ space: Const.DEFAULT_12 }) {
      Button() {
        Text($r('app.string.clock_in'))
          .height($r('app.float.default_42'))
          .fontSize($r('app.float.default_20'))
          .fontWeight(FontWeight.Normal)
          .textStyle()
      }
      .width($r('app.float.default_220'))
      .borderRadius($r('app.float.default_24'))
      .backgroundColor('rgba(255,255,255,0.40)')
      .onClick(() => {
        GlobalContext.getContext().setObject('taskListChange', true);
        this.confirm();
      })
      .visibility(!this.showButton ? Visibility.None : Visibility.Visible)
      Text($r('app.string.got_it'))
        .fontSize($r('app.float.default_14'))
        .fontWeight(FontWeight.Regular)
        .textStyle()
        .onClick(() => {
          this.cancel();
        })
    }
  }
}

打卡接口调用

// HomeViewModel.ets
public async taskClock(taskInfo: TaskInfo) {
  let taskItem = await this.updateTask(taskInfo);
  let dateStr = this.selectedDayInfo?.dateStr;
  // 更新任务失败
  if (!taskItem) {
    return {
      achievementLevel: 0,
      showAchievement: false
    } as AchievementInfo;
  }
  // 更新当前时间的任务列表
  this.selectedDayInfo.taskList = this.selectedDayInfo.taskList.map((item) => {
    return item.taskID === taskItem?.taskID ? taskItem : item;
  });
  let achievementLevel: number = 0;
  if(taskItem.isDone) {
    // 更新每日任务完成情况数据
    let dayInfo = await this.updateDayInfo();
    ... 
    // 当日任务完成数量等于总任务数量时 累计连续打卡一天
    // 更新成就勋章数据 判断是否弹出获得勋章弹出及勋章类型
    if (dayInfo && dayInfo?.finTaskNum === dayInfo?.targetTaskNum) {
      achievementLevel = await this.updateAchievement(this.selectedDayInfo.dayInfo);
    }
  }
  ...
  return {
    achievementLevel: achievementLevel,
    showAchievement: ACHIEVEMENT_LEVEL_LIST.includes(achievementLevel)
  } as AchievementInfo;
}
// HomeViewModel.ets
// 更新当天任务列表
updateTask(task: TaskInfo): Promise<TaskInfo> {
  return new Promise((resolve, reject) => {
    let taskID = task.taskID;
    let targetValue = task.targetValue;
    let finValue = task.finValue;
    let updateTask = new TaskInfo(task.id, task.date, taskID, targetValue, task.isAlarm, task.startTime,
      task.endTime, task.frequency, task.isDone, finValue, task.isOpen);
    let step = TaskMapById[taskID - 1].step; // 任务步长
    let hasExceed = updateTask.isDone;
    if (step === 0) { // 任务步长为0 打卡一次即完成该任务
      updateTask.isDone = true; // 打卡一次即完成该任务
      updateTask.finValue = targetValue;
    } else {
      let value = Number(finValue) + step; // 任务步长非0 打卡一次 步长与上次打卡进度累加
      updateTask.isDone = updateTask.isDone || value >= Number(targetValue); // 判断任务是否完成
      updateTask.finValue = updateTask.isDone ? targetValue : `${value}`;
    }
    TaskInfoTableApi.updateDataByDate(updateTask, (res: number) => { // 更新数据库
      if (!res || hasExceed) {
        Logger.error('taskClock-updateTask', JSON.stringify(res));
        reject(res);
      }
      resolve(updateTask);
    })
  })
}

为了帮助大家更深入有效的学习到鸿蒙开发知识点,小编特意给大家准备了一份全套最新版的HarmonyOS NEXT学习资源,获取完整版方式请点击→HarmonyOS教学视频

HarmonyOS教学视频:语法ArkTS、TypeScript、ArkUI等…视频教程

鸿蒙生态应用开发白皮书V2.0PDF:

获取完整版白皮书方式请点击→《鸿蒙生态应用开发白皮书V2.0PDF

在这里插入图片描述

鸿蒙 (Harmony OS)开发学习手册

一、入门必看

  1. 应用开发导读(ArkTS)
  2. .……

在这里插入图片描述


二、HarmonyOS 概念

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

在这里插入图片描述

三、如何快速入门?《鸿蒙基础入门学习指南》

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. .……

在这里插入图片描述


四、开发基础知识

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. .……

在这里插入图片描述


五、基于ArkTS 开发

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 7.网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. .……

在这里插入图片描述


更多了解更多鸿蒙开发的相关知识可以参考:《鸿蒙 (Harmony OS)开发学习手册

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/505407.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++list的模拟实现

为了实现list&#xff0c;我们需要实现三个类 一、List的节点类 template<class T> struct ListNode {ListNode(const T& val T()):_pPre(nullptr),_pNext(nullptr),_val(val){}ListNode<T>* _pPre;ListNode<T>* _pNext;T _val; }; 二、List的迭代器…

2024年腾讯云服务器99元一年_老用户优惠续费不涨价

腾讯云99元一年服务器配置为轻量2核2G4M、50GB SSD盘、300GB月流量、4M带宽&#xff0c;新用户和老用户都可以购买&#xff0c;续费不涨价&#xff0c;续费价格也是99元一年。以往腾讯云优惠服务器都是新用户专享的&#xff0c;这款99元服务器老用户也可以购买&#xff0c;这是…

Spring Task 知识点详解、案例、源代码解析

简介&#xff1a;Spring Task 定时任务   所谓定时任务。就是依据我们设定的时间定时运行任务&#xff0c;就像定时发邮件一样&#xff0c;设定时间到了。邮件就会自己主动发送。 在Spring大行其道的今天&#xff0c;Spring也提供了其定时任务功能&#xff0c;Spring Task。同…

安装dalton过程中出现的pcre问题

在前面文章中&#xff0c;基于多种流量检测引擎识别pcap数据包中的威胁&#xff0c;并没有详细的说明dalton的安装。由于dalton提供了脚本./start-dalton.sh &#xff0c;执行之后会自动的安装各种依赖以及suricata&#xff0c;zeek&#xff0c;snort的容器环境。但是在实际执行…

编程新手必看!从零起步掌握Python的终极指南,Python简介(1)

1、Python语言的诞生 Python的作者&#xff0c;Guido von Rossum&#xff08;吉多范罗苏姆&#xff0c;中国Python程序员都叫他 龟叔&#xff09;&#xff0c;荷兰人。1982年&#xff0c;龟叔从阿姆斯特丹大学获得了数学和计算机硕士学位。然而&#xff0c;尽管他算得上是一位…

内存管理--柔性数组

本次讲的是&#xff0c;柔性数组&#xff0c;如果哪位小博客想要了解的更多&#xff0c;可以登录下面这个网站&#xff0c;了解详细内容 C语言结构体里的成员数组和指针 | 酷 壳 - CoolShellhttps://coolshell.cn/articles/11377.html 我们就听说过数组&#xff0c;听说过柔性数…

Excel求解二元一次方程

背景&#xff1a;如果想求解二元一次方程&#xff0c;常规方法就是联立方程求出一个未知数&#xff0c;然后带入任意一个等式。那么在excel里面应该怎么解决呢&#xff1f; 总所周知&#xff0c;大学里面会学矩阵行列式&#xff0c;二元一次方程其实就是一个简单的矩阵行列式。…

006 高并发内存池_PageCache设计

​&#x1f308;个人主页&#xff1a;Fan_558 &#x1f525; 系列专栏&#xff1a;高并发内存池 &#x1f339;关注我&#x1f4aa;&#x1f3fb;带你学更多知识 文章目录 前言文章重点一、回顾PageCache页缓存结构二、PageCache结构设计三、完善申请内存函数小结 前言 本文将…

聊一聊电子邮件?

电子邮件是什么&#xff1f; 电子邮件是一种基于客户/服务器架构的应用。功能是实现人与人之间的交流。直到现在&#xff0c;电子邮件依然是当前因特网 注意&#xff1a;基于客户/服务器方式和基于B/S架构不一样&#xff01;客户/服务器表示的范围更广&#xff0c;当基于客户…

Linux环境jdk、git、maven、MySQL和redis的安装和配置

这里整理了在Linux上如何安装和配置jdk、git、maven、MySQL和redis&#xff0c;主要用于在Linux上部署Java项目 文章中博主使用了一个叫FinalShell远程连接软件进行上传&#xff0c;如果没有类似的工具也可以直接通过yum命令行下载包 博主使用的Linux服务器为centos7&#xf…

2024最新版Android studio安装入门教程(非常详细)

目录 JDK安装与配置 一、下载JDK 二、JDK安装 三、JDK的环境配置 四、JDK的配置验证 Android studio安装 Android studio连接手机真机调试&#xff08;以华为鸿蒙为例&#xff09; 一、新建一个android项目 二、进入项目面板 三、配置Android Studio 四、安装手机驱…

【JavaSE】初识线程,线程与进程的区别

文章目录 ✍线程是什么&#xff1f;✍线程和进程的区别✍线程的创建1.继承 Thread 类2.实现Runnable接口3.匿名内部类4.匿名内部类创建 Runnable ⼦类对象5.lambda 表达式创建 Runnable ⼦类对象 ✍线程是什么&#xff1f; ⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按…

BigInteger的应用

这里写目录标题 例题BigInteger常用方法关于BigInteger初始化为nullcompareTo()方法 : 返回一个int型数据(1 大于; 0 等于 ; -1 小于) 例题 import java.math.BigInteger; import java.util.*; public class Main{public static void main(String[] args) {BigInteger n BigIn…

android 消息提醒

1.创建 MyBackgroundService.java 继承 Service public class MyBackgroundService extends Service {Overridepublic void onCreate() {super.onCreate();Log.i("业务服务", "开起业务服务");//调用服务后在页面手机上创建一个通知消息。if (android.os…

TS的基础

TS Typed JavaScript at Any Scale. 它强调了 TypeScript 的两个最重要的特性——类型系统、适用于任何规模。 我们知道&#xff0c;JavaScript 是一门非常灵活的编程语言&#xff0c; 它没有类型约束&#xff0c;一个变量可能初始化时是字符串&#xff0c;过一会儿又被赋值为…

如何用 C++ 在 10 行内写出八皇后?

在编程世界中&#xff0c;有时挑战在于以最简洁的方式表达复杂的逻辑。八皇后问题就是这样一道经典难题&#xff0c;它要求在88的棋盘上放置8个皇后&#xff0c;使得任意两个皇后之间都不能位于同一行、同一列或同一斜线上。虽然这个问题可以通过多种算法解决&#xff0c;包括递…

Scala介绍与环境搭建

Scala环境搭建与介绍 一、Scala环境搭建 1、环境准备与下载 2、验证Scala 3、IDEA新建项目&#xff0c;配置Scala&#xff0c;运行Hello world 二、Scala介绍 1、Scala 简介 2、Scala 概述 一、Scala环境搭建 1、环境准备与下载 JDK1.8 Java Downloads | Oracle 下载需求版本…

代码随想录|Day29|贪心04|860.柠檬水找零、406.根据身高重建队列、452.用最少数量的箭引爆气球

860.柠檬水找零 我们维护三种金额的数量&#xff1a;five&#xff0c;ten&#xff0c;twenty 有如下三种情况&#xff1a; 账单是5&#xff1a;five 1&#xff0c;无需找零账单是10&#xff1a;ten 1&#xff0c;找零一张5元&#xff08;five - 1&#xff09;账单是20&#x…

Eclipse EMF教程(上)

What every Eclipse developer should know about EMF 翻译自&#xff1a;https://eclipsesource.com/blogs/tutorials/emf-tutorial/ 本教程是对EMF的介绍&#xff0c;解释了EMF的基础知识。我们首先向您展示如何基于EMF构建一个非常简单的以数据为中心的应用程序&#xff0c…

MES_ENT_STD

生产执行系统&#xff08;企业标准版&#xff09;MES_ENT_STD ERP_ENT_STD_59438.ieqq.ent-CSDN博客 OAMS_ENT_STD-CSDN博客