From c4219767d48c8a30326963103e1c735134504d7e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 30 Aug 2026 17:23:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(ohos):=20MotionBase=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=8C=89=E5=8E=8B=E5=8A=A8=E6=95=88=20+=20=E5=88=86=E5=B1=82?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E9=9A=8F=E4=B8=BB=E9=A2=98=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 动效统一 各组件重复实现按压反馈(@State pressed + scale + animation + onTouch 四件套), 时长各写魔数导致全局手感不一致。 - 新增 components/MotionBase.ets:通用动效的"父组件"。ArkUI V1 的 @Component struct 无法继承他人 build,改用组合表达继承——调用组件把内容经 @BuilderParam 内容插槽传入,MotionBase 在包装节点统一挂动效修饰器。 pressEnabled 默认关(纯展示容器零开销);fillWidth=false 供气泡内卡片按内容 自适应宽度;flexWeight 供等分排列按钮参与剩余空间分配。 onPress 只作按压瞬时轻量钩子,导航/提交语义仍由调用组件 onClick 负责, 避免"按下即触发"的手感偏差。 - Constants.ets 新增动效 token:ANIM_FAST(150) / ANIM_NORMAL(220) / ANIM_ENTER(280) / ANIM_SLOW(400) / PRESS_SCALE(0.97)。 取值依据:状态切换 150-250ms(>300ms 显拖沓),大位移进出场 300-400ms 才不突兀;曲线统一 EaseOut 起步快收尾缓。 - NavRow / StatusSummaryCard / AttachmentCard / 插件卡片等公共组件接入 MotionBase,移除各自的按压四件套。组件专属动效(聊天输入框上弹、加号菜单 浮起、折叠面板展开、toast 进出场)保留在各组件内,不塞进父组件。 ## 图标与启动页随主题切换 原先直接指向位图 app_icon.png,浅色底被烧进图标,深色模式下桌面与启动页跳脱。 - 改用分层图标 layered_image:foreground 为字形,background(沉淀色)在 base/ 与 dark/ 各一份,随系统主题切换。app.json5 与 module.json5 的 icon 均指向 :layered_image。 - startWindowIcon 改用透明底 start_icon.png,配合 start_window_background 的 base(#F1F3F5) / dark(#000000) 两份取值,浅深模式遮罩与图标都能对上。 ## 验证 清空 entry/build 后全量重编:hvigorw assembleHap BUILD SUCCESSFUL(9.8s), 零 ArkTS 错误。提交内容已确认不含 build/ oh_modules/ .hap 与签名材料。 --- cmd/ohos/HomeAgent/AppScope/app.json5 | 4 +- .../resources/base/media/background.png | Bin 0 -> 989 bytes .../resources/base/media/foreground.png | Bin 0 -> 12812 bytes .../resources/base/media/layered_image.json | 6 + .../resources/dark/media/background.png | Bin 0 -> 988 bytes .../entry/src/main/ets/common/Constants.ets | 19 ++ .../src/main/ets/components/Attachment.ets | 25 +- .../src/main/ets/components/MotionBase.ets | 87 +++++++ .../src/main/ets/components/PageTopBar.ets | 57 +++-- .../main/ets/components/SettingsEditor.ets | 10 + .../src/main/ets/components/StatusCards.ets | 18 +- .../entry/src/main/ets/components/SubPage.ets | 211 +++++++++------- .../entry/src/main/ets/pages/ChatPage.ets | 225 ++++++++++-------- .../entry/src/main/ets/pages/DevicePage.ets | 144 +++++++---- .../entry/src/main/ets/pages/Index.ets | 44 ++-- .../entry/src/main/ets/pages/PluginsPage.ets | 174 +++++++++----- .../entry/src/main/ets/pages/SettingsPage.ets | 56 +++-- .../HomeAgent/entry/src/main/module.json5 | 6 +- .../main/resources/base/element/color.json | 2 +- .../main/resources/base/media/start_icon.png | Bin 0 -> 9741 bytes .../main/resources/dark/element/color.json | 8 + 21 files changed, 732 insertions(+), 364 deletions(-) create mode 100644 cmd/ohos/HomeAgent/AppScope/resources/base/media/background.png create mode 100644 cmd/ohos/HomeAgent/AppScope/resources/base/media/foreground.png create mode 100644 cmd/ohos/HomeAgent/AppScope/resources/base/media/layered_image.json create mode 100644 cmd/ohos/HomeAgent/AppScope/resources/dark/media/background.png create mode 100644 cmd/ohos/HomeAgent/entry/src/main/ets/components/MotionBase.ets create mode 100644 cmd/ohos/HomeAgent/entry/src/main/resources/base/media/start_icon.png create mode 100644 cmd/ohos/HomeAgent/entry/src/main/resources/dark/element/color.json diff --git a/cmd/ohos/HomeAgent/AppScope/app.json5 b/cmd/ohos/HomeAgent/AppScope/app.json5 index 5f89745..5291775 100644 --- a/cmd/ohos/HomeAgent/AppScope/app.json5 +++ b/cmd/ohos/HomeAgent/AppScope/app.json5 @@ -4,7 +4,9 @@ "vendor": "HomeAgent", "versionCode": 1000000, "versionName": "1.0.0", - "icon": "$media:app_icon", + // 分层图标:前景是字形,背景(沉淀色)在 base/ 与 dark/ 各一份,随系统主题切换。 + // 直接指向位图会把浅色底烧进图标,深色模式下桌面和启动页都会跳脱。 + "icon": "$media:layered_image", "label": "$string:app_name" } } diff --git a/cmd/ohos/HomeAgent/AppScope/resources/base/media/background.png b/cmd/ohos/HomeAgent/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..c4873ea236ff18a78e5ab282c40cd6591e9c6c6e GIT binary patch literal 989 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 C=~xB; literal 0 HcmV?d00001 diff --git a/cmd/ohos/HomeAgent/AppScope/resources/base/media/layered_image.json b/cmd/ohos/HomeAgent/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000..d2e870e --- /dev/null +++ b/cmd/ohos/HomeAgent/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,6 @@ +{ + "layered-image": { + "background": "$media:background", + "foreground": "$media:foreground" + } +} diff --git a/cmd/ohos/HomeAgent/AppScope/resources/dark/media/background.png b/cmd/ohos/HomeAgent/AppScope/resources/dark/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..c29bd990c3775b84735d96d1e59314b3db395463 GIT binary patch literal 988 zcmeAS@N?(olHy`uVBq!ia0y~yU{nBM4mJh`29f4<4Gav-i#=T&Ln`LHy<*7Apuljz zz=8eXe(wnzo5K#SJG=M1EJI@`JHvk?1%+e>hhq&5J&cT!%uGgXEGax3CnN+W7>-Je h2FYk*plvpgyDi5&wRisRR$#7W@O1TaS?83{1OTQ9@ofMA literal 0 HcmV?d00001 diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets index 9049d71..46b5fa2 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/Constants.ets @@ -52,6 +52,25 @@ export const WIDE_MIN_CONTENT: number = 380; /** 悬浮导航胶囊的最大宽度(vp):宽屏下避免被拉成长条 */ export const NAV_PILL_MAX_WIDTH: number = 620; +// ===== 动效时长(ms)===== +/** + * 统一动效节奏,避免每处各写一个魔数导致快慢不一。 + * + * 取值依据:状态切换类交互(按下、展开、选中)控制在 150-250ms, + * 超过 300ms 会显得"拖";进出场大位移(导航栏收起、悬浮层)用 300-400ms + * 才不突兀。曲线统一 EaseOut:起步快、收尾缓,手感上更"跟手"。 + */ +/** 按压反馈、图标旋转等即时反馈 */ +export const ANIM_FAST: number = 150; +/** 展开/折叠、选中态迁移等常规状态切换 */ +export const ANIM_NORMAL: number = 220; +/** 卡片入场、列表项出现 */ +export const ANIM_ENTER: number = 280; +/** 导航栏收起、悬浮层进出等大位移 */ +export const ANIM_SLOW: number = 400; +/** 按压态缩放:轻微下沉即可,过度缩放显廉价 */ +export const PRESS_SCALE: number = 0.97; + /** Every color token the app renders with. */ export interface ThemePalette { bgPrimary: string; diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/Attachment.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/Attachment.ets index cc8c4df..9ff7f6a 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/Attachment.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/Attachment.ets @@ -4,8 +4,9 @@ import { common } from '@kit.AbilityKit'; import { apiClient } from '../common/ApiClient'; import { userMessage } from '../common/UserError'; import { ChatAttachment } from '../model/Model'; -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG, RADIUS_MD, RADIUS_SM } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG, RADIUS_MD, RADIUS_SM, ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; import { COLOR_ERROR } from '../common/Constants'; +import { MotionBase } from './MotionBase'; import { PlainCard } from './SubPage'; /** @@ -131,6 +132,9 @@ export struct AttachmentCard { } build() { + // 整卡按压反馈统一收进 MotionBase(父组件):scale 回弹 + 透明度压暗。 + // fillWidth: false —— 附件卡在聊天气泡内按内容自适应宽度,不能撑满整行。 + MotionBase({ pressEnabled: true, pressOpacity: 0.88, fillWidth: false }) { Column({ space: 6 }) { Text(this.mine ? '你发送的' : '小宅发送的') .fontSize(11) @@ -144,6 +148,9 @@ export struct AttachmentCard { .objectFit(ImageFit.Cover) .borderRadius(RADIUS_MD) .draggable(false) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.scale({ x: 0.98, y: 0.98 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } else if (this.failed) { Row({ space: 6 }) { Image($r('app.media.ic_error')) @@ -158,6 +165,7 @@ export struct AttachmentCard { .padding({ left: 10, right: 10, top: 8, bottom: 8 }) .borderRadius(RADIUS_SM) .backgroundColor(this.palette().bgHover) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } else { Row() { LoadingProgress() @@ -170,6 +178,7 @@ export struct AttachmentCard { .justifyContent(FlexAlign.Center) .borderRadius(RADIUS_SM) .backgroundColor(this.palette().bgHover) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } } else { Row({ space: 8 }) { @@ -212,6 +221,7 @@ export struct AttachmentCard { cb(); } }) + } } private palette(): ThemePalette { @@ -259,7 +269,9 @@ export struct AttachmentDetailContent { } catch (e) { this.err = userMessage('attachment.load', e); } - this.loading = false; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.loading = false; + }); } /** 保存到应用沙箱 files 目录(不申请媒体库权限,避免为了看一张图要授权相册)。 */ @@ -300,6 +312,9 @@ export struct AttachmentDetailContent { .border({ width: 1, color: this.palette().glassBorder }) .alignItems(VerticalAlign.Top) .margin({ bottom: 14 }) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: -12 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } // 预览:图片铺满宽度可缩放查看;文件给一个大图标占位 @@ -315,6 +330,7 @@ export struct AttachmentDetailContent { .width('100%') .height(200) .justifyContent(FlexAlign.Center) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } else if (this.pixel !== undefined) { Image(this.pixel) .width('100%') @@ -322,6 +338,7 @@ export struct AttachmentDetailContent { .objectFit(ImageFit.Contain) .borderRadius(RADIUS_MD) .draggable(false) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } } else { Column({ space: 10 }) { @@ -375,6 +392,9 @@ export struct AttachmentDetailContent { .fontColor(this.palette().textMuted) .width('100%') .margin({ top: 8 }) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: 6 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } } } @@ -401,6 +421,7 @@ export struct AttachmentDetailContent { .width('100%') .padding({ top: 8, bottom: 8 }) .alignItems(VerticalAlign.Top) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } private palette(): ThemePalette { diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/MotionBase.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/MotionBase.ets new file mode 100644 index 0000000..ca6a537 --- /dev/null +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/MotionBase.ets @@ -0,0 +1,87 @@ +/** + * MotionBase —— 全局通用动效的"父组件"。 + * + * 目标:把各个组件重复实现的按压反馈收拢到这唯一一个父组件里, + * 全局统一手感(时长 token + EaseOut 曲线 + 形变幅度)。 + * 公共组件(NavRow / StatusSummaryCard / AttachmentCard / 插件卡片 …) + * 在自己的 build 根节点接入 MotionBase,通用按压动效即随父组件生效, + * 子组件无需再各自维护 @State pressed + scale + animation + onTouch 四件套。 + * + * 特有动画(多态扩展):留在各组件内部。例如聊天输入框向上弹出 + * (ChatPage 的 inputMultiLine 上移 + Curve.Friction 展开)、加号菜单浮起、 + * 折叠面板展开、toast 进出场——这些是"某个组件专属"的动效, + * 不应塞进父组件,由各组件按需声明。 + * + * ArkUI V1 的 @Component struct 不能继承另一位 struct 的 build, + * 这里用「组合」表达继承:调用组件把内容经唯一内容插槽传给 MotionBase, + * MotionBase 在包装节点上统一挂通用动效修饰器。 + * + * 通用动效契约: + * - 按压反馈:pressEnabled 时整容器 scale → pressScale,快速回弹。 + * V1 自带按压态在玻璃/透明底上几乎不可见,这里统一补足; + * scale 只作用于容器自身,不侵入子节点外观。 + * - onPress 只是按压瞬时的轻量钩子(如提前高亮), + * 真正的导航/提交语义仍由调用组件的 onClick(点击释放)负责, + * 二者不耦合,避免"按下即触发"的手感偏差。 + * + * 用法: + * MotionBase({ pressEnabled: true }) { + * // 实际内容;外层 onClick 由调用组件自己绑在内容上 + * } + * + * 注意:包装节点默认 width('100%')。像聊天气泡里的附件卡那样需要 + * 「内容自适应宽度」的组件传 fillWidth: false,否则会被撑满整行。 + */ +import { ANIM_FAST, PRESS_SCALE } from '../common/Constants'; + +@Component +export struct MotionBase { + /** 内容插槽:调用组件把实际内容传进来 */ + @BuilderParam content: () => void; + /** 是否启用按压反馈(默认关,纯展示容器传 false 零开销) */ + @Prop pressEnabled: boolean = false; + /** 按下的瞬间回调(可选):只做轻量即时反馈,不要在这里做导航/提交 */ + onPress?: () => void; + /** 按压缩放幅度,默认全局 PRESS_SCALE */ + @Prop pressScale: number = PRESS_SCALE; + /** 按压时整体透明度(1 = 不启用透明度反馈) */ + @Prop pressOpacity: number = 1; + /** 包装节点是否撑满父宽;false = 由内容决定宽度(气泡内的卡片必须传 false) */ + @Prop fillWidth: boolean = true; + /** 在 Row/Column 中参与剩余空间分配的权重;0 = 不参与(等分排列的按钮传 1) */ + @Prop flexWeight: number = 0; + + /** 按压态:只对容器自身 scale 生效 */ + @State private pressed: boolean = false; + + build() { + Column() { + this.content() + } + .width(this.fillWidth && this.flexWeight <= 0 ? '100%' : undefined) + .layoutWeight(this.flexWeight) + .alignItems(HorizontalAlign.Start) + // ===== 通用动效(父组件统一声明,子组件继承) ===== + // 按压回弹:pressed 驱动 scale 增量,.animation 在下方覆盖该增量属性 + .scale({ + x: this.pressed ? this.pressScale : 1, + y: this.pressed ? this.pressScale : 1, + }) + .opacity(this.pressed ? this.pressOpacity : 1) + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) + .onTouch((e: TouchEvent) => { + if (!this.pressEnabled) { + return; + } + if (e.type === TouchType.Down) { + this.pressed = true; + const cb = this.onPress; + if (cb !== undefined) { + cb(); + } + } else if (e.type === TouchType.Up || e.type === TouchType.Cancel) { + this.pressed = false; + } + }) + } +} \ No newline at end of file diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/PageTopBar.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PageTopBar.ets index 9f48c65..35d645e 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/PageTopBar.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/PageTopBar.ets @@ -1,4 +1,6 @@ import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE } from '../common/Constants'; +import { ANIM_FAST, ANIM_SLOW } from '../common/Constants'; +import { MotionBase } from './MotionBase'; /** * 顶栏高度(vp)。页面 padding-top 应略大于此值,避免内容被标题压住, @@ -123,31 +125,36 @@ export struct FloatIconButton { onTap?: () => void; build() { - Button() { - Image(this.icon) - .width(18) - .height(18) - .fillColor(this.accent ? this.palette().accent : this.palette().textSecondary) - } - .width(FLOAT_NODE_SIZE) - .height(FLOAT_NODE_SIZE) - .type(ButtonType.Circle) - .backgroundColor(this.palette().navBarBg) - .border({ - width: { left: 1, top: 1, right: 1, bottom: 1 }, - color: this.palette().navBarBorder, - }) - .shadow({ - radius: 24, - color: this.palette().shadow, - offsetY: 8, - }) - .onClick(() => { - const cb = this.onTap; - if (cb !== undefined) { - cb(); + // 按压缩放交给 MotionBase(父组件统一手感); + // fillWidth: false —— 悬浮区靠右对齐,包装节点必须按内容 42vp 收窄。 + MotionBase({ pressEnabled: true, fillWidth: false }) { + Button() { + Image(this.icon) + .width(18) + .height(18) + .fillColor(this.accent ? this.palette().accent : this.palette().textSecondary) + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) } - }) + .width(FLOAT_NODE_SIZE) + .height(FLOAT_NODE_SIZE) + .type(ButtonType.Circle) + .backgroundColor(this.palette().navBarBg) + .border({ + width: { left: 1, top: 1, right: 1, bottom: 1 }, + color: this.palette().navBarBorder, + }) + .shadow({ + radius: 24, + color: this.palette().shadow, + offsetY: 8, + }) + .onClick(() => { + const cb = this.onTap; + if (cb !== undefined) { + cb(); + } + }) + } } private palette(): ThemePalette { @@ -203,6 +210,6 @@ export struct NavFloatOverlay { .margin({ bottom: 94 }) .translate({ y: (!this.navVisible || this.currentTab !== this.tab) ? 140 : 0 }) .opacity((!this.navVisible || this.currentTab !== this.tab) ? 0 : 1) - .animation({ duration: 400, curve: Curve.EaseOut }) + .animation({ duration: ANIM_SLOW, curve: Curve.EaseOut }) } } \ No newline at end of file diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/SettingsEditor.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/SettingsEditor.ets index f21976c..023a95a 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/SettingsEditor.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/SettingsEditor.ets @@ -3,6 +3,7 @@ import { userMessage, noConnectionMessage } from '../common/UserError'; import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE } from '../common/Constants'; import { RADIUS_SM } from '../common/Constants'; import { COLOR_ERROR } from '../common/Constants'; +import { ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; /** * 可复用的后端配置编辑器(按 key 前缀取一段配置并就地编辑)。 @@ -243,6 +244,7 @@ export struct SettingsEditor { .width('100%') .justifyContent(FlexAlign.Center) .padding({ top: 14, bottom: 14 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } if (this.err.length > 0) { @@ -250,6 +252,8 @@ export struct SettingsEditor { .fontSize(12) .fontColor(COLOR_ERROR) .width('100%') + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -12 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } if (!this.loading && this.err.length === 0 && this.entries.length === 0) { @@ -257,6 +261,7 @@ export struct SettingsEditor { .fontSize(12) .fontColor(this.palette().textMuted) .width('100%') + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } ForEach(this.entries, (entry: EditorEntry) => { @@ -268,6 +273,8 @@ export struct SettingsEditor { .fontSize(11) .fontColor(this.toastErr ? COLOR_ERROR : this.palette().accent) .margin({ top: 4 }) + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -12 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } } .width('100%') @@ -302,6 +309,7 @@ export struct SettingsEditor { Text(entry.value === 'true' ? '已开启' : '已关闭') .fontSize(12) .fontColor(entry.value === 'true' ? this.palette().accent : this.palette().textMuted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) Blank() Toggle({ type: ToggleType.Switch, isOn: entry.value === 'true' }) .selectedColor(this.palette().accent) @@ -325,6 +333,7 @@ export struct SettingsEditor { .margin({ right: 6, bottom: 6 }) .backgroundColor(entry.value === opt ? this.palette().accent : this.palette().bgHover) .fontColor(entry.value === opt ? Color.White : this.palette().textSecondary) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) .onClick(() => { this.save(entry, opt); }) @@ -352,6 +361,7 @@ export struct SettingsEditor { .fontSize(12) .backgroundColor(entry.dirty ? '#D99A2B' : this.palette().accent) .fontColor(Color.White) + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) .margin({ left: 8 }) .onClick(() => { this.save(entry, this.latestValue(entry.key)); diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets index 3bf9b70..29b06f3 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets @@ -1,5 +1,7 @@ -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG, RADIUS_SM } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG, RADIUS_SM, + ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; import { COLOR_ACCENT, COLOR_SUCCESS, COLOR_CYAN, COLOR_ERROR } from '../common/Constants'; +import { MotionBase } from './MotionBase'; import { statusStore, StatGroup, StatField, compactDuration, K_UP, K_VERSION, K_STARTED, K_AGENTS, K_PLUGINS, K_TOOLS, K_ERR, K_LOADING, K_REV, @@ -43,6 +45,10 @@ export struct StatusSummaryCard { } build() { + // 整卡按压反馈统一收进 MotionBase(父组件):这里是进明细页的唯一入口, + // 父组件负责 scale 回弹;导航语义仍由本卡自己的 onClick(点击释放)负责, + // 避免"按下即跳转"的误触手感。 + MotionBase({ pressEnabled: true }) { Column() { if (this.err.length > 0) { // 连接失败:只显示一句人话,技术细节在 hilog 里 @@ -60,6 +66,7 @@ export struct StatusSummaryCard { } .width('100%') .alignItems(VerticalAlign.Top) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } else if (this.loading && !this.up) { Row() { LoadingProgress() @@ -70,6 +77,7 @@ export struct StatusSummaryCard { .width('100%') .justifyContent(FlexAlign.Center) .padding({ top: 20, bottom: 20 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } else { // 状态主行:一枚状态图标(运行=脉搏,离线=断开)+ 状态词 + 运行时长。 // 之前那个环形进度条表达的是"100% / 0%",而在线与否是布尔量, @@ -81,18 +89,21 @@ export struct StatusSummaryCard { .width(24) .height(24) .fillColor(this.up ? COLOR_SUCCESS : this.palette().textMuted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) .draggable(false) } .width(48) .height(48) .borderRadius(RADIUS_SM) .backgroundColor(this.up ? 'rgba(23, 169, 100, 0.16)' : this.palette().bgHover) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) Column({ space: 3 }) { Text(this.up ? '运行中' : '离线') .fontSize(17) .fontWeight(FontWeight.Bold) .fontColor(this.up ? COLOR_SUCCESS : this.palette().textMuted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) Text(this.up ? '已运行 ' + this.uptime : '未连接到后端服务') .fontSize(12) .fontColor(this.palette().textSecondary) @@ -104,6 +115,7 @@ export struct StatusSummaryCard { } .width('100%') .alignItems(VerticalAlign.Center) + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 12 })).animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) // 能力计数:图标 + 数字,只保留真正会变的两项(插件 / 工具) Row({ space: 10 }) { @@ -155,6 +167,7 @@ export struct StatusSummaryCard { cb(); } }) + } } @Builder @@ -237,6 +250,7 @@ export struct StatusDetailContent { .border({ width: 1, color: this.palette().glassBorder }) .alignItems(VerticalAlign.Top) .margin({ bottom: 14 }) + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 12 })).animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } if (this.loading && this.groups.length === 0) { @@ -249,6 +263,7 @@ export struct StatusDetailContent { .width('100%') .justifyContent(FlexAlign.Center) .padding({ top: 40, bottom: 40 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } // 这里原本有一张"服务负载"卡,用两条容量条画插件/工具数量。 @@ -295,6 +310,7 @@ export struct StatusDetailContent { .border({ width: 1, color: this.palette().glassBorder }) .alignItems(HorizontalAlign.Start) .margin({ bottom: 14 }) + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 12 })).animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) }, (g: StatGroup) => g.title) } .width('100%') diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/SubPage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/SubPage.ets index fd1e140..5a4069e 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/SubPage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/SubPage.ets @@ -1,4 +1,6 @@ -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, RADIUS_LG, + ANIM_NORMAL } from '../common/Constants'; +import { MotionBase } from './MotionBase'; import { handleNavOnScroll } from '../common/NavBarController'; import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from './PageTopBar'; import { GradientBackground } from './GradientBackground'; @@ -69,6 +71,7 @@ export struct SubPageLayer { .width('100%') // 宽屏右栏底部没有主导航胶囊,只保留悬浮键的空间 .padding({ left: 16, right: 16, top: 76, bottom: this.isWide ? 108 : 174 }) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) .alignItems(HorizontalAlign.Start) } .width('100%') @@ -88,25 +91,34 @@ export struct SubPageLayer { NavFloatOverlay({ tab: this.tab }) { NavFloatRow() { if (!this.isWide) { - SubPageBackButton({ - onTap: () => { - const cb = this.onBack; - if (cb !== undefined) { - cb(); - } - }, - }) - if (this.showRefresh) { - FloatIconButton({ - icon: $r('app.media.ic_refresh'), + // 自定义组件不能直接挂 .transition()(会生成 __Common__ 包装节点), + // 所以各自套一层无 padding 的 Column,由它承载出入场动画, + // 让悬浮键与 400ms 平滑滑出的悬浮层不再"一帧硬切"。 + Column() { + SubPageBackButton({ onTap: () => { - const cb = this.onRefresh; + const cb = this.onBack; if (cb !== undefined) { cb(); } }, }) } + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.scale({ x: 0.8, y: 0.8 })).animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) + if (this.showRefresh) { + Column() { + FloatIconButton({ + icon: $r('app.media.ic_refresh'), + onTap: () => { + const cb = this.onRefresh; + if (cb !== undefined) { + cb(); + } + }, + }) + } + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.scale({ x: 0.8, y: 0.8 })).animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) + } } } } @@ -138,71 +150,82 @@ export struct NavRow { onTap?: () => void; build() { - Row() { - if (this.icon !== undefined) { - Row() { - Image(this.icon) - .width(17) - .height(17) - .fillColor(this.iconColor()) - .draggable(false) + // 通用按压动画收进 MotionBase(父组件):scale 回弹统一。 + // 选中态底色仍在这里管:它是 NavRow 特有外观,不归父组件。 + MotionBase({ pressEnabled: true }) { + Row() { + if (this.icon !== undefined) { + Row() { + Image(this.icon) + .width(17) + .height(17) + .fillColor(this.iconColor()) + .draggable(false) + } + .width(32) + .height(32) + .borderRadius(10) + .backgroundColor(this.iconBg()) + .justifyContent(FlexAlign.Center) + .margin({ right: 12 }) } - .width(32) - .height(32) - .borderRadius(10) - .backgroundColor(this.iconBg()) - .justifyContent(FlexAlign.Center) - .margin({ right: 12 }) - } - Column({ space: 2 }) { - Text(this.title) - .fontSize(15) - .fontColor(this.selected ? this.palette().accent : this.palette().textPrimary) - .fontWeight(this.selected ? FontWeight.Medium : FontWeight.Normal) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - if (this.subtitle.length > 0) { - Text(this.subtitle) - .fontSize(11) - .fontColor(this.palette().textMuted) + Column({ space: 2 }) { + Text(this.title) + .fontSize(15) + .fontColor(this.selected ? this.palette().accent : this.palette().textPrimary) + // 父行 MotionBase 的 .animation() 到不了子节点,选中态字色自己缓动 + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + .fontWeight(this.selected ? FontWeight.Medium : FontWeight.Normal) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) + if (this.subtitle.length > 0) { + Text(this.subtitle) + .fontSize(11) + .fontColor(this.palette().textMuted) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } } - } - .layoutWeight(1) - .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) - if (this.value.length > 0) { - Text(this.value) - .fontSize(12) - .fontColor(this.palette().textSecondary) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .constraintSize({ maxWidth: 130 }) - .margin({ right: 8 }) - } + if (this.value.length > 0) { + Text(this.value) + .fontSize(12) + .fontColor(this.palette().textSecondary) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .constraintSize({ maxWidth: 130 }) + .margin({ right: 8 }) + } - Image($r('app.media.ic_chevron_right')) - .width(15) - .height(15) - .fillColor(this.selected ? this.palette().accent : this.palette().textMuted) - .draggable(false) + Image($r('app.media.ic_chevron_right')) + .width(15) + .height(15) + .fillColor(this.selected ? this.palette().accent : this.palette().textMuted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + .draggable(false) + } + .width('100%') + .padding({ left: 14, right: 14, top: 12, bottom: 12 }) + .alignItems(VerticalAlign.Center) + // 选中态底色:写在 .border 之前,避免分割线宽度被纳入动画。 + // MotionBase 的 .animation() 只作用在包装 Column 上,到不了这里, + // 选中高亮的底色迁移要本行自己声明。 + .backgroundColor(this.selected ? this.palette().accentBg : Color.Transparent) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + .border({ + width: { bottom: this.showDivider ? 1 : 0 }, + color: this.palette().kvBorder, + }) + .onClick(() => { + const cb = this.onTap; + if (cb !== undefined) { + cb(); + } + }) } - .width('100%') - .padding({ left: 14, right: 14, top: 12, bottom: 12 }) - .alignItems(VerticalAlign.Center) - .backgroundColor(this.selected ? this.palette().accentBg : Color.Transparent) - .border({ - width: { bottom: this.showDivider ? 1 : 0 }, - color: this.palette().kvBorder, - }) - .onClick(() => { - const cb = this.onTap; - if (cb !== undefined) { - cb(); - } - }) } private iconColor(): string { @@ -301,30 +324,34 @@ export struct SubPageBackButton { onTap?: () => void; build() { - Button() { - Row({ space: 4 }) { - Image($r('app.media.ic_arrow_back')) - .width(17) - .height(17) - .fillColor(this.palette().textSecondary) - .draggable(false) + // 玻璃底自带的按压变暗在 45% 透明度上几乎看不见,缩放反馈由 MotionBase 统一提供。 + // fillWidth: false —— 悬浮区按内容宽度排列,不能撑满整行。 + MotionBase({ pressEnabled: true, fillWidth: false }) { + Button() { + Row({ space: 4 }) { + Image($r('app.media.ic_arrow_back')) + .width(17) + .height(17) + .fillColor(this.palette().textSecondary) + .draggable(false) + } } + .width(42) + .height(42) + .type(ButtonType.Circle) + .backgroundColor(this.palette().navBarBg) + .border({ + width: { left: 1, top: 1, right: 1, bottom: 1 }, + color: this.palette().navBarBorder, + }) + .shadow({ radius: 24, color: this.palette().shadow, offsetY: 8 }) + .onClick(() => { + const cb = this.onTap; + if (cb !== undefined) { + cb(); + } + }) } - .width(42) - .height(42) - .type(ButtonType.Circle) - .backgroundColor(this.palette().navBarBg) - .border({ - width: { left: 1, top: 1, right: 1, bottom: 1 }, - color: this.palette().navBarBorder, - }) - .shadow({ radius: 24, color: this.palette().shadow, offsetY: 8 }) - .onClick(() => { - const cb = this.onTap; - if (cb !== undefined) { - cb(); - } - }) } private palette(): ThemePalette { diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/ChatPage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/ChatPage.ets index 312ee12..9a8547a 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/ChatPage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/ChatPage.ets @@ -9,6 +9,8 @@ import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE } from '../common/Constants'; import { RADIUS_MD, RADIUS_PILL, RADIUS_SM } from '../common/Constants'; import { WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT } from '../common/Constants'; import { CHAT_PAGE_SIZE } from '../common/Constants'; +import { ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; +import { MotionBase } from '../components/MotionBase'; import { MarkdownView } from '../components/MarkdownView'; import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from '../components/PageTopBar'; import { SubPageLayer, markSubPageOpen, subPageParam } from '../components/SubPage'; @@ -746,7 +748,9 @@ export struct ChatPage { /** 从图库挑一张图 */ private async pickImage(): Promise { - this.attachMenuOpen = false; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.attachMenuOpen = false; + }); try { const options = new picker.PhotoSelectOptions(); options.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; @@ -765,7 +769,9 @@ export struct ChatPage { /** 从文件管理器挑一个文件 */ private async pickFile(): Promise { - this.attachMenuOpen = false; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.attachMenuOpen = false; + }); try { const options = new picker.DocumentSelectOptions(); options.maxSelectNumber = 1; @@ -993,8 +999,12 @@ export struct ChatPage { } private toggleReasoning(msg: ChatMessage): void { - msg.reasoningOpen = !(msg.reasoningOpen === true); - this.forceRefresh(); + // 在 animateTo 里翻转:展开/收起时 chevron 走已有 .animation, + // 面板节点在 animateTo 帧内获得默认过渡,不会再硬切。 + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + msg.reasoningOpen = !(msg.reasoningOpen === true); + this.forceRefresh(); + }); } /** @@ -1168,7 +1178,9 @@ export struct ChatPage { .width('100%') .justifyContent(FlexAlign.Start) .padding({ left: 52, top: 6, bottom: 6 }) - .opacity(1) + // if 控制的节点无法用 .animation() 做进出场(那只驱动自身属性的增量更新); + // 用 transition 才能让"处理中"这条在出现和消失时都淡入淡出。 + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } } .width('100%') @@ -1229,7 +1241,11 @@ export struct ChatPage { FloatIconButton({ icon: $r('app.media.ic_plus'), onTap: () => { - this.attachMenuOpen = !this.attachMenuOpen; + // 菜单展开会同时改变 listBottomPad,用 animateTo 把 + // 列表内边距和菜单进出场拉到同一个时钟上。 + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.attachMenuOpen = !this.attachMenuOpen; + }); }, }) Blank() @@ -1244,6 +1260,8 @@ export struct ChatPage { .height(44) .type(ButtonType.Circle) .backgroundColor('#77809A') + // 发送/中断切换是 if 分支整体替换,用 transition 淡入淡出 + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.scale({ x: 0.9, y: 0.9 })).animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) .onClick(() => { this.interruptChat(); }) @@ -1259,6 +1277,7 @@ export struct ChatPage { .type(ButtonType.Circle) .backgroundColor(this.palette().accent) .enabled(this.inputText.trim().length > 0 || this.pendingPath.length > 0) + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.scale({ x: 0.9, y: 0.9 })).animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) .onClick(() => { this.sendChat(); }) @@ -1353,73 +1372,83 @@ export struct ChatPage { .justifyContent(FlexAlign.Start) .margin({ bottom: 8 }) .hitTestBehavior(HitTestMode.Transparent) + // 加号菜单由 if 控制,进出场只能靠 transition;配合下面 toggle 处的 + // animateTo,展开时两枚胶囊从加号上方浮起而不是硬闪出来。 + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 12 })).animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } @Builder AttachOption(icon: Resource, label: string, tap: () => void) { - Row({ space: 6 }) { - Image(icon) - .width(15) - .height(15) - .fillColor(this.palette().textSecondary) - .draggable(false) - Text(label) - .fontSize(12) - .fontColor(this.palette().textPrimary) + // 按压反馈交给 MotionBase:每个实例自带独立按压态, + // 修掉了原先两枚胶囊共用一个 @State、按一枚两枚同时缩放的问题。 + MotionBase({ pressEnabled: true, fillWidth: false }) { + Row({ space: 6 }) { + Image(icon) + .width(15) + .height(15) + .fillColor(this.palette().textSecondary) + .draggable(false) + Text(label) + .fontSize(12) + .fontColor(this.palette().textPrimary) + } + .padding({ left: 12, right: 14, top: 8, bottom: 8 }) + .backgroundColor(this.palette().navBarBg) + .borderRadius(RADIUS_PILL) + .border({ width: 1, color: this.palette().navBarBorder }) + .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 }) + .onClick(tap) } - .padding({ left: 12, right: 14, top: 8, bottom: 8 }) - .backgroundColor(this.palette().navBarBg) - .borderRadius(RADIUS_PILL) - .border({ width: 1, color: this.palette().navBarBorder }) - .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 }) - .onClick(tap) } /** 待发送附件预览条:缩略信息 + 一个移除按钮 */ @Builder PendingAttachmentChip() { - Row({ space: 8 }) { - Image(this.pendingIsImage ? $r('app.media.ic_image') : $r('app.media.ic_file')) - .width(15) - .height(15) - .fillColor(this.palette().accent) - .draggable(false) - Column({ space: 1 }) { - Text(this.pendingName) - .fontSize(12) - .fontColor(this.palette().textPrimary) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - Text(this.uploading ? '上传中...' : formatBytes(this.pendingSize)) - .fontSize(10) - .fontColor(this.palette().textMuted) - } - .alignItems(HorizontalAlign.Start) - .layoutWeight(1) - if (this.uploading) { - LoadingProgress() - .width(14) - .height(14) - .color(this.palette().accent) - } else { - Image($r('app.media.ic_close')) - .width(13) - .height(13) - .fillColor(this.palette().textMuted) + // 按压反馈交给 MotionBase(全宽预览条) + MotionBase({ pressEnabled: true }) { + Row({ space: 8 }) { + Image(this.pendingIsImage ? $r('app.media.ic_image') : $r('app.media.ic_file')) + .width(15) + .height(15) + .fillColor(this.palette().accent) .draggable(false) - .onClick(() => { - this.clearPendingFile(); - this.forceRefresh(); - }) + Column({ space: 1 }) { + Text(this.pendingName) + .fontSize(12) + .fontColor(this.palette().textPrimary) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Text(this.uploading ? '上传中...' : formatBytes(this.pendingSize)) + .fontSize(10) + .fontColor(this.palette().textMuted) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + if (this.uploading) { + LoadingProgress() + .width(14) + .height(14) + .color(this.palette().accent) + } else { + Image($r('app.media.ic_close')) + .width(13) + .height(13) + .fillColor(this.palette().textMuted) + .draggable(false) + .onClick(() => { + this.clearPendingFile(); + this.forceRefresh(); + }) + } } + .width('100%') + .padding({ left: 12, right: 12, top: 8, bottom: 8 }) + .margin({ bottom: 8 }) + .backgroundColor(this.palette().navBarBg) + .borderRadius(RADIUS_MD) + .border({ width: 1, color: this.palette().navBarBorder }) + .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 }) } - .width('100%') - .padding({ left: 12, right: 12, top: 8, bottom: 8 }) - .margin({ bottom: 8 }) - .backgroundColor(this.palette().navBarBg) - .borderRadius(RADIUS_MD) - .border({ width: 1, color: this.palette().navBarBorder }) - .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 }) } @Builder @@ -1487,6 +1516,8 @@ export struct ChatPage { .maxLines(24) .textOverflow({ overflow: TextOverflow.Ellipsis }) .wordBreak(WordBreak.BREAK_ALL) + // 面板内容靠 if 挂载:用 transition 在展开/收起时淡入淡出 + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -6 })).animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } // 流式思考中:扫光动画条(对齐 WebGUI reasoningSweep) @@ -1614,38 +1645,44 @@ export struct ChatPage { }) if (tc.open === true) { - if (tc.args.length > 0 && tc.args !== '{}') { - Text('参数') - .fontSize(9.5).fontWeight(FontWeight.Medium) - .fontColor(this.palette().textMuted) - .margin({ top: 6, bottom: 2 }) - Text(tc.args) - .fontSize(11) - .fontColor(this.palette().preText) - .backgroundColor(this.palette().preBg) - .borderRadius(4) - .padding({ left: 7, right: 7, top: 5, bottom: 5 }) - .width('100%') - .textAlign(TextAlign.Start) - .wordBreak(WordBreak.BREAK_ALL) - } - if (tc.result !== undefined && tc.result.length > 0) { - Text('结果') - .fontSize(9.5).fontWeight(FontWeight.Medium) - .fontColor(this.palette().textMuted) - .margin({ top: 6, bottom: 2 }) - Text(tc.result) - .fontSize(11) - .fontColor(this.palette().preText) - .backgroundColor(this.palette().preBg) - .borderRadius(4) - .padding({ left: 7, right: 7, top: 5, bottom: 5 }) - .width('100%') - .textAlign(TextAlign.Start) - .wordBreak(WordBreak.BREAK_ALL) - .maxLines(8) - .textOverflow({ overflow: TextOverflow.Ellipsis }) + Column() { + if (tc.args.length > 0 && tc.args !== '{}') { + Text('参数') + .fontSize(9.5).fontWeight(FontWeight.Medium) + .fontColor(this.palette().textMuted) + .margin({ top: 6, bottom: 2 }) + Text(tc.args) + .fontSize(11) + .fontColor(this.palette().preText) + .backgroundColor(this.palette().preBg) + .borderRadius(4) + .padding({ left: 7, right: 7, top: 5, bottom: 5 }) + .width('100%') + .textAlign(TextAlign.Start) + .wordBreak(WordBreak.BREAK_ALL) + } + if (tc.result !== undefined && tc.result.length > 0) { + Text('结果') + .fontSize(9.5).fontWeight(FontWeight.Medium) + .fontColor(this.palette().textMuted) + .margin({ top: 6, bottom: 2 }) + Text(tc.result) + .fontSize(11) + .fontColor(this.palette().preText) + .backgroundColor(this.palette().preBg) + .borderRadius(4) + .padding({ left: 7, right: 7, top: 5, bottom: 5 }) + .width('100%') + .textAlign(TextAlign.Start) + .wordBreak(WordBreak.BREAK_ALL) + .maxLines(8) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } } + .width('100%') + .alignItems(HorizontalAlign.Start) + // 展开内容整体用 if 挂载:transition 让参数/结果随 chevron 一起淡入 + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -6 })).animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } } .layoutWeight(1) @@ -1710,8 +1747,10 @@ export struct ChatPage { /** 工具卡/思考卡在气泡内是独占一行的块,气泡 78% 宽度对它们太窄 —— 见 BubbleBody */ private toggleToolCard(tc: ToolCallInfo): void { - tc.open = !(tc.open === true); - this.forceRefresh(); + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + tc.open = !(tc.open === true); + this.forceRefresh(); + }); } /** diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/DevicePage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/DevicePage.ets index defc85d..617dfdf 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/DevicePage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/DevicePage.ets @@ -6,7 +6,8 @@ import { noConnectionMessage } from '../common/UserError'; import { handleNavOnScroll } from '../common/NavBarController'; import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry'; import { DeviceInfo } from '../model/Model'; -import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT } from '../common/Constants'; +import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT, ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; +import { MotionBase } from '../components/MotionBase'; import { common } from '@kit.AbilityKit'; import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from '../components/PageTopBar'; import { SubPageLayer, NavGroup, NavRow, PlainCard, markSubPageOpen, subPageParam } from '../components/SubPage'; @@ -79,7 +80,9 @@ export struct DevicePage { // ignore context errors } deviceBridge.setStateListener((open: boolean) => { - this.bridgeConnected = open; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.bridgeConnected = open; + }); if (open) { this.lastError = ''; this.showToast('设备网关已连接', false); @@ -140,7 +143,9 @@ export struct DevicePage { * 所以用 replace 换页而不是叠栈 —— 否则返回手势要一层层退回去。 */ private openSub(id: string): void { - this.activeSub = id; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.activeSub = id; + }); if (this.isWide && this.navStack.size() > 0) { this.navStack.replacePathByName(id, subPageParam(id), false); } else { @@ -151,7 +156,9 @@ export struct DevicePage { /** 二级页面返回键:宽屏下左栏常驻可见,SubPageLayer 已隐藏返回键 */ private closeSub(): void { this.navStack.pop(); - this.activeSub = SUB_NONE; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.activeSub = SUB_NONE; + }); } /** Toggle local authorization flag; hello 同步到网关。 */ @@ -203,10 +210,15 @@ export struct DevicePage { } private showToast(msg: string, isError: boolean): void { - this.toastMsg = msg; + // 颜色标记必须在动画闭包外先落定,否则第一帧用的还是上一条 toast 的配色 this.toastIsError = isError; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.toastMsg = msg; + }); setTimeout(() => { - this.toastMsg = ''; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.toastMsg = ''; + }); }, 2500); } @@ -244,7 +256,9 @@ export struct DevicePage { list.push(info); } } - this.devices = list; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.devices = list; + }); } else { this.devices = []; } @@ -327,7 +341,9 @@ export struct DevicePage { } } else { this.navStack.clear(false); - this.activeSub = SUB_NONE; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.activeSub = SUB_NONE; + }); markSubPageOpen(false); } }) @@ -403,6 +419,9 @@ export struct DevicePage { .justifyContent(FlexAlign.End) .padding({ right: 20 }) .margin({ bottom: 166 }) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: 12 })) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } } @@ -499,10 +518,12 @@ export struct DevicePage { Row() { Circle({ width: 8, height: 8 }) .fill(this.authorized ? '#17A964' : '#E84026') + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) .margin({ right: 8 }) Text(this.authorized ? '已授权 — agent 可远程调用能力' : '未授权 — agent 将拒绝远程命令') .fontSize(12) .fontColor(this.authorized ? '#17A964' : this.palette().textMuted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) } .width('100%') .margin({ top: 12 }) @@ -551,36 +572,46 @@ export struct DevicePage { .fontSize(12) .fontColor('#E84026') .padding({ left: 4 }) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: -8 })) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } PlainCard({ caption: '操作' }) { Row() { - Button(this.bridgeConnected ? '断开' : '连接网关') - .height(34) - .fontSize(12) - .backgroundColor(this.bridgeConnected ? Color.Transparent : this.palette().accent) - .fontColor(this.bridgeConnected ? this.palette().textSecondary : Color.White) - .border({ - width: this.bridgeConnected ? 1 : 0, - color: this.palette().btnGhostBorder, - }) - .onClick(() => { - if (this.bridgeConnected) { - deviceBridge.disconnect(); - } else { - this.connectBridge(); - } - }) + // 缩放反馈由 MotionBase 统一;连接态的底色/字色切换仍需按钮自己缓动, + // 因为父容器的 .animation() 到不了子节点。 + MotionBase({ pressEnabled: true, fillWidth: false }) { + Button(this.bridgeConnected ? '断开' : '连接网关') + .height(34) + .fontSize(12) + .backgroundColor(this.bridgeConnected ? Color.Transparent : this.palette().accent) + .fontColor(this.bridgeConnected ? this.palette().textSecondary : Color.White) + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) + .border({ + width: this.bridgeConnected ? 1 : 0, + color: this.palette().btnGhostBorder, + }) + .onClick(() => { + if (this.bridgeConnected) { + deviceBridge.disconnect(); + } else { + this.connectBridge(); + } + }) + } Blank() - Button('刷新设备') - .height(34) - .fontSize(12) - .backgroundColor(Color.Transparent) - .border({ width: 1, color: this.palette().btnGhostBorder }) - .fontColor(this.palette().textSecondary) - .onClick(() => { - this.refreshDevices(); - }) + MotionBase({ pressEnabled: true, fillWidth: false }) { + Button('刷新设备') + .height(34) + .fontSize(12) + .backgroundColor(Color.Transparent) + .border({ width: 1, color: this.palette().btnGhostBorder }) + .fontColor(this.palette().textSecondary) + .onClick(() => { + this.refreshDevices(); + }) + } } .width('100%') @@ -600,31 +631,40 @@ export struct DevicePage { .fontSize(12) .fontColor(this.palette().textMuted) .padding({ left: 4 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } ForEach(this.devices, (dev: DeviceInfo) => { - PlainCard({ caption: '' }) { - Row() { - Circle({ width: 8, height: 8 }) - .fill(dev.online ? '#17A964' : '#77809A') - .margin({ right: 10 }) - Column() { - Text(dev.name.length > 0 ? dev.name : dev.deviceId) - .fontSize(14) - .fontColor(this.palette().textPrimary) - Text(dev.kind + (dev.authorized ? ' · 已授权' : ' · 未授权')) - .fontSize(11) - .fontColor(dev.authorized ? '#17A964' : this.palette().textMuted) - } - .layoutWeight(1) - .alignItems(HorizontalAlign.Start) + // PlainCard 是自定义组件,transition 不能直接挂在它上面(会生成 __Common__ 包装), + // 所以用一个无 padding、满宽的 Column 承载入场动画,布局不受影响。 + Column() { + PlainCard({ caption: '' }) { + Row() { + Circle({ width: 8, height: 8 }) + .fill(dev.online ? '#17A964' : '#77809A') + .margin({ right: 10 }) + Column() { + Text(dev.name.length > 0 ? dev.name : dev.deviceId) + .fontSize(14) + .fontColor(this.palette().textPrimary) + Text(dev.kind + (dev.authorized ? ' · 已授权' : ' · 未授权')) + .fontSize(11) + .fontColor(dev.authorized ? '#17A964' : this.palette().textMuted) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) - Text(dev.caps.length.toString() + ' 能力') - .fontSize(10) - .fontColor(this.palette().textMuted) + Text(dev.caps.length.toString() + ' 能力') + .fontSize(10) + .fontColor(this.palette().textMuted) + } + .width('100%') } - .width('100%') } + .width('100%') + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: 12 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) }, (dev: DeviceInfo) => dev.deviceId + dev.online.toString()) } diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets index ac9f814..e0ee45c 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/Index.ets @@ -8,6 +8,8 @@ import { navBar } from '../common/NavBarController'; import { handleBackPress } from '../common/NavStackRegistry'; import { ConnectionConfig } from '../model/Model'; import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_MIN_WIDTH, WIDE_NAV_BAR_WIDTH } from '../common/Constants'; +import { ANIM_NORMAL, ANIM_SLOW } from '../common/Constants'; +import { MotionBase } from '../components/MotionBase'; import { GradientBackground } from '../components/GradientBackground'; import { registerScreensueHandler, installCmdRouter } from '../common/BridgeRouter'; import { ScreensuePayload, snapshotComponentId } from '../common/BridgeCaps'; @@ -33,24 +35,32 @@ export struct RailTab { @Prop muted: string = ''; build() { - Column({ space: 3 }) { - Image(this.icon) - .width(22) - .height(22) - .fillColor(this.active ? this.accent : this.muted) - .draggable(false) + // 底部导航按压反馈:交给 MotionBase 统一(flexWeight 由外层调用点的 + // .layoutWeight(1) 负责,这里内部只需撑满自己那一格)。 + MotionBase({ pressEnabled: true }) { + Column({ space: 3 }) { + Image(this.icon) + .width(22) + .height(22) + .fillColor(this.active ? this.accent : this.muted) + // 选中态的颜色迁移必须写在子节点上:父容器的 .animation() + // 只驱动它自己的属性,不会下传给 Image / Text。 + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + .draggable(false) - Text(this.label) - .fontSize(10) - .fontWeight(this.active ? FontWeight.Medium : FontWeight.Normal) - .fontColor(this.active ? this.accent : this.muted) - .maxLines(1) + Text(this.label) + .fontSize(10) + // fontWeight 不可插值,切换时仍是跳变,只对颜色做过渡 + .fontWeight(this.active ? FontWeight.Medium : FontWeight.Normal) + .fontColor(this.active ? this.accent : this.muted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + .maxLines(1) + } + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Center) + .width('100%') + .height(48) } - .justifyContent(FlexAlign.Center) - .alignItems(HorizontalAlign.Center) - .width('100%') - .height(48) - .animation({ duration: 200, curve: Curve.EaseOut }) } } @@ -325,7 +335,7 @@ struct Index { // 所有页面滑动时隐藏导航栏 .translate({ y: !this.navVisible ? 130 : 0 }) .opacity(!this.navVisible ? 0 : 1) - .animation({ duration: 400, curve: Curve.EaseOut }) + .animation({ duration: ANIM_SLOW, curve: Curve.EaseOut }) // 导航栏本身不吃触摸空白区,避免遮住下层内容点击 .hitTestBehavior(HitTestMode.Transparent) diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets index 8b0543c..854331c 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/PluginsPage.ets @@ -5,6 +5,8 @@ import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry import { PluginRow, PluginDetail, emptyPluginDetail } from '../model/Model'; import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT } from '../common/Constants'; import { RADIUS_LG, RADIUS_MD, RADIUS_SM, RADIUS_PILL, COLOR_ERROR } from '../common/Constants'; +import { ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; +import { MotionBase } from '../components/MotionBase'; import { PageTopBar, NavFloatOverlay, NavFloatRow, GlassShell, FloatIconButton } from '../components/PageTopBar'; import { SubPageLayer, PlainCard, markSubPageOpen, subPageParam } from '../components/SubPage'; import { SettingsEditor } from '../components/SettingsEditor'; @@ -184,11 +186,17 @@ export struct PluginsPage { }; rows.push(row); } - this.plugins = rows; + // 列表整体重建(ForEach key 含 loaded/disabled,启停会整行重挂载): + // 放进 animateTo 让新旧行走 transition 交叉淡入,而不是硬切一帧。 + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.plugins = rows; + }); } catch (e) { this.showToast(userMessage('plugins.load', e), true); } - this.loading = false; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.loading = false; + }); } /** 徽标状态:与 GUI 一致 —— 已加载绿 / 禁用待生效黄 / 已禁用红 / 未加载灰。 */ @@ -239,7 +247,9 @@ export struct PluginsPage { await apiClient.post('/plugins', bodyObj); this.showToast('安装请求已发送', false); this.installUrl = ''; - this.showInstallForm = false; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.showInstallForm = false; + }); this.loadPlugins(); } catch (e) { this.showToast(userMessage('plugins.install', e), true); @@ -293,7 +303,10 @@ export struct PluginsPage { d.deprecated = o['deprecated'] as boolean ?? false; d.tags = this.strArray(o['tags']); d.files = this.strArray(o['files']); - this.detail = d; + // 详情字段一次性落地:条件卡片在 animateTo 帧内挂载,V1 给它们默认透明度过渡 + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.detail = d; + }); } catch (e) { // 内置插件没有清单,属于预期情况,不当成错误刷红 const row: PluginRow | null = this.findRow(name); @@ -303,12 +316,15 @@ export struct PluginsPage { d.version = row.version ?? ''; d.description = row.description ?? ''; } - this.detail = d; - if (row !== null && row.external) { - this.detailError = userMessage('plugins.detail', e); - } + const errText: string = (row !== null && row.external) ? userMessage('plugins.detail', e) : ''; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.detail = d; + this.detailError = errText; + }); } - this.detailLoading = false; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.detailLoading = false; + }); } private strArray(raw: Object | undefined): string[] { @@ -351,10 +367,15 @@ export struct PluginsPage { } private showToast(msg: string, isError: boolean): void { - this.toastMsg = msg; + // 颜色标志必须在 animateTo 闭包之外先落地,否则第一帧配色是上一次的 this.toastIsError = isError; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.toastMsg = msg; + }); setTimeout(() => { - this.toastMsg = ''; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.toastMsg = ''; + }); }, 2500); } @@ -404,6 +425,8 @@ export struct PluginsPage { .width(20) .height(20) .fillColor(Color.White) + .rotate({ angle: this.showInstallForm ? 45 : 0 }) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) } .width(50) .height(50) @@ -411,7 +434,9 @@ export struct PluginsPage { .backgroundColor(this.palette().accent) .shadow({ radius: 24, color: this.palette().shadow, offsetY: 8 }) .onClick(() => { - this.showInstallForm = !this.showInstallForm; + this.getUIContext().animateTo({ duration: ANIM_ENTER, curve: Curve.EaseOut }, () => { + this.showInstallForm = !this.showInstallForm; + }); }) } } @@ -510,6 +535,9 @@ export struct PluginsPage { .border({ width: 1, color: this.palette().navBarBorder }) .shadow({ radius: 20, color: this.palette().shadow, offsetY: 6 }) .alignItems(VerticalAlign.Center) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: 12 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) } /** 加载中 / 未配置 / 空列表三种占位态 */ @@ -521,6 +549,7 @@ export struct PluginsPage { .height(32) .color(this.palette().accent) .margin({ top: 40 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } if (!apiClient.hasConnection()) { @@ -528,6 +557,7 @@ export struct PluginsPage { .fontSize(13) .fontColor(this.palette().textMuted) .padding(20) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } if (!this.loading && this.plugins.length === 0 && apiClient.hasConnection()) { @@ -535,6 +565,7 @@ export struct PluginsPage { .fontSize(13) .fontColor(this.palette().textMuted) .padding(20) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } } @@ -545,62 +576,78 @@ export struct PluginsPage { @Builder PluginList() { ForEach(this.plugins, (plugin: PluginRow) => { - Row() { - Column({ space: 3 }) { - Row({ space: 6 }) { - Text(plugin.name) - .fontSize(15) - .fontWeight(this.activeName === plugin.name ? FontWeight.Medium : FontWeight.Normal) - .fontColor(this.activeName === plugin.name - ? this.palette().accent : this.palette().textPrimary) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - if (plugin.version !== undefined && plugin.version.length > 0) { - Text('v' + plugin.version) - .fontSize(10) - .fontColor(this.palette().textMuted) + // 外层 Column 只为承载 transition:.transition() 不能直接挂在自定义组件 + // 调用点上(会生成 __Common__ 包装节点)。按压缩放由 MotionBase 统一提供, + // 每行自带独立按压态,不再需要 pressedName 这种"哪一行被按"的手工记账。 + Column() { + MotionBase({ pressEnabled: true }) { + Row() { + Column({ space: 3 }) { + Row({ space: 6 }) { + Text(plugin.name) + .fontSize(15) + .fontWeight(this.activeName === plugin.name ? FontWeight.Medium : FontWeight.Normal) + .fontColor(this.activeName === plugin.name + ? this.palette().accent : this.palette().textPrimary) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) + if (plugin.version !== undefined && plugin.version.length > 0) { + Text('v' + plugin.version) + .fontSize(10) + .fontColor(this.palette().textMuted) + } + } + // 徽标全部去掉(用户要求):状态用一个 3vp 圆点表达, + // 其余信息退化为一行灰字副标题 —— 列表只负责"选谁",细节看详情页。 + Row({ space: 6 }) { + Circle({ width: 6, height: 6 }) + .fill(this.statusDotColor(plugin)) + Text(this.rowSubtitle(plugin)) + .fontSize(11) + .fontColor(this.palette().textMuted) + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + } + .width('100%') + .alignItems(VerticalAlign.Center) } - } - // 徽标全部去掉(用户要求):状态用一个 3vp 圆点表达, - // 其余信息退化为一行灰字副标题 —— 列表只负责"选谁",细节看详情页。 - Row({ space: 6 }) { - Circle({ width: 6, height: 6 }) - .fill(this.statusDotColor(plugin)) - Text(this.rowSubtitle(plugin)) - .fontSize(11) - .fontColor(this.palette().textMuted) - .maxLines(1) - .textOverflow({ overflow: TextOverflow.Ellipsis }) - .layoutWeight(1) + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Image($r('app.media.ic_chevron_right')) + .width(15) + .height(15) + .fillColor(this.activeName === plugin.name + ? this.palette().accent : this.palette().textMuted) + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) + .draggable(false) } .width('100%') + .padding(14) + .borderRadius(RADIUS_LG) + .backgroundColor(this.activeName === plugin.name + ? this.palette().accentBg : this.palette().bgCard) + .border({ + width: 1, + color: this.activeName === plugin.name + ? this.palette().accent : this.palette().glassBorder, + }) + // 选中态的底色/描边渐变:MotionBase 的 .animation() 到不了这里 + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) .alignItems(VerticalAlign.Center) + .onClick(() => { + this.openDetail(plugin.name); + }) } - .layoutWeight(1) - .alignItems(HorizontalAlign.Start) - - Image($r('app.media.ic_chevron_right')) - .width(15) - .height(15) - .fillColor(this.activeName === plugin.name - ? this.palette().accent : this.palette().textMuted) - .draggable(false) } .width('100%') - .padding(14) - .borderRadius(RADIUS_LG) - .backgroundColor(this.activeName === plugin.name - ? this.palette().accentBg : this.palette().bgCard) - .border({ - width: 1, - color: this.activeName === plugin.name - ? this.palette().accent : this.palette().glassBorder, - }) .margin({ bottom: 10 }) - .alignItems(VerticalAlign.Center) - .onClick(() => { - this.openDetail(plugin.name); - }) + // ForEach key 含 loaded/disabled:启停会整行重挂载,靠 transition 变成交叉淡入 + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: 12 })) + .animation({ duration: ANIM_ENTER, curve: Curve.EaseOut })) }, (plugin: PluginRow) => plugin.name + (plugin.loaded ? 'L' : '') + (plugin.disabled ? 'D' : '')) } @@ -621,6 +668,7 @@ export struct PluginsPage { .width('100%') .justifyContent(FlexAlign.Center) .padding({ top: 30, bottom: 30 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } if (this.detailError.length > 0) { @@ -628,6 +676,7 @@ export struct PluginsPage { .fontSize(12) .fontColor(COLOR_ERROR) .padding({ left: 4, bottom: 12 }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } // 概览卡:名称、版本、状态徽标、描述 @@ -786,6 +835,7 @@ export struct PluginsPage { }) .fontColor(this.activeIsDisabled() ? this.palette().textSecondary : '#D99A2B') + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) .onClick(() => { const r: PluginRow | null = this.activeRow(); if (r !== null) { @@ -806,6 +856,7 @@ export struct PluginsPage { this.removePlugin(r); } }) + .transition(TransitionEffect.OPACITY.animation({ duration: ANIM_FAST, curve: Curve.EaseOut })) } } .width('100%') @@ -892,6 +943,9 @@ export struct PluginsPage { .justifyContent(FlexAlign.End) .padding({ right: 20 }) .margin({ bottom: 166 }) + .transition(TransitionEffect.OPACITY + .combine(TransitionEffect.translate({ y: 12 })) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } } diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets index d2222dc..2a5470f 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets +++ b/cmd/ohos/HomeAgent/entry/src/main/ets/pages/SettingsPage.ets @@ -5,6 +5,8 @@ import { handleNavOnScroll } from '../common/NavBarController'; import { registerNavStack, unregisterNavStack } from '../common/NavStackRegistry'; import { ConnectionConfig, AppSettings, emptySettings } from '../model/Model'; import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, applyThemeMode, WIDE_NAV_BAR_WIDTH, WIDE_MIN_CONTENT } from '../common/Constants'; +import { ANIM_FAST, ANIM_NORMAL, ANIM_ENTER } from '../common/Constants'; +import { MotionBase } from '../components/MotionBase'; import { picker, fileIo } from '@kit.CoreFileKit'; import { common } from '@kit.AbilityKit'; import { PageTopBar, NavFloatOverlay, NavFloatRow, FloatIconButton } from '../components/PageTopBar'; @@ -572,10 +574,15 @@ export struct SettingsPage { } private showToast(msg: string, isError: boolean): void { - this.toastMsg = msg; + // 颜色标记必须在 animateTo 之外先定好,否则第一帧会用上一条 toast 的配色 this.toastIsError = isError; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.toastMsg = msg; + }); setTimeout(() => { - this.toastMsg = ''; + this.getUIContext().animateTo({ duration: ANIM_NORMAL, curve: Curve.EaseOut }, () => { + this.toastMsg = ''; + }); }, 2500); } @@ -748,6 +755,8 @@ export struct SettingsPage { .justifyContent(FlexAlign.End) .padding({ right: 20 }) .margin({ bottom: 166 }) + // if 控制的节点靠 transition 做进出场,配合 showToast 里的 animateTo + .transition(TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: 12 })).animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut })) } } @@ -1139,23 +1148,31 @@ export struct SettingsPage { @Builder ThemeOption(label: string, mode: string) { - Column() { - Text(label) - .fontSize(12) - .fontColor(this.themeMode === mode ? Color.White : this.palette().textSecondary) + // 按压缩放由 MotionBase 统一;flexWeight: 1 让三枚选项在 Row 里继续等分。 + MotionBase({ pressEnabled: true, flexWeight: 1 }) { + Column() { + Text(label) + .fontSize(12) + .fontColor(this.themeMode === mode ? Color.White : this.palette().textSecondary) + // 子节点的颜色迁移要自己声明:父容器的 .animation() 不下传 + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + } + .justifyContent(FlexAlign.Center) + .width('100%') + .height(34) + .borderRadius(RADIUS_MD) + .backgroundColor(this.themeMode === mode ? this.palette().accent : this.palette().bgHover) + // 三选一的选中态迁移:底色与描边一起过渡。写在 .border 之后、 + // 覆盖它上面的所有状态驱动属性。 + .border({ + width: 1, + color: this.themeMode === mode ? this.palette().accent : this.palette().btnGhostBorder, + }) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) + .onClick(() => { + this.applyTheme(mode); + }) } - .justifyContent(FlexAlign.Center) - .layoutWeight(1) - .height(34) - .borderRadius(RADIUS_MD) - .backgroundColor(this.themeMode === mode ? this.palette().accent : this.palette().bgHover) - .border({ - width: 1, - color: this.themeMode === mode ? this.palette().accent : this.palette().btnGhostBorder, - }) - .onClick(() => { - this.applyTheme(mode); - }) } @Builder @@ -1200,6 +1217,7 @@ export struct SettingsPage { Text(entry.value === 'true' ? 'true' : 'false') .fontSize(12) .fontColor(entry.value === 'true' ? '#17A964' : this.palette().textMuted) + .animation({ duration: ANIM_NORMAL, curve: Curve.EaseOut }) Blank() Toggle({ type: ToggleType.Switch, isOn: entry.value === 'true' }) .selectedColor(this.palette().accent) @@ -1223,6 +1241,8 @@ export struct SettingsPage { .margin({ right: 6, bottom: 6 }) .backgroundColor(entry.value === opt ? this.palette().accent : this.palette().bgHover) .fontColor(entry.value === opt ? Color.White : this.palette().textSecondary) + // 选中项迁移:底色与字色一起过渡,避免整排选项同时硬切 + .animation({ duration: ANIM_FAST, curve: Curve.EaseOut }) .onClick(() => { this.saveSetting(entry, opt); }) diff --git a/cmd/ohos/HomeAgent/entry/src/main/module.json5 b/cmd/ohos/HomeAgent/entry/src/main/module.json5 index 56ce176..6dcbc37 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/module.json5 +++ b/cmd/ohos/HomeAgent/entry/src/main/module.json5 @@ -24,9 +24,11 @@ "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "HomeAgent main entry", - "icon": "$media:app_icon", + "icon": "$media:layered_image", "label": "$string:app_name", - "startWindowIcon": "$media:app_icon", + // 启动页图标用透明底字形,配合随主题切换的 start_window_background, + // 浅色/深色模式下遮罩与图标都能对上,不再恒为深色样式。 + "startWindowIcon": "$media:start_icon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ diff --git a/cmd/ohos/HomeAgent/entry/src/main/resources/base/element/color.json b/cmd/ohos/HomeAgent/entry/src/main/resources/base/element/color.json index abcff3b..5ad94d7 100644 --- a/cmd/ohos/HomeAgent/entry/src/main/resources/base/element/color.json +++ b/cmd/ohos/HomeAgent/entry/src/main/resources/base/element/color.json @@ -2,7 +2,7 @@ "color": [ { "name": "start_window_background", - "value": "#0B1020" + "value": "#F1F3F5" } ] } diff --git a/cmd/ohos/HomeAgent/entry/src/main/resources/base/media/start_icon.png b/cmd/ohos/HomeAgent/entry/src/main/resources/base/media/start_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5aa3e4339deca073475d804bbe7194931aeabb72 GIT binary patch literal 9741 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