简介
本场景解决方案主要面向于新闻类页面开发人员,指导开发者从零开始构建一个新闻类的首页面,包含地址选择、tabs和tabContent切换的动态图标和流畅动效、下拉刷新上拉加载、首页feed流等常见功能,及功能的流畅体验。
整体场景介绍
主要介绍了用户操作应用的主要流程,展示了用户进入首页通过页签切换页面内容,上拉加载和下拉刷新页面,从首页地址进入地址选择页更换地址等功能效果。
- 操作流程
2. 点击位置信息,跳转地址页,可修改当前位置信息;
3. 点击顶部页签或者滑动切换页面,页签同步切换;
4. 点击底部页签切换页面,同步切换页签,触发页签切换的动画效果;
5. 下拉刷新页面信息;
6. 上拉加载页面信息;
7. 点击右下角按钮回弹至顶部。
场景说明
适用范围
本场景主要适用于新闻类的应用首页,使用原生组件及三方库组件完成新闻首页与功能的实现。
场景优势
本场景可以带给用户更加流畅和便捷的首页体验。具体优势如下:
- 导航栏点击切换动效流畅,响应时延为51ms。
- 左右滑动切换动效流畅,响应时延为67ms。
- 地址选择页定位精确,选择目标城市更加便捷。
- 底部页签跳转精致流畅不丢帧,时延349ms。
- 具有上拉加载下拉刷新页面功能,动效回弹流畅不丢帧,下拉刷新响应时延为153ms,上拉加载响应时延为150ms。
场景分析
典型场景与实现方案
- 实现方案如下表:
场景名称
描述
实现方案
导航栏切换动效流畅
点击页签或者滑动切换页面,页签同步切换
tab组件添加动画开始时触发事件
底部页签跳转精致流畅
底部页签切换具有动画效果
添加lottie动画
上拉加载下拉刷新
上拉加载更多的新闻内容,下拉刷新整个页面,均具有加载动效
pullToRefresh组件
首页feed流
首页展示流畅图文列表
使用LazyForEach对子组件进行渲染,实现懒加载功能
地址选择页
提供地址选择,定位,地址首字母定位及模糊查询功能
位置服务与AlphabetIndexer组件
场景实现
导航栏切换动效流畅
通过添加tab组件动效触发事件实现页面内容切换与页签样式切换同步触发。效果如图所示:
- 动效触发事件节点
推荐使用onAnimationStart事件设置切换标签动效,使用onChange会导致页面切换后再触发动效导致效果延迟触发,使用onClick事件会与页面切换冲突。
build() { Tabs({ barPosition: BarPosition.Start }) { ForEach(this.tabBarArray, (tabsItem: NewsTypeModel, index: number) => { TabContent() { ... } ... }, (item: NewsTypeModel) => JSON.stringify(item)); } ... .onAnimationStart((_index: number, targetIndex: number, _event: TabsAnimationEvent) => { this.currentIndex = targetIndex; }) } // 标签样式 @Builder TabBuilder(id: number, index: number) { Column() { Text(this.tabBarArray[id].name) ... } .alignItems(HorizontalAlign.Start) }
底部页签跳转精致流畅
底部页签样式添加lottie动画使跳转精致流畅。效果如图所示:
- TabBar集成lottie动画
Lottie是一个适用于OpenHarmony的动画库,它可以解析Adobe After Effects软件通过Bodymovin插件导出的json格式的动画,并在移动设备上进行本地渲染。支持动画的交互性,通过添加触摸事件与TabBar相结合可实现动态图标效果。
引入lottie三方库。
准备lottie动画资源,建议放置到Entry目录下的common文件夹下(放置本模块中,使用相对路径无法读取)。
导入lottie模块。
-
import lottie from "@ohos/lottie";
使用RenderingContext在Canvas组件上进行绘制,声明CanvasRenderingContext2D变量,RenderingContextSettings用来配置CanvasRenderingContext2D对象的参数表明canvas是否开启抗锯齿。
-
private renderingSettings1: RenderingContextSettings = new RenderingContextSettings(true); private canvasRenderingContext1: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderingSettings1);
定义所需数据类型的接口,初始化变量,接口中需要包含资源路径信息和CanvasRenderingContext2D。
-
interface TabBarOption { index: number; text: string; name: string; path: string; canvasRenderingContext: CanvasRenderingContext2D; }
实现动画播放方法。
lottieController(): void { if (this.currentBottomIndex === 0) { lottie.stop(); lottie.play(this.tabBarOption1.name); } if (this.currentBottomIndex === 1) { lottie.stop(); lottie.play(this.tabBarOption2.name); } if (this.currentBottomIndex === 2) { lottie.stop(); lottie.play(this.tabBarOption3.name); } }
在TabBar样式中实现子组件Canvas。
Canvas(tabBarOption.canvasRenderingContext) .width($r('app.float.canvas_size')) .height($r('app.float.canvas_size')) .onReady(() => { lottie.loadAnimation({ container: tabBarOption.canvasRenderingContext, renderer: 'canvas', loop: false, autoplay: false, name: tabBarOption.name, path: tabBarOption.path, }); this.lottieController(); })
在tab组件的onAnimationStart事件中调用播放方法。
.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { this.currentBottomIndex = targetIndex this.lottieController(); })
上拉加载下拉刷新
通过三方库组件pullToRefresh实现下拉刷新页面,上拉加载更多数据效果。效果如图所示:
- pullToRefresh组件
使用第三方库pullToRefresh组件,将列表组件、绑定的数据对象和scroller对象包含进去,并添加上滑与下拉方法。支持lazyForEarch的数据作为数据源,使用的List组件需要设置edgeEffect属性为(EdgeEffect.None)。
引入三方库pullToRefresh。
ohpm install @ohos/pulltorefresh
导入lottie模块。
import { PullToRefresh } from '@ohos/pulltorefresh/index';
准备数据源,本场景使用本地资源模拟效果,资源文件在resources/rawfile目录下,实际情况可替换为从网络获取。
const MOCK_DATA_FILE_ONE_DIR: string = 'mockDataOne.json'; const MOCK_DATA_FILE_TWO_DIR: string = 'mockDataTwo.json';
将列表组件、绑定的数据对象和scroller对象包含进去,并添加上滑与下拉方法。
PullToRefresh({ // 必传项,列表组件所绑定的数据 data: $newsData, // 必传项,需绑定传入主体布局内的列表或宫格组件 scroller: this.scroller, // 必传项,自定义主体布局,内部有列表或宫格组件 customList: () => { // 一个用@Builder修饰过的UI方法 this.getListView(); }, // 下拉刷新回调 onRefresh: () => { return new Promise<string>((resolve, reject) => { ... }); }, // 上滑加载回调 onLoadMore: () => { return new Promise<string>((resolve, reject) => { ... }); }, customLoad: null, customRefresh: null, })
首页feed流
通过懒加载实现首页feed流快速渲染与流畅滑动。效果如图所示:
- 懒加载
新闻应用列表数据往往会达到上万条,针对这类大量数据加载的长列表应用,使用懒加载解决一次性加载长列表数据耗时长、占用过多资源的问题,可以提升页面响应速度。
创建需要加载的数据源。
@State newsData: NewsDataSource = new NewsDataSource();
创建子组件。
@Component struct newsItem{...}
使用LazyForEach对子组件进行渲染。
-
List({ space: 3, scroller: this.scroller }) { LazyForEach(this.newsData, (item: NewsData) => { ListItem() { newsItem({...}) ... } }, (item: NewsData, index?: number) => JSON.stringify(item) + index); }
地址选择页
使用原生位置服务实现定位功能,AlphabetIndexer组件实现地址首字母定位导航条。效果如图所示:
- 位置服务与索引条导航
开启手机位置服务功能,使用位置服务提供GNSS定位、网络定位、逆地理编码的功能获取当前地理位置信息,通过AlphabetIndexer组件实现首字母快速定位城市的索引条导航。
申请权限,API9及之后的版本,需要申请ohos.permission.APPROXIMATELY_LOCATION或者同时申请ohos.permission.APPROXIMATELY_LOCATION和ohos.permission.LOCATION;无法单独申请ohos.permission.LOCATION。
requestPermissions: [ { "name": "ohos.permission.APPROXIMATELY_LOCATION", "reason" : "$string:reason", "usedScene" : {} }, { "name": "ohos.permission.LOCATION", "reason" : "$string:reason", "usedScene" : {} } ]
申请用户安全权限。
onPageShow(): void { abilityAccessCtrl.createAtManager().requestPermissionsFromUser(getContext(), [ 'ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']).then(() => { ... }); }
导入位置服务。
获取地理位置信息,进行逆地理编码获取当前位置。import { geoLocationManager } from '@kit.LocationKit';
使用AlphabetIndexer组件实现导航条。... let locationChange: (err: BusinessError, location: geoLocationManager.Location) => void = (err, location) => { if (err) { hilog.error(0x00000, 'locationChanger: err=', JSON.stringify(err)); } if (location) { let reverseGeocodeRequest: geoLocationManager.ReverseGeoCodeRequest = { 'latitude': location.latitude, 'longitude': location.longitude, 'maxItems': CommonConstants.MAX_ITEMS }; geoLocationManager.getAddressesFromLocation(reverseGeocodeRequest, (err, data) => { if (data) { hilog.info(0x00000, 'getAddressesFromLocation: data=', JSON.stringify(data)); if (data[0].locality !== undefined) { AppStorage.setOrCreate('local', data[0].locality.replace(/"/g, '').slice(0, -1)); AppStorage.setOrCreate('currentLocal', data[0].locality.replace(/"/g, '').slice(0, -1)); } } }); } }; ... onPageShow(): void { abilityAccessCtrl.createAtManager().requestPermissionsFromUser(getContext(), [ 'ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']).then(() => { if (this.status) { geoLocationManager.getCurrentLocation(locationChange); this.status = false; } }); } ...
... AlphabetIndexer({arrayValue: TAB_VALUE, selected: this.stabIndex }) .height(CommonConstants.FULL_PERCENT) .selectedColor($r('app.color.alphabet_select_color')) .popupColor($r('app.color.alphabet_pop_color')) .selectedBackgroundColor($r('app.color.alphabet_selected_bgc')) .popupBackground($r('app.color.alphabet_pop_bgc')) .popupPosition({ x: $r('app.integer.pop_position_x'), y: $r('app.integer.pop_position_y') }) .usingPopup(true) .selectedFont({size: $r('app.integer.select_font'), weight: FontWeight.Bolder }) .popupFont({ size: $r('app.integer.pop_font'), weight: FontWeight.Bolder }) .alignStyle(IndexerAlign.Right) ...
通过AlphabetIndexer组件onSelect事件获取选中索引值,通过Scroller的scrollToIndex方法滑动到指定索引位置。
... private scroller: Scroller = new Scroller(); ... AlphabetIndexer({ arrayValue: TAB_VALUE, selected: this.stabIndex }) ... .onSelect((tabIndex: number) => { this.scroller.scrollToIndex(tabIndex); }) ...