FormLayoutManager.ets 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import { WidgetSize, WidgetData, FormattedWidgetData, WidgetConfig, ContainerPadding, ButtonSize, FontSizeConfig, SpacingConfig, ShowElementsConfig, ResponsiveLayoutParams } from './WidgetTypes';
  2. import hilog from '@ohos.hilog';
  3. const TAG = 'FormLayoutManager';
  4. /**
  5. * 卡片布局管理器
  6. * 负责处理多尺寸卡片的布局适配和UI重构
  7. * 需求: 5.4, 5.5
  8. */
  9. export class FormLayoutManager {
  10. private static instance: FormLayoutManager;
  11. /**
  12. * 获取单例实例
  13. */
  14. public static getInstance(): FormLayoutManager {
  15. if (!FormLayoutManager.instance) {
  16. FormLayoutManager.instance = new FormLayoutManager();
  17. }
  18. return FormLayoutManager.instance;
  19. }
  20. private constructor() {
  21. hilog.info(0x0000, TAG, 'FormLayoutManager initialized');
  22. }
  23. /**
  24. * 根据卡片尺寸适配数据
  25. * @param widgetData 原始卡片数据
  26. * @param size 卡片尺寸
  27. * @returns 适配后的格式化数据
  28. */
  29. public adaptDataForSize(widgetData: WidgetData, size: WidgetSize): FormattedWidgetData {
  30. hilog.info(0x0000, TAG, `Adapting data for size: ${size}`);
  31. const baseData = this.formatBaseData(widgetData);
  32. switch (size) {
  33. case WidgetSize.SMALL:
  34. return this.adaptForSmallWidget(baseData, widgetData);
  35. case WidgetSize.MEDIUM:
  36. return this.adaptForMediumWidget(baseData, widgetData);
  37. case WidgetSize.LARGE:
  38. return this.adaptForLargeWidget(baseData, widgetData);
  39. default:
  40. hilog.warn(0x0000, TAG, `Unknown widget size: ${size}, using medium as default`);
  41. return this.adaptForMediumWidget(baseData, widgetData);
  42. }
  43. }
  44. /**
  45. * 获取卡片尺寸对应的页面路径
  46. * @param size 卡片尺寸
  47. * @returns 页面路径
  48. */
  49. public getWidgetPagePath(size: WidgetSize): string {
  50. switch (size) {
  51. case WidgetSize.SMALL:
  52. return 'widget/pages/PlayerWidgetSmall';
  53. case WidgetSize.MEDIUM:
  54. return 'widget/pages/PlayerWidgetMedium';
  55. case WidgetSize.LARGE:
  56. return 'widget/pages/PlayerWidgetLarge';
  57. default:
  58. hilog.warn(0x0000, TAG, `Unknown widget size: ${size}, using medium as default`);
  59. return 'widget/pages/PlayerWidgetMedium';
  60. }
  61. }
  62. /**
  63. * 检查尺寸变化并返回是否需要重构UI
  64. * @param oldSize 旧尺寸
  65. * @param newSize 新尺寸
  66. * @returns 是否需要重构UI
  67. */
  68. public shouldRebuildUI(oldSize: WidgetSize, newSize: WidgetSize): boolean {
  69. const needsRebuild = oldSize !== newSize;
  70. hilog.info(0x0000, TAG, `Size change from ${oldSize} to ${newSize}, needs rebuild: ${needsRebuild}`);
  71. return needsRebuild;
  72. }
  73. /**
  74. * 获取卡片尺寸的显示配置
  75. * @param size 卡片尺寸
  76. * @returns 显示配置
  77. */
  78. public getSizeDisplayConfig(size: WidgetSize): WidgetConfig {
  79. const baseConfig: WidgetConfig = {
  80. size: size,
  81. theme: 'auto',
  82. showProgress: true,
  83. showCover: true
  84. };
  85. switch (size) {
  86. case WidgetSize.SMALL:
  87. const smallConfig: WidgetConfig = {
  88. size: baseConfig.size,
  89. theme: baseConfig.theme,
  90. showProgress: false,
  91. showCover: false
  92. };
  93. return smallConfig;
  94. case WidgetSize.MEDIUM:
  95. const mediumConfig: WidgetConfig = {
  96. size: baseConfig.size,
  97. theme: baseConfig.theme,
  98. showProgress: true,
  99. showCover: false
  100. };
  101. return mediumConfig;
  102. case WidgetSize.LARGE:
  103. const largeConfig: WidgetConfig = {
  104. size: baseConfig.size,
  105. theme: baseConfig.theme,
  106. showProgress: true,
  107. showCover: true
  108. };
  109. return largeConfig;
  110. default:
  111. return baseConfig;
  112. }
  113. }
  114. /**
  115. * 验证卡片尺寸是否有效
  116. * @param size 卡片尺寸
  117. * @returns 是否有效
  118. */
  119. public isValidSize(size: string): boolean {
  120. return Object.values(WidgetSize).includes(size as WidgetSize);
  121. }
  122. /**
  123. * 从字符串解析卡片尺寸
  124. * @param sizeStr 尺寸字符串
  125. * @returns 卡片尺寸枚举
  126. */
  127. public parseSizeFromString(sizeStr: string): WidgetSize {
  128. if (this.isValidSize(sizeStr)) {
  129. return sizeStr as WidgetSize;
  130. }
  131. // 尝试从维度字符串解析 (如 "2*1", "4*2", "4*3")
  132. switch (sizeStr) {
  133. case '2*1':
  134. case '2x1':
  135. return WidgetSize.SMALL;
  136. case '4*2':
  137. case '4x2':
  138. return WidgetSize.MEDIUM;
  139. case '4*3':
  140. case '4x3':
  141. return WidgetSize.LARGE;
  142. default:
  143. hilog.warn(0x0000, TAG, `Unknown size string: ${sizeStr}, using medium as default`);
  144. return WidgetSize.MEDIUM;
  145. }
  146. }
  147. /**
  148. * 获取响应式布局参数
  149. * @param size 卡片尺寸
  150. * @returns 布局参数
  151. */
  152. public getResponsiveLayoutParams(size: WidgetSize): ResponsiveLayoutParams {
  153. switch (size) {
  154. case WidgetSize.SMALL:
  155. const smallPadding: ContainerPadding = { left: 16, right: 16, top: 8, bottom: 8 };
  156. const smallButtonSize: ButtonSize = { width: 32, height: 32 };
  157. const smallPlayButtonSize: ButtonSize = { width: 36, height: 36 };
  158. const smallFontSize: FontSizeConfig = { title: 14, artist: 12, time: 10 };
  159. const smallSpacing: SpacingConfig = { horizontal: 8, vertical: 4 };
  160. const smallShowElements: ShowElementsConfig = {
  161. progress: false,
  162. cover: false,
  163. album: false,
  164. time: false
  165. };
  166. return {
  167. containerPadding: smallPadding,
  168. buttonSize: smallButtonSize,
  169. playButtonSize: smallPlayButtonSize,
  170. fontSize: smallFontSize,
  171. spacing: smallSpacing,
  172. showElements: smallShowElements
  173. };
  174. case WidgetSize.MEDIUM:
  175. const mediumPadding: ContainerPadding = { left: 16, right: 16, top: 16, bottom: 16 };
  176. const mediumButtonSize: ButtonSize = { width: 36, height: 36 };
  177. const mediumPlayButtonSize: ButtonSize = { width: 44, height: 44 };
  178. const mediumFontSize: FontSizeConfig = { title: 16, artist: 12, time: 10 };
  179. const mediumSpacing: SpacingConfig = { horizontal: 12, vertical: 8 };
  180. const mediumShowElements: ShowElementsConfig = {
  181. progress: true,
  182. cover: false,
  183. album: true,
  184. time: true
  185. };
  186. return {
  187. containerPadding: mediumPadding,
  188. buttonSize: mediumButtonSize,
  189. playButtonSize: mediumPlayButtonSize,
  190. fontSize: mediumFontSize,
  191. spacing: mediumSpacing,
  192. showElements: mediumShowElements
  193. };
  194. case WidgetSize.LARGE:
  195. const largePadding: ContainerPadding = { left: 16, right: 16, top: 16, bottom: 16 };
  196. const largeButtonSize: ButtonSize = { width: 40, height: 40 };
  197. const largePlayButtonSize: ButtonSize = { width: 52, height: 52 };
  198. const largeFontSize: FontSizeConfig = { title: 16, artist: 13, time: 11 };
  199. const largeSpacing: SpacingConfig = { horizontal: 16, vertical: 16 };
  200. const largeShowElements: ShowElementsConfig = {
  201. progress: true,
  202. cover: true,
  203. album: true,
  204. time: true
  205. };
  206. return {
  207. containerPadding: largePadding,
  208. buttonSize: largeButtonSize,
  209. playButtonSize: largePlayButtonSize,
  210. fontSize: largeFontSize,
  211. spacing: largeSpacing,
  212. showElements: largeShowElements
  213. };
  214. default:
  215. return this.getResponsiveLayoutParams(WidgetSize.MEDIUM);
  216. }
  217. }
  218. /**
  219. * 格式化基础数据
  220. */
  221. private formatBaseData(widgetData: WidgetData): FormattedWidgetData {
  222. return {
  223. isPlaying: widgetData.playState.isPlaying,
  224. isPaused: widgetData.playState.isPaused,
  225. isLoading: widgetData.playState.isLoading,
  226. songTitle: widgetData.currentSong.title || '暂无播放',
  227. songArtist: widgetData.currentSong.artist || '未知艺术家',
  228. songAlbum: widgetData.currentSong.album || '未知专辑',
  229. coverImage: widgetData.currentSong.coverImagePath || '',
  230. currentTime: widgetData.progress.currentTimeText || '00:00',
  231. totalTime: widgetData.progress.totalTimeText || '00:00',
  232. progressPercentage: widgetData.progress.percentage || 0,
  233. hasNext: widgetData.playlist.hasNext,
  234. hasPrevious: widgetData.playlist.hasPrevious,
  235. showProgress: widgetData.config.showProgress,
  236. showCover: widgetData.config.showCover,
  237. widgetSize: widgetData.config.size.toString(),
  238. timestamp: Date.now()
  239. };
  240. }
  241. /**
  242. * 适配小尺寸卡片数据
  243. */
  244. private adaptForSmallWidget(baseData: FormattedWidgetData, widgetData: WidgetData): FormattedWidgetData {
  245. const result: FormattedWidgetData = {
  246. isPlaying: baseData.isPlaying,
  247. isPaused: baseData.isPaused,
  248. isLoading: baseData.isLoading,
  249. songTitle: this.truncateText(baseData.songTitle, 20), // 小卡片只显示歌曲标题,如果标题过长则截断
  250. songArtist: baseData.songArtist,
  251. songAlbum: baseData.songAlbum,
  252. coverImage: baseData.coverImage,
  253. currentTime: baseData.currentTime,
  254. totalTime: baseData.totalTime,
  255. progressPercentage: baseData.progressPercentage,
  256. hasNext: baseData.hasNext,
  257. hasPrevious: baseData.hasPrevious,
  258. showProgress: false,
  259. showCover: false,
  260. widgetSize: WidgetSize.SMALL,
  261. timestamp: baseData.timestamp
  262. };
  263. return result;
  264. }
  265. /**
  266. * 适配中等尺寸卡片数据
  267. */
  268. private adaptForMediumWidget(baseData: FormattedWidgetData, widgetData: WidgetData): FormattedWidgetData {
  269. const result: FormattedWidgetData = {
  270. isPlaying: baseData.isPlaying,
  271. isPaused: baseData.isPaused,
  272. isLoading: baseData.isLoading,
  273. songTitle: this.truncateText(baseData.songTitle, 30), // 中等卡片显示更多信息,但仍需要适当截断
  274. songArtist: this.truncateText(baseData.songArtist, 25),
  275. songAlbum: this.truncateText(baseData.songAlbum, 25),
  276. coverImage: baseData.coverImage,
  277. currentTime: baseData.currentTime,
  278. totalTime: baseData.totalTime,
  279. progressPercentage: baseData.progressPercentage,
  280. hasNext: baseData.hasNext,
  281. hasPrevious: baseData.hasPrevious,
  282. showProgress: true,
  283. showCover: false,
  284. widgetSize: WidgetSize.MEDIUM,
  285. timestamp: baseData.timestamp
  286. };
  287. return result;
  288. }
  289. /**
  290. * 适配大尺寸卡片数据
  291. */
  292. private adaptForLargeWidget(baseData: FormattedWidgetData, widgetData: WidgetData): FormattedWidgetData {
  293. const result: FormattedWidgetData = {
  294. isPlaying: baseData.isPlaying,
  295. isPaused: baseData.isPaused,
  296. isLoading: baseData.isLoading,
  297. songTitle: this.truncateText(baseData.songTitle, 40), // 大卡片可以显示完整信息
  298. songArtist: this.truncateText(baseData.songArtist, 35),
  299. songAlbum: this.truncateText(baseData.songAlbum, 35),
  300. coverImage: baseData.coverImage,
  301. currentTime: baseData.currentTime,
  302. totalTime: baseData.totalTime,
  303. progressPercentage: baseData.progressPercentage,
  304. hasNext: baseData.hasNext,
  305. hasPrevious: baseData.hasPrevious,
  306. showProgress: true,
  307. showCover: true,
  308. widgetSize: WidgetSize.LARGE,
  309. timestamp: baseData.timestamp
  310. };
  311. return result;
  312. }
  313. /**
  314. * 截断文本
  315. */
  316. private truncateText(text: string, maxLength: number): string {
  317. if (!text || text.length <= maxLength) {
  318. return text;
  319. }
  320. return text.substring(0, maxLength - 3) + '...';
  321. }
  322. }