1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import promptAction from '@ohos.promptAction'; 17import { Text, List, ListItem, Column, Button, Image, Row, Stack,ListItemGroup, Divider, Blank, 18 Tabs, TabContent, Flex, FlexOptions, Callback, px2vp, ItemAlign, BackgroundBrightnessOptions, 19 ButtonAttribute, ClickEvent, Component, BuilderParam, Padding, $r, SafeAreaEdge, BackgroundBlurStyleOptions, 20 BarState, NestedScrollOptions, NestedScrollMode, Color, JSON, Alignment, FlexDirection, BarPosition, 21 FlexAlign, BlurStyle, LazyForEach, ForEach, Builder, Margin, SafeAreaType, MenuItemOptions, 22 TabsController, TabsOptions ,StackOptions, ThemeColorMode, AdaptiveColor, ResourceStr, SizeOptions, 23 Reusable, TextOverflowOptions, ListOptions, LinearGradientOptions, Menu, MenuItem, Axis, BorderRadiuses, 24 WaterFlow, FlowItem, ImageFit, TextAlign, Scroll, ScrollAlign, Scroller, TextOverflow, ShadowStyle, TabsAnimationEvent, 25 OnTabsAnimationStartCallback, Rating, RatingOptions, GestureGroup, GestureEvent, GestureMode, TapGesture, $$, 26 SheetSize, SheetType, ScrollSizeMode, SheetOptions, CustomBuilder, Entry 27} from '@ohos.arkui.component' 28import { State, StateDecoratedVariable, MutableState, stateOf, AppStorage, 29 observableProxy,ObjectLink, Observed, Consume, Link, Provide, Watch} from '@ohos.arkui.stateManagement' 30import display from '@ohos.display'; 31import { SceneModuleInfo } from '../../model/functionalScenes/SceneModuleInfo'; 32import { WaterFlowDataSource } from '../../model/functionalScenes/WaterFlowDataSource'; 33import { TAB_DATA, TabDataModel } from '../../model/functionalScenes/TabsData'; 34import { SCENE_MODULE_LIST } from '../../model/functionalScenes/SceneModuleDatas' 35import { CollapseMenuSection } from '../collapsemenu/CollapseMenuSection'; 36import { UIContext } from '@ohos.arkui.UIContext' 37 38/** 39 * 主页瀑布流列表 40 */ 41@Entry 42@Component 43export struct FunctionalScenes { 44 listData: SceneModuleInfo[] = SCENE_MODULE_LIST; 45 dataSource: WaterFlowDataSource<SceneModuleInfo> = new WaterFlowDataSource<SceneModuleInfo>(this.listData); 46 @State tabsIndex: number = 0; 47 tabsController: TabsController = new TabsController(); 48 private scrollController: Scroller = new Scroller(); 49 items:number[] = [1,2,3,4,5,6,7,8,9,10] as number[]; 50 @Builder 51 tabBuilder(index: number, name: string | undefined) { 52 Stack() { 53 Column() { 54 } 55 .width(this.tabsIndex === index ? 97 : 71) 56 .backgroundColor(this.tabsIndex === index ? '#0A59F7' : '#000000') 57 .opacity(this.tabsIndex === index ? 1 : 0.05) 58 .height(38) 59 .borderRadius(21) 60 Text(name) 61 .fontSize(14) 62 .fontColor(this.tabsIndex === index ? Color.White : Color.Black) 63 .opacity(this.tabsIndex === index ? 1 : 0.8) 64 .height('100%') 65 .id('section') 66 } 67 .margin(index !== 0 && index !== TAB_DATA.length ? { left: 9 } as Margin : { 68 left: 0, 69 right: 0 70 } as Margin) 71 .align(Alignment.Center) 72 .onClick((e: ClickEvent) => { 73 this.tabsIndex = index; 74 this.tabsController.changeIndex(index); 75 }) 76 } 77 78 @Builder 79 tabsMenu() { 80 Menu() { 81 ForEach(TAB_DATA, (item: TabDataModel) => { 82 MenuItem({ content: item.navData } as MenuItemOptions) 83 .onClick((e: ClickEvent) => { 84 this.tabsIndex = item.id; 85 this.tabsController.changeIndex(item.id); 86 }) 87 .id('menu_item') 88 }) 89 } 90 } 91 92 /** 93 * 主页通过瀑布流和LazyForeach加载 94 * WaterFlow+LazyForEach详细用法可参考性能范例: 95 * waterflow_optimization.md/ 96 */ 97 build() { 98 Column() { 99 Row() { 100 Text($r('app.string.custom_return')) 101 } 102 .margin(3) 103 .width('100%') 104 .onClick((e: ClickEvent) => { 105 this.getUIContext().getRouter().back(); 106 }) 107 Row() { 108 Stack() { 109 List({ scroller: this.scrollController } as ListOptions) { 110 ForEach(TAB_DATA, (tabItem: TabDataModel) => { 111 ListItem() { 112 this.tabBuilder(tabItem.id, tabItem.navData); 113 } 114 }) 115 } 116 .id('MainList') 117 .margin({ top: 3 } as Margin) 118 .height(38) 119 .listDirection(Axis.Horizontal) 120 .padding({ right: 46 } as Padding) 121 .scrollBar(BarState.Off) 122 Row() { 123 Row() { 124 Image($r('app.media.ic_public_more')) 125 .width(20) 126 .id('mainPageTabsImage') 127 } 128 .bindMenu(this.tabsMenu) 129 .justifyContent(FlexAlign.Center) 130 .width(43) 131 .height(43) 132 .borderRadius(100) 133 .backgroundColor('rgba(216, 216, 216, 0)') 134 .id('menu_button') 135 } 136 .linearGradient({ 137 angle: 90, 138 colors: [['rgba(241, 241, 241, 0)', 0], ['#F1F3F5', 0.2], ['#F1F3F5', 1]] 139 } as LinearGradientOptions) 140 .justifyContent(FlexAlign.End) 141 .width(60) 142 .height(43) 143 } 144 .alignContent(Alignment.TopEnd) 145 } 146 .padding({ 147 left: 13, 148 right: 13 149 } as Padding) 150 .margin({ top: 8 } as Margin) 151 Tabs({ controller: this.tabsController } as TabsOptions) { 152 ForEach(TAB_DATA, (tabItem: TabDataModel) => { 153 TabContent() { 154 if (tabItem.navData === '全部') { 155 List() { 156 LazyForEach(this.dataSource, (waterFlowItem: SceneModuleInfo) => { 157 ListItem(){ 158 methodPoints({ listData: waterFlowItem }) 159 } 160 }, (waterFlowItem: SceneModuleInfo) => JSON.stringify(waterFlowItem)) 161 } 162 .nestedScroll({ 163 scrollForward: NestedScrollMode.PARENT_FIRST, 164 scrollBackward: NestedScrollMode.SELF_FIRST 165 } as NestedScrollOptions) 166 .width('100%') 167 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 168 .padding({ bottom: $r('app.integer.functional_scenes_water_flow_padding_bottom') } as Padding) 169 } else { 170 Column() { 171 Text(tabItem.navData).fontSize(20) 172 } 173 .height('100%') 174 .width('100%') 175 .justifyContent(FlexAlign.Center) 176 .align(Alignment.Center) 177 } 178 } 179 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 180 .align(Alignment.TopStart) 181 .alignSelf(ItemAlign.Start) 182 }) 183 } 184 .margin({ top: 8 } as Margin) 185 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 186 .padding({ 187 left: 13, 188 right: 13 189 } as Padding) 190 .barWidth(0) 191 .barHeight(0) 192 .onAnimationStart((index: number, targetIndex: number, extraInfo: TabsAnimationEvent) => { 193 this.tabsIndex = targetIndex; 194 this.scrollController.scrollToIndex(targetIndex, true, ScrollAlign.START); 195 } as OnTabsAnimationStartCallback) 196 } 197 .height('100%') 198 .backgroundColor('#F1F1F1') 199 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) 200 } 201} 202 203/** 204 * 瀑布流列表项组件布局 205 * 206 * @param listData 组件列表信息 207 */ 208// TODO:知识点: 209// 1.@Reusable标识自定义组件具备可复用的能力,它可以被添加到任意的自定义组件上。 210// 2.复用自定义组件时避免一切可能改变自定义组件的组件树结构和可能使可复用组件中产生重新布局的操作以将组件复用的性能提升到最高。 211 212@Reusable 213@Component 214struct methodPoints { 215 @State listData: SceneModuleInfo = 216 new SceneModuleInfo($r('app.media.functional_scenes_address_exchange'), '地址交换动画', 217 'addressexchange/AddressExchangeView', '动效', 1); 218 @State helperUrl: string = 'about://blank'; 219 @State screenW: number = px2vp(display.getDefaultDisplaySync().width); 220 @State isNeedClear: boolean = false; 221 private deviceSize: number = 600; // 依据Navigation的mode属性说明,如使用Auto,窗口宽度>=600vp时,采用Split模式显示;窗口宽度<600vp时,采用Stack模式显示。 222 // 当前屏幕折叠态(仅折叠屏设备下有效) 223 curFoldStatus: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN; 224 // 从AppStorage中获取设别类别,判断是否为折叠屏 225 isFoldable: boolean | undefined = AppStorage.get('isFoldable'); 226 @State @Watch('onShowReadMeChange') isShowReadMe: boolean = false; 227 228 aboutToAppear(): void { 229 if (display.isFoldable()) { 230 this.regDisplayListener(); 231 } else { 232 if (this.screenW >= this.deviceSize) { 233 this.isNeedClear = true; 234 } else { 235 this.isNeedClear = false; 236 } 237 } 238 } 239 240 /** 241 * 组件的生命周期回调,在可复用组件从复用缓存中加入到组件树之前调用 242 * @param params:组件更新时所需参数 243 */ 244 aboutToReuse(params: Record<string, SceneModuleInfo>): void { 245 const listData = params['listData']; 246 if (listData) { 247 this.listData = listData as SceneModuleInfo; 248 } 249 } 250 251 /** 252 * 注册屏幕状态监听 (仅限折叠屏) 253 * @returns {void} 254 */ 255 regDisplayListener(): void { 256 this.changeNeedClear(display.getFoldStatus()); 257 display.on('foldStatusChange', (curFoldStatus: display.FoldStatus) => { 258 // 同一个状态重复触发不做处理 259 if (this.curFoldStatus === curFoldStatus) { 260 return; 261 } 262 // 缓存当前折叠状态 263 this.curFoldStatus = curFoldStatus; 264 this.changeNeedClear(this.curFoldStatus); 265 }) 266 } 267 268 changeNeedClear(status: number): void { 269 if (status === display.FoldStatus.FOLD_STATUS_FOLDED) { 270 this.isNeedClear = false; 271 } else { 272 this.isNeedClear = true; 273 } 274 } 275 276 changeHelpUrl(): void { 277 this.helperUrl = this.listData.helperUrl; 278 } 279 280 onShowReadMeChange(text: string): void { 281 if (!this.isShowReadMe) { 282 283 } 284 } 285 286 // 帮助功能:半模态弹窗显示对应案例README 287 @Builder 288 buildReadMeSheet(): void { 289 Column() { 290 Row() { 291 Row() { 292 Text(this.listData.name) 293 .textOverflow({ overflow: TextOverflow.Clip } as TextOverflowOptions) 294 .fontColor(Color.White) 295 .fontWeight(700) 296 .fontSize($r('app.integer.nav_destination_title_text_size')) 297 } 298 .width($r('app.integer.readme_sheet_text_size')) 299 300 Column() { 301 Stack() { 302 Column() { 303 } 304 .width($r('app.integer.readme_sheet_size')) 305 .height($r('app.integer.readme_sheet_size')) 306 .borderRadius($r('app.integer.nav_destination_title_image_border_radius')) 307 .backgroundColor(Color.White) 308 .opacity(0.05) 309 Image($r('app.media.ic_public_cancel')) 310 .fillColor(Color.White) 311 .width($r('app.integer.readme_sheet_cancel_image_width')) 312 } 313 } 314 .onClick((e: ClickEvent) => { 315 this.isShowReadMe = false; 316 }) 317 .justifyContent(FlexAlign.Center) 318 .width($r('app.integer.readme_sheet_size')) 319 .height($r('app.integer.readme_sheet_size')) 320 .borderRadius($r('app.integer.nav_destination_title_image_border_radius')) 321 } 322 .padding({ left: $r('app.integer.readme_sheet_padding'), right: $r('app.integer.readme_sheet_padding') } as Padding) 323 .margin({ top: $r('app.integer.readme_sheet_margin')} as Margin) 324 .justifyContent(FlexAlign.SpaceBetween) 325 .width('100%') 326 } 327 .width('100%') 328 .height('100%') 329 } 330 331 build() { 332 Column() { 333 Image($r('app.media.background_pic_3')) 334 .borderRadius({ 335 topLeft: 8, 336 topRight: 8, 337 bottomLeft: 0, 338 bottomRight: 0 339 } as BorderRadiuses) 340 .objectFit(ImageFit.Contain) 341 .width('100%') 342 343 Text(this.listData?.serialNumber?.toString() + '. ' + this.listData.name) 344 .padding({ 345 left: 10, 346 right: 10 347 } as Padding) 348 .width('100%') 349 .fontColor(Color.Black) 350 .textAlign(TextAlign.Start) 351 .maxLines(2) 352 .fontSize(14) 353 .margin({ 354 top: 10, 355 bottom: 10 356 } as Margin) 357 .textOverflow({ overflow: TextOverflow.Ellipsis } as TextOverflowOptions) 358 359 Row() { 360 Button($r('app.string.functional_scenes_readme')) 361 .fontSize(12) 362 .fontColor(Color.White) 363 .height(25) 364 .width(100) 365 .margin({ left: 6, right: 10 } as Margin) 366 .gesture( 367 GestureGroup( 368 GestureMode.Exclusive, 369 TapGesture({ fingers: 1, count: 1 }) 370 .onAction(() => { 371 this.getUIContext().getRouter().pushUrl({url: 'pages/collapsemenu/Concent'}) 372 }) 373 ) 374 ) 375 376 Text($r('app.string.functional_scenes_difficulty')) 377 .fontColor(Color.Black) 378 .opacity(0.6) 379 .textAlign(TextAlign.Start) 380 .maxLines(1) 381 .height(18) 382 .fontSize(12) 383 .width(25) 384 385 Rating({ 386 rating: this.listData.ratingNumber, 387 indicator: true 388 } as RatingOptions) 389 .stars(5) 390 .width(70) 391 } 392 .margin({ bottom: 10 } as Margin) 393 .width('100%') 394 .justifyContent(FlexAlign.Start) 395 } 396 .shadow(ShadowStyle.OUTER_DEFAULT_XS) 397 .backgroundColor(Color.White) 398 .width('100%') 399 .borderRadius(8) 400 .margin({ 401 top: 4, 402 bottom: 4 403 } as Margin) 404 .onClick((e: ClickEvent) => { 405 }) 406 } 407} 408 409