| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- ---
- description: UI组件开发模式与最佳实践
- globs: ["**/pages/**/*.ets", "**/view/**/*.ets", "**/dialog/**/*.ets"]
- alwaysApply: false
- ---
- # UI组件开发模式与最佳实践
- ## 页面组件结构
- ### 基本页面模板
- ```typescript
- import { CommonConstants } from '../common/constants/CommonConstants';
- import Logger from '../common/util/Logger';
- @Entry
- @Component
- struct PageName {
- // 状态变量
- @State isLoading: boolean = false;
- @State dataList: Array<ItemType> = [];
-
- // 上下文
- private context = getContext(this);
-
- // 生命周期
- aboutToAppear() {
- this.initData();
- }
-
- aboutToDisappear() {
- this.cleanup();
- }
-
- // 初始化数据
- private initData() {
- // 初始化逻辑
- }
-
- // 清理资源
- private cleanup() {
- // 清理逻辑
- }
-
- // 构建方法
- build() {
- Column() {
- // 页面内容
- }
- .width('100%')
- .height('100%')
- .backgroundColor($r('app.color.backgroundPrimary'))
- }
- }
- ```
- ### 导航栏组件
- ```typescript
- @Builder
- NavigationArea(title: string, showBack: boolean = true) {
- Row() {
- if (showBack) {
- Image($r('app.media.ic_back'))
- .width(24)
- .height(24)
- .margin({ left: 16 })
- .onClick(() => {
- router.back();
- })
- }
-
- Text(title)
- .fontSize(18)
- .fontWeight(FontWeight.Medium)
- .fontColor($r('app.color.fontPrimary'))
- .layoutWeight(1)
- .textAlign(TextAlign.Center)
- .margin({ right: showBack ? 40 : 16 })
- }
- .width('100%')
- .height(56)
- .backgroundColor($r('app.color.backgroundPrimary'))
- }
- ```
- ## 列表组件
- ### 基本列表组件
- ```typescript
- @Component
- struct MusicListItem {
- @Prop musicItem: MusicItem;
- @Prop isPlaying: boolean = false;
- private onItemClick?: (item: MusicItem) => void;
-
- build() {
- Row() {
- Image(this.musicItem.cover || $r('app.media.default_music_icon'))
- .width(50)
- .height(50)
- .borderRadius(8)
- .objectFit(ImageFit.Cover)
- .margin({ right: 12 })
-
- Column() {
- Text(this.musicItem.title)
- .fontSize(16)
- .fontColor($r('app.color.fontPrimary'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
-
- Text(this.musicItem.artist)
- .fontSize(14)
- .fontColor($r('app.color.fontSecondary'))
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .width('100%')
- .margin({ top: 4 })
- }
- .layoutWeight(1)
- .alignItems(HorizontalAlign.Start)
-
- if (this.isPlaying) {
- Image($r('app.media.ic_playing'))
- .width(24)
- .height(24)
- .margin({ left: 12 })
- }
- }
- .width('100%')
- .height(70)
- .padding({ horizontal: 16, vertical: 10 })
- .onClick(() => {
- if (this.onItemClick) {
- this.onItemClick(this.musicItem);
- }
- })
- }
- }
- ```
- ### 高性能列表
- ```typescript
- @Component
- struct MusicList {
- @State musicList: MusicItem[] = [];
- @State currentPlayingId: string = '';
-
- build() {
- List({ space: 1 }) {
- LazyForEach(new MusicDataSource(this.musicList), (item: MusicItem, index: number) => {
- ListItem() {
- MusicListItem({
- musicItem: item,
- isPlaying: item.id === this.currentPlayingId,
- onItemClick: (musicItem: MusicItem) => {
- this.playMusic(musicItem);
- }
- })
- }
- }, (item: MusicItem) => item.id)
- }
- .width('100%')
- .layoutWeight(1)
- .divider({ strokeWidth: 1, color: $r('app.color.compDivider') })
- }
-
- private playMusic(musicItem: MusicItem) {
- this.currentPlayingId = musicItem.id;
- // 播放音乐逻辑
- }
- }
- // 数据源类
- class MusicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private data: MusicItem[] = [];
-
- constructor(data: MusicItem[]) {
- this.data = data;
- }
-
- totalCount(): number {
- return this.data.length;
- }
-
- getData(index: number): MusicItem {
- return this.data[index];
- }
-
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- this.listeners.push(listener);
- }
- }
-
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- this.listeners.splice(pos, 1);
- }
- }
-
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- });
- }
- }
- ```
- ## 播放控制组件
- ### 播放控制栏
- ```typescript
- @Component
- struct PlayerControlBar {
- @Prop isPlaying: boolean = false;
- @Prop currentTime: number = 0;
- @Prop duration: number = 0;
- private onPlayPause?: () => void;
- private onPrevious?: () => void;
- private onNext?: () => void;
- private onSeek?: (position: number) => void;
-
- build() {
- Column() {
- // 进度条
- Row() {
- Text(this.formatTime(this.currentTime))
- .fontSize(12)
- .fontColor($r('app.color.fontSecondary'))
-
- Slider({
- value: this.currentTime,
- min: 0,
- max: this.duration || 1,
- style: SliderStyle.InSet
- })
- .layoutWeight(1)
- .margin({ horizontal: 12 })
- .trackColor($r('app.color.compBackgroundTertiary'))
- .selectedColor($r('app.color.brand'))
- .blockColor($r('app.color.brand'))
- .onChange((value: number) => {
- if (this.onSeek) {
- this.onSeek(value);
- }
- })
-
- Text(this.formatTime(this.duration))
- .fontSize(12)
- .fontColor($r('app.color.fontSecondary'))
- }
- .width('100%')
- .margin({ bottom: 20 })
-
- // 控制按钮
- Row() {
- Button() {
- Image($r('app.media.ic_previous'))
- .width(28)
- .height(28)
- .fillColor($r('app.color.iconPrimary'))
- }
- .type(ButtonType.Circle)
- .backgroundColor(Color.Transparent)
- .width(48)
- .height(48)
- .onClick(() => {
- if (this.onPrevious) {
- this.onPrevious();
- }
- })
-
- Button() {
- Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
- .width(36)
- .height(36)
- .fillColor($r('app.color.iconOnPrimary'))
- }
- .type(ButtonType.Circle)
- .backgroundColor($r('app.color.brand'))
- .width(64)
- .height(64)
- .margin({ horizontal: 20 })
- .onClick(() => {
- if (this.onPlayPause) {
- this.onPlayPause();
- }
- })
-
- Button() {
- Image($r('app.media.ic_next'))
- .width(28)
- .height(28)
- .fillColor($r('app.color.iconPrimary'))
- }
- .type(ButtonType.Circle)
- .backgroundColor(Color.Transparent)
- .width(48)
- .height(48)
- .onClick(() => {
- if (this.onNext) {
- this.onNext();
- }
- })
- }
- .width('100%')
- .justifyContent(FlexAlign.Center)
- }
- .width('100%')
- .padding({ horizontal: 20, vertical: 16 })
- }
-
- private formatTime(time: number): string {
- if (isNaN(time) || time < 0) return '00:00';
-
- const minutes = Math.floor(time / 60);
- const seconds = Math.floor(time % 60);
- return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
- }
- }
- ```
- ## 对话框组件
- ### 自定义对话框
- ```typescript
- @Component
- struct CustomDialog {
- @Prop title: string = '';
- @Prop content: string = '';
- @Prop confirmText: string = '确定';
- @Prop cancelText: string = '取消';
- @Prop showCancel: boolean = true;
- private onConfirm?: () => void;
- private onCancel?: () => void;
-
- build() {
- Column() {
- // 标题
- Text(this.title)
- .fontSize(18)
- .fontWeight(FontWeight.Medium)
- .fontColor($r('app.color.fontPrimary'))
- .margin({ top: 24, bottom: 12 })
- .padding({ horizontal: 20 })
-
- // 内容
- Text(this.content)
- .fontSize(14)
- .fontColor($r('app.color.fontSecondary'))
- .textAlign(TextAlign.Center)
- .padding({ horizontal: 20 })
- .margin({ bottom: 24 })
-
- // 按钮
- Row() {
- if (this.showCancel) {
- Button(this.cancelText)
- .fontSize(16)
- .fontColor($r('app.color.fontPrimary'))
- .backgroundColor(Color.Transparent)
- .layoutWeight(1)
- .onClick(() => {
- if (this.onCancel) {
- this.onCancel();
- }
- })
- }
-
- Button(this.confirmText)
- .fontSize(16)
- .fontColor($r('app.color.brand'))
- .backgroundColor(Color.Transparent)
- .layoutWeight(1)
- .onClick(() => {
- if (this.onConfirm) {
- this.onConfirm();
- }
- })
- }
- .width('100%')
- .height(48)
- }
- .backgroundColor($r('app.color.backgroundPrimary'))
- .borderRadius(12)
- .width('80%')
- }
- }
- ```
- ## 主题适配
- ### 主题感知组件
- ```typescript
- @Component
- struct ThemeAwareButton {
- @Prop text: string = '';
- @Prop type: 'primary' | 'secondary' = 'primary';
- private onClick?: () => void;
-
- build() {
- Button(this.text)
- .fontSize(16)
- .fontColor(this.type === 'primary' ?
- $r('app.color.fontOnPrimary') :
- $r('app.color.fontPrimary'))
- .backgroundColor(this.type === 'primary' ?
- $r('app.color.brand') :
- $r('app.color.compBackgroundSecondary'))
- .borderRadius(8)
- .padding({ horizontal: 20, vertical: 10 })
- .onClick(() => {
- if (this.onClick) {
- this.onClick();
- }
- })
- }
- }
- ```
- ## 动画效果
- ### 页面转场动画
- ```typescript
- // 页面跳转带动画
- router.pushUrl({
- url: 'pages/DetailPage',
- params: { id: this.itemId }
- }).then(() => {
- // 页面跳转成功
- }).catch((err: Error) => {
- Logger.error(`页面跳转失败: ${err.message}`);
- });
- // 在目标页面中
- @Entry
- @Component
- struct DetailPage {
- // 页面转场动画
- pageTransition() {
- PageTransitionEnter({ duration: 300, curve: Curve.EaseInOut })
- .slide(SlideEffect.Right)
-
- PageTransitionExit({ duration: 300, curve: Curve.EaseInOut })
- .slide(SlideEffect.Left)
- }
-
- build() {
- // 页面内容
- }
- }
- ```
- ### 状态变化动画
- ```typescript
- @Component
- struct AnimatedButton {
- @State isPressed: boolean = false;
-
- build() {
- Button('点击我')
- .scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 })
- .animation({
- duration: 100,
- curve: Curve.EaseInOut
- })
- .onTouch((event: TouchEvent) => {
- if (event.type === TouchType.Down) {
- this.isPressed = true;
- } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
- this.isPressed = false;
- }
- })
- }
- }
- ```
- ## 响应式布局
- ### 断点适配
- ```typescript
- @Component
- struct ResponsiveLayout {
- @State currentBreakpoint: string = 'sm';
-
- aboutToAppear() {
- // 监听窗口大小变化
- window.getLastWindow(getContext(this))
- .then((windowClass) => {
- windowClass.on('windowSizeChange', (windowSize) => {
- this.updateBreakpoint(windowSize.width);
- });
- this.updateBreakpoint(windowClass.getWindowProperties().windowRect.width);
- });
- }
-
- private updateBreakpoint(width: number) {
- if (width < 600) {
- this.currentBreakpoint = 'sm';
- } else if (width < 840) {
- this.currentBreakpoint = 'md';
- } else {
- this.currentBreakpoint = 'lg';
- }
- }
-
- build() {
- if (this.currentBreakpoint === 'sm') {
- // 小屏幕布局
- this.buildSmallLayout();
- } else if (this.currentBreakpoint === 'md') {
- // 中等屏幕布局
- this.buildMediumLayout();
- } else {
- // 大屏幕布局
- this.buildLargeLayout();
- }
- }
-
- @Builder
- buildSmallLayout() {
- Column() {
- // 小屏幕布局内容
- }
- }
-
- @Builder
- buildMediumLayout() {
- Row() {
- // 中等屏幕布局内容
- }
- }
-
- @Builder
- buildLargeLayout() {
- Grid() {
- // 大屏幕布局内容
- }
- }
- }
- ```
|