鸿蒙-从 0 到 1 开发一个天气 APP

项目名称

浅草天气是一款个人独立开发,设计的 鸿蒙 APP,开发过程中主要参考了 HUAWEI 天气,使用的开发工具有 DevEco studio 和 Figma,在开发的过程中经历过多次改版优化,最终实现了如下的 Feature

  • 定位
  • 当前天气
  • 天气指数
  • 每小时天气
  • 多日天气预报

运行条件

列出运行该项目所必须的条件和相关依赖

  • API 11
  • deploy retrofit

技术使用

  • 使用装饰器来简化权限请求
  • 网络请求
  • list 列表渲染,grid 列表渲染
  • state,provide-consume 使用
  • 组件的提取
  • 自定义view 绘制
  • 贝塞尔曲线
  • 迁移 ts-retrofit 到 Harmony OS

技术讲解

定位权限管理

考虑到天气对地理位置要求不高,本 App 默认是不需要请求定位权限的,目前的技术方案是从高德地图的 Web Api 里面通过IP来获取地址。当然如果需要精确的位置可以先获取 GPS 权限,然后进行请求。 详见本人前段时间的文章

网络请求

目前使用鸿蒙自带的 http + Promise 来实现的,通过 gpt 去生成对应的 class,使用 JSON 来解析对象,具体实现可以参考以下代码

getHourWeather(): Promise<DayForecast> {
  return new Promise(async (resolve: Function, reject: Function) => {
    let url = `${WeatherConstants.SERVER}/${WeatherConstants.HOUR_FORECAST}`;
    if (this.address.infocode == null) {
      this.address = await this.getCityFromIp();
    }
    let location = this.address
    const result = calculateAverageCoordinates(location.rectangle);

    url += '?location=' + result.averageLongitude.toFixed(2) + "," + result.averageLatitude.toFixed(2) + '&key=' + WeatherConstants.KEY;
    let httpRequest = http.createHttp();
    httpRequest.request(url, (err, data) => {
      if (!err) {
        let day : DayForecast = JSON.parse(data.result as string)
        resolve(day)
      }
      else {
        reject(err)
        console.log("getHourWeather:", err.message);
      }
    });
  });
}

天气的 Api 是来自 和风天气 ,可以去上面申请免费的 Api 来使用,免费的额度有限制,也可以花个一杯奶茶钱用付费版的,次数管饱。

页面布局

图片

首先展示的是当前的天气,看天气自然最关系当天的天气咯,其次展示当天的天气指数,天气指数里面我选取了几个比较重要的指数,当然大家也可以选择自己感兴趣的指数。后面展示的是逐小时天气预报,这个是用一个横向的 list 实现的。最后就是一个最重要的多日预报,是一个自定义绘制的 View,也是本篇要重点讲的内容。

多日预报实现

如何绘制这样一个多日天气布局呢,首先我们了解一下 鸿蒙绘制的 Api ,在这里我们需要使用三个 Api。

  1. fillText(text: string, x: number, y: number, maxWidth?: number): void; 用来在指定位置绘制文字
  2. drawImage(image: ImageBitmap | PixelMap, dx: number, dy: number, dw: number, dh: number): void; 在指定位置绘制图像
  3. bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; 绘三阶制贝塞尔曲线
fillText

参数比较简单,相信大家一眼就懂了,但是这里面有一点比较重要,就是我们在绘制文字包括图片的时候要跟当前天气坐标对齐,也就是我们习惯上看起来的上下居中对齐,为了达到这一点我们一般需要测量一下文字的宽度,然后沿着 X 轴平移一半的宽度。下面是示例代码

const w = this.context.measureText(temp).width
this.context.fillText(temp, lowPs[i][0] - (w >> 1), lowPs[i][1] + 20, 40)
drawImage

这个 Api 对我来说有点奇怪,因为我没找到可以对图片进行 tint 的参数,感觉还是挺麻烦的,项目里面图标颜色都是我在 Figma 上面自己改的,如果有好的方案希望有知道的同学告知一下。

bezierCurveTo

绘制三阶贝塞尔曲线,里面的参数我需要介绍一下,首先呢 cp 就是 control point 翻译过来就是控制点,主要是用来决定曲线走向的,最后一个点是 end point,一般是我们实际用到的数据。在很多图像曲线展示中为了使得展示效果比较自然平滑,我们都会采用贝塞尔曲线绘制。

在绘制过程中我们会使用一些算法来生成贝塞尔曲线上面的点,这样我们就可以按照 Api 进行绘制曲线。在这里面随便多嘴一句,写代码是一项具有挑战的工作,但是我们不能等所有的工作都准备好采取行动,很多时候我们需要边学边用。

好了,回归正题,项目里面的贝塞尔曲线的点就是用 github 上搜出来的算法计算出来的,亲测还挺好用。需要注意的是计算出来的点是不包括初始点的,很好理解第一个点不是任何点的 end point,而且他是一个始发点,所以在实现过程中我们需要 moveTo 第一点,至于剩下的坐标我们只需要调用bezierCurveTo 即可。下面是实现的示例代码

const x = highPs[0][0];
const y = highPs[0][1];

this.context.moveTo(x, y)
let points = curveToBezier(highPs)

for (let i = 1; i < points.length - 2; i += 3) {
  const controlPoint1 = points[i];
  const controlPoint2 = points[i + 1];
  const endPoint = points[i + 2];
  this.context.bezierCurveTo(
    controlPoint1[0], controlPoint1[1],
    controlPoint2[0], controlPoint2[1],
    endPoint[0], endPoint[1]
  );
}
如何计算贝塞尔曲线的坐标点?

一般来说我们绘制的时候一个 view 的高度是固定的,上面绘制什么,下面绘制什么都已经确定了。那么中间的坐标位置就是 marginTop+otherViewsHeight(minY) 到 marginTop+otherViewsHeight+curveHeight(maxY) 至于 x 轴的位置,则没有影响跟每日坐标保持一致即可,至于每日的坐标,大家可以按照自己的要求或者其他考虑,有一个合理的 x 轴的 interval 即可。

那么我们知道 y 轴上的 max 以及 min 之后,如何确定每个点在 y 轴上距离呢,比如有 10 个点 [10,11,13,20,30,32,23,32,23,20], 这些点要能全部展示在 view 里面不会跑出来,或者显示的不会留下很多空白,那么我们首先计算 max 和 min ,用这两个点标定最低位和最高位,这样的话温度曲线展示的就会比较自然。下面是示意代码:

for (let index = 0; index < highs.length; index++) {
  const element = highs[index];
  let y = this.calY(max - parseFloat(element), unit, remainSpace) + curTop // margin top
  let x = index * perGap + curLeft // margin left
  highPs.push([x, y])
}

结尾

由于项目里面用到的是免费的 Api 可能不稳定,如果你遇到请求错误的情况可以自己花个十块钱买一个付费 key ,由于本人精力,技术有限代码中难免存在一些问题,如果有问题可以留言或者提 pr。最后附上代码传送门 周末愉快。

鸿蒙-从 0 到 1 开发一个 APP

作者:CaptainZ
链接:https://juejin.cn/post/7401060368087777318
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

<think>好的,用户想了解如何在HarmonyOS天气预报App中实现管理城市页面。首先,我需要结合用户提供的三个引用内容,特别是引用3,其中提到使用城市ID存储和调用。另外,引用2提到了界面设计,使用ArkUI框架,Flex和Grid组件,以及顶部导航栏的布局。 首先,管理城市页面通常包括城市列表、添加和删除功能。根据引用3,城市数据需要存储,所以应该使用HarmonyOS的数据管理能力,比如Preferences或关系型数据库。然后,界面部分需要展示城市列表,每个城市可能有删除按钮,顶部有添加按钮跳转到添加页面。添加页面需要搜索或选择城市,调用和风天气API查询城市ID,如引用1提到的HTTP请求。 接下来,要考虑ArkUI组件的使用,比如List组件展示城市,每个列表项用Flex布局,包含城市名和删除按钮。添加按钮放在导航栏右侧,点击后跳转到添加城市页面,该页面可能有搜索框和结果列表,调用API后存储城市ID。 数据存储方面,使用Preferences保存城市ID数组,每次添加或删除时更新。在添加城市页面,输入城市名后,发送HTTP请求到和风天气的搜索API,获取城市ID,验证后保存到本地。 还需要处理用户交互,比如点击删除按钮时弹出确认对话框,确保数据安全。另外,多设备适配方面,引用2提到使用Flex和Grid组件,确保布局在不同屏幕尺寸上表现良好。 最后,测试功能是否正常,包括添加、删除城市,以及数据持久化。可能还需要考虑网络错误处理,比如API调用失败时的提示。 总结步骤:数据存储设计、界面布局、API调用、用户交互处理、多设备适配。确保各部分符合HarmonyOS开发规范,如使用ArkUI组件和HTTP库。</think>### HarmonyOS天气预报App管理城市页面实现方式 #### 1. 数据存储设计 - **城市ID存储**:使用`Preferences`或`关系型数据库`存储城市ID数组,每个城市对应和风天气API的唯一标识[^3] ```typescript // 示例:使用Preferences存储城市列表 import preferences from '@ohos.data.preferences'; let cityListKey = "cityIdList"; let prefs: preferences.Preferences = await preferences.getPreferences(this.context, "weatherDB"); // 添加城市 async function addCity(cityId: string) { let cities = await prefs.get(cityListKey, JSON.stringify([])); let cityArray = JSON.parse(cities); if (!cityArray.includes(cityId)) { cityArray.push(cityId); await prefs.put(cityListKey, JSON.stringify(cityArray)); } } // 删除城市 async function removeCity(index: number) { let cities = await prefs.get(cityListKey, JSON.stringify([])); let cityArray = JSON.parse(cities); cityArray.splice(index, 1); await prefs.put(cityListKey, JSON.stringify(cityArray)); } ``` #### 2. 界面实现(ArkUI) - **主界面结构**:采用`Column`+`List`布局,包含顶部导航栏和可滚动城市列表 ```typescript @Entry @Component struct CityManagementPage { @State cityList: Array<string> = [] build() { Column() { // 顶部导航栏 Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { Text('城市管理').fontSize(20) Button('+').onClick(() => { router.pushUrl({ url: 'pages/AddCityPage' }) }) }.padding(10) // 城市列表 List({ space: 10 }) { ForEach(this.cityList, (cityId: string, index: number) => { ListItem() { Flex({ justifyContent: FlexAlign.SpaceBetween }) { Text(this.getCityName(cityId)) // 调用城市ID转换方法 Button('删除').onClick(() => { this.handleDelete(index) }) } } }) } } } // 获取城市名称方法(需对接API) private getCityName(cityId: string) { // 实现与本地缓存/API的对接 } // 删除处理 private handleDelete(index: number) { prompt.showDialog({ message: '确认删除该城市?', buttons: [{ text: '取消' }, { text: '确定' }] }).then(result => { if (result.index === 1) { removeCity(index); this.cityList.splice(index, 1); } }) } } ``` #### 3. 添加城市页面 - **API对接**:通过和风天气的`城市搜索API`实现查询功能[^1] ```typescript // 示例:城市搜索请求 import http from '@ohos.net.http'; function searchCities(keyword: string) { let httpRequest = http.createHttp(); httpRequest.request( "https://geoapi.qweather.com/v2/city/lookup?key=API_KEY&location=" + keyword, { method: http.RequestMethod.GET, header: { 'Content-Type': 'application/json' } }, (err, data) => { if (!err) { let result = JSON.parse(data.result); // 处理返回的城市数据 } } ); } ``` #### 4. 关键实现要点 1. **数据同步**:城市列表更新后需触发`@State`变量刷新视图 2. **多设备适配**:使用`栅格布局`和`百分比尺寸`适配不同屏幕[^2] 3. **网络异常处理**:添加`try/catch`块处理API请求失败 4. **本地缓存**:对频繁访问的城市数据建立`LRU缓存`机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值