文档管理中心
您当前正在浏览新版开发者文档中心,目录分类和层级有所调整。点击左侧当前文档分类名称前的“☰”图标,可切换文档分类。 了解新版目录
问题处理与运维开发与测试问题应用框架开发UI框架UI界面如何实现全局层级的自定义UI控件

如何实现全局层级的自定义UI控件

问题现象

如何开放UI实现到组件外部,由外部自由定制一个或多个封装好的UI内容,使其可以被跨组件或跨页面调用?

背景知识

  • @Builder:@Builder装饰器用于封装可复用的UI结构,通过提取重复的布局代码提高开发效率。
  • wrapBuilder:当在一个struct内使用多个全局@Builder函数实现UI的不同效果时,代码维护将变得非常困难,且页面不够整洁。此时,可以使用wrapBuilder封装全局@Builder。

解决方案

实现思路如下:UI内容由全局@Builder自定义构建函数进行封装。通过注册表注册一个WrappedBuilder数组来管理UI,在组件中通过ForEach的方式渲染这个WrappedBuilder数组,这样可以通过管理该WrappedBuilder数组来动态渲染定制的UI内容。

  1. 采用单例模式管理全局UI组件注册表(EllaBookUIView)。
    • 通过IBookUIComponent(书籍UI组件接口)规范组件实现,通过getUIBuilder获取组件构建器实现解耦。
      收起
      自动换行
      深色代码主题
      复制
      1. export interface IBookUIComponent {
      2. // 获取UI构建器
      3. getUIBuilder: WrappedBuilder<[]>;
      4. // 组件名称
      5. componentName?: string;
      6. }
    • 通过全局UI组件注册表(EllaBookUIView)实现动态扩展组件注册机制。
      收起
      自动换行
      深色代码主题
      复制
      1. export class EllaBookUIView {
      2. private static instance: EllaBookUIView | null = null;
      3. private components: IBookUIComponent[] = [];
      4. private constructor() {
      5. }
      6. // 获取单例
      7. public static getInstance(): EllaBookUIView {
      8. if (!EllaBookUIView.instance) {
      9. EllaBookUIView.instance = new EllaBookUIView();
      10. }
      11. return EllaBookUIView.instance;
      12. }
      13. // 注册组件
      14. public registerComponent(component: IBookUIComponent): void {
      15. this.components.push(component);
      16. console.info(`[BookUIRegistry] 组件注册成功: ${component.componentName || '未命名组件'}`);
      17. }
      18. // 获取所有组件构建器
      19. public getAllBuilders(): WrappedBuilder<[]>[] {
      20. return this.components.map(component => component.getUIBuilder);
      21. }
      22. // 清除所有组件
      23. public clearComponents(): void {
      24. this.components = [];
      25. console.info('[BookUIRegistry] 所有组件已清除');
      26. }
      27. }
  2. 控制组件实现:ControlButtons结构体实现书籍基础控制功能。
    • 顶部控制栏:退出按钮+播放/暂停按钮。
    • 底部控制栏:翻页按钮组。
      收起
      自动换行
      深色代码主题
      复制
      1. @Component
      2. struct ControlButtons {
      3. @State isPlaying: boolean = true; // 播放状态
      4. // 退出书籍
      5. private exitBook() {
      6. console.info('退出书籍');
      7. }
      8. // 切换播放状态
      9. private togglePlay() {
      10. this.isPlaying = !this.isPlaying;
      11. console.info(this.isPlaying ? '播放中' : '已暂停');
      12. }
      13. // 上一页
      14. private prevPage() {
      15. console.info('上一页');
      16. }
      17. // 下一页
      18. private nextPage() {
      19. console.info('下一页');
      20. }
      21. build() {
      22. Stack({ alignContent: Alignment.BottomStart }) {
      23. // 顶部控制栏
      24. Row() {
      25. // 1.顶部最左侧---退出按钮
      26. Button() {
      27. Image($r('app.media.xmark'))
      28. .objectFit(ImageFit.Contain);
      29. }
      30. .height(24)
      31. .width(24)
      32. .backgroundColor(Color.Transparent)
      33. .borderRadius(20)
      34. .onClick(() => {
      35. // 退出逻辑
      36. this.exitBook();
      37. });
      38. Blank();
      39. // 2.顶部最右侧---播放/暂停按钮
      40. Button() {
      41. if (this.isPlaying) {
      42. Image($r('app.media.pause'))
      43. .objectFit(ImageFit.Contain);
      44. } else {
      45. Image($r('app.media.play'))
      46. .objectFit(ImageFit.Contain);
      47. }
      48. }
      49. .height(24)
      50. .width(24)
      51. .backgroundColor(Color.Transparent)
      52. .borderRadius(20)
      53. .onClick(() => {
      54. this.togglePlay();
      55. });
      56. }
      57. .width('100%')
      58. .padding(10)
      59. .position({ x: 0, y: 0 });
      60. // 底部控制栏
      61. Row() {
      62. // 3.底部最左侧---上一页按钮
      63. Button() {
      64. Image($r('app.media.chevron_left'))
      65. .objectFit(ImageFit.Contain);
      66. }
      67. .height(24)
      68. .width(24)
      69. .backgroundColor(Color.Transparent)
      70. .borderRadius(25)
      71. .onClick(() => {
      72. this.prevPage();
      73. });
      74. Blank();
      75. // 4.底部最右侧---下一页按钮
      76. Button() {
      77. Image($r('app.media.chevron_right'))
      78. .objectFit(ImageFit.Contain);
      79. }
      80. .height(24)
      81. .width(24)
      82. .backgroundColor(Color.Transparent)
      83. .borderRadius(25)
      84. .onClick(() => {
      85. this.nextPage();
      86. });
      87. }
      88. .width('100%')
      89. .padding(10);
      90. }
      91. .width('100%')
      92. .height('100%');
      93. };
      94. }
      95. // 创建UI构建器
      96. @Builder
      97. function controlBuilder() {
      98. ControlButtons();
      99. }
  3. BookUIComponents类实现接口规范,将构建器包装为可注册单元。
    收起
    自动换行
    深色代码主题
    复制
    1. export class BookUIComponents implements IBookUIComponent {
    2. componentName: string = '书籍控制组件';
    3. getUIBuilder: WrappedBuilder<[]> = wrapBuilder(controlBuilder);
    4. }
  4. 页面调用:通过ForEach动态渲染注册的构建器,BuilderComponent作为通用容器解析并执行构建器逻辑。
    收起
    自动换行
    深色代码主题
    复制
    1. Stack({ alignContent: Alignment.TopStart }) {
    2. Column({ space: 16 }) {
    3. Button('打开书籍控制器控件')
    4. .onClick(() => {
    5. ellaBookUIView.registerComponent(new BookUIComponents());
    6. this.uiBuilders = ellaBookUIView.getAllBuilders();
    7. });
    8. Button('关闭书籍控制器控件')
    9. .onClick(() => {
    10. ellaBookUIView.clearComponents();
    11. this.uiBuilders = ellaBookUIView.getAllBuilders();
    12. });
    13. }.height('100%').width('100%');
    14. ForEach(this.uiBuilders,
    15. (builder: WrappedBuilder<[]>) => {
    16. // 使用公共属性传递索引
    17. BuilderComponent({ builder: builder })
    18. .hitTestBehavior(HitTestMode.Transparent);
    19. }
    20. );
    21. };

完整示例如下:

收起
自动换行
深色代码主题
复制
  1. // 书籍UI组件接口
  2. export interface IBookUIComponent {
  3. // 获取UI构建器
  4. getUIBuilder: WrappedBuilder<[]>;
  5. // 组件名称
  6. componentName?: string;
  7. }
  8. // 全局UI组件注册表
  9. export class EllaBookUIView {
  10. private static instance: EllaBookUIView | null = null;
  11. private components: IBookUIComponent[] = [];
  12. private constructor() {
  13. }
  14. // 获取单例
  15. public static getInstance(): EllaBookUIView {
  16. if (!EllaBookUIView.instance) {
  17. EllaBookUIView.instance = new EllaBookUIView();
  18. }
  19. return EllaBookUIView.instance;
  20. }
  21. // 注册组件
  22. public registerComponent(component: IBookUIComponent): void {
  23. this.components.push(component);
  24. console.info(`[BookUIRegistry] 组件注册成功: ${component.componentName || '未命名组件'}`);
  25. }
  26. // 获取所有组件构建器
  27. public getAllBuilders(): WrappedBuilder<[]>[] {
  28. return this.components.map(component => component.getUIBuilder);
  29. }
  30. // 清除所有组件
  31. public clearComponents(): void {
  32. this.components = [];
  33. console.info('[BookUIRegistry] 所有组件已清除');
  34. }
  35. }
  36. // 导出全局单例
  37. export const ellaBookUIView = EllaBookUIView.getInstance();
  38. @Component
  39. struct ControlButtons {
  40. @State isPlaying: boolean = true; // 播放状态
  41. // 退出书籍
  42. private exitBook() {
  43. console.info('退出书籍');
  44. }
  45. // 切换播放状态
  46. private togglePlay() {
  47. this.isPlaying = !this.isPlaying;
  48. console.info(this.isPlaying ? '播放中' : '已暂停');
  49. }
  50. // 上一页
  51. private prevPage() {
  52. console.info('上一页');
  53. }
  54. // 下一页
  55. private nextPage() {
  56. console.info('下一页');
  57. }
  58. build() {
  59. Stack({ alignContent: Alignment.BottomStart }) {
  60. // 顶部控制栏
  61. Row() {
  62. // 1.顶部最左侧---退出按钮
  63. Button() {
  64. Image($r('app.media.xmark'))
  65. .objectFit(ImageFit.Contain);
  66. }
  67. .height(24)
  68. .width(24)
  69. .backgroundColor(Color.Transparent)
  70. .borderRadius(20)
  71. .onClick(() => {
  72. // 退出逻辑
  73. this.exitBook();
  74. });
  75. Blank();
  76. // 2.顶部最右侧---播放/暂停按钮
  77. Button() {
  78. if (this.isPlaying) {
  79. Image($r('app.media.pause'))
  80. .objectFit(ImageFit.Contain);
  81. } else {
  82. Image($r('app.media.play'))
  83. .objectFit(ImageFit.Contain);
  84. }
  85. }
  86. .height(24)
  87. .width(24)
  88. .backgroundColor(Color.Transparent)
  89. .borderRadius(20)
  90. .onClick(() => {
  91. this.togglePlay();
  92. });
  93. }
  94. .width('100%')
  95. .padding(10)
  96. .position({ x: 0, y: 0 });
  97. // 底部控制栏
  98. Row() {
  99. // 3.底部最左侧---上一页按钮
  100. Button() {
  101. Image($r('app.media.chevron_left'))
  102. .objectFit(ImageFit.Contain);
  103. }
  104. .height(24)
  105. .width(24)
  106. .backgroundColor(Color.Transparent)
  107. .borderRadius(25)
  108. .onClick(() => {
  109. this.prevPage();
  110. });
  111. Blank();
  112. // 4.底部最右侧---下一页按钮
  113. Button() {
  114. Image($r('app.media.chevron_right'))
  115. .objectFit(ImageFit.Contain);
  116. }
  117. .height(24)
  118. .width(24)
  119. .backgroundColor(Color.Transparent)
  120. .borderRadius(25)
  121. .onClick(() => {
  122. this.nextPage();
  123. });
  124. }
  125. .width('100%')
  126. .padding(10);
  127. }
  128. .width('100%')
  129. .height('100%');
  130. };
  131. }
  132. // 创建UI构建器
  133. @Builder
  134. function controlBuilder() {
  135. ControlButtons();
  136. }
  137. export class BookUIComponents implements IBookUIComponent {
  138. componentName: string = '书籍控制组件';
  139. getUIBuilder: WrappedBuilder<[]> = wrapBuilder(controlBuilder);
  140. }
  141. @Entry
  142. @Component
  143. struct Page {
  144. @State uiBuilders: WrappedBuilder<[]>[] = [];
  145. build() {
  146. Column() {
  147. Stack({ alignContent: Alignment.TopStart }) {
  148. Column({ space: 16 }) {
  149. Button('打开书籍控制器控件')
  150. .onClick(() => {
  151. ellaBookUIView.registerComponent(new BookUIComponents());
  152. this.uiBuilders = ellaBookUIView.getAllBuilders();
  153. });
  154. Button('关闭书籍控制器控件')
  155. .onClick(() => {
  156. ellaBookUIView.clearComponents();
  157. this.uiBuilders = ellaBookUIView.getAllBuilders();
  158. });
  159. }.height('100%').width('100%');
  160. ForEach(this.uiBuilders,
  161. (builder: WrappedBuilder<[]>) => {
  162. // 使用公共属性传递索引
  163. BuilderComponent({ builder: builder })
  164. .hitTestBehavior(HitTestMode.Transparent);
  165. }
  166. );
  167. };
  168. };
  169. }
  170. }
  171. @Component
  172. struct BuilderComponent {
  173. builder?: WrappedBuilder<[]>;
  174. build() {
  175. if (this.builder) {
  176. this.builder.builder();
  177. } else {
  178. Text('暂无构建');
  179. }
  180. }
  181. }

实现效果如下: