折叠屏悬停看视频,上半屏和下半屏应该各做什么

折叠屏进入悬停姿态后,视频区域和控制区域有了天然分界,但播放会话不应该重新创建。

该工具是让媒体场景、播放阶段、进度、音量、请求版本和资源释放形成可以独立验证的业务链路。让播放器生命周期独立于页面形态,上屏专注内容,下屏承载时间轴、场景选择和输出设备。

先把状态从界面里拿出来

切换形态时重新prepare媒体,会造成画面闪烁、进度归零和多个播放器同时占用资源。这类问题通常不是某个组件颜色或间距造成的,而是业务状态没有明确归属。页面只应表达结果,控制器负责判断动作是否有效以及下一步进入什么阶段。

第一步先定义媒体场景、播放阶段、进度、音量、请求版本和资源释放。模型中保留稳定标识和必要边界字段,数组顺序只用于展示,不能承担业务身份。

ts 复制代码
export type PlaybackPhase = 'idle' | 'preparing' | 'ready' | 'playing' | 'paused' | 'completed' | 'error'

export interface MediaScene {
  id: string
  title: string
  durationMs: number
  source: string
}

export interface PlayerSnapshot {
  phase: PlaybackPhase
  sceneId: string
  positionMs: number
  durationMs: number
  volume: number
  errorMessage: string
}

export class FeatureController {
  private scenes: MediaScene[] = [
    { id: 'main', title: '悬停影院主场景', durationMs: 252000, source: 'resource://main' },
    { id: 'ambient', title: '环境声场', durationMs: 198000, source: 'resource://ambient' }
  ]
  private state: PlayerSnapshot = { phase: 'idle', sceneId: 'main', positionMs: 0, durationMs: 252000, volume: 0.72, errorMessage: '' }
  private requestVersion: number = 0

  getState(): PlayerSnapshot {

模型放进独立文件后,页面重排不会影响数据。即使同一份状态分别出现在紧凑窗口、宽屏工作区或穿戴界面中,读取方式仍然一致。

核心动作必须有边界

接下来处理同步进度音量。动作入口先验证目标对象和输入范围,确认有效以后再修改状态。失败时返回明确结果,不能让按钮文字变化代替真实处理。

这里不让组件直接改数组。控制器统一处理后,快捷键、触摸、表冠或系统回调都可以复用同一条逻辑,也更容易补充撤销和错误提示。

ts 复制代码
    return { phase: this.state.phase, sceneId: this.state.sceneId, positionMs: this.state.positionMs, durationMs: this.state.durationMs, volume: this.state.volume, errorMessage: this.state.errorMessage }
  }

  prepare(sceneId: string): boolean {
    const scene: MediaScene | undefined = this.scenes.find((item: MediaScene) => item.id === sceneId)
    if (!scene) {
      this.fail('找不到媒体资源')
      return false
    }
    this.requestVersion++
    this.state.phase = 'preparing'
    this.state.sceneId = scene.id
    this.state.durationMs = scene.durationMs
    this.state.positionMs = 0
    this.state.errorMessage = ''
    this.state.phase = 'ready'
    return true
  }

  togglePlayback(): PlaybackPhase {
    if (this.state.phase === 'playing') {
      this.state.phase = 'paused'
    } else if (this.state.phase === 'ready' || this.state.phase === 'paused') {
      this.state.phase = 'playing'
    }
    return this.state.phase
  }

以【切换形态时重新prepare媒体,会造成画面闪烁、进度归零和多个播放器同时占用资源】为例,控制器层保存稳定ID或请求版本后,页面就不会被异步顺序牵着走。

让首屏直接呈现项目能力

界面面向折叠屏、平板设计。上半屏保留沉浸画面,下半屏集中控制,展开模式恢复侧栏与大时间轴。运行截图使用稳定的演示数据,但显示的数据结构与功能控制层保持一致,因此后续替换为真实接口时不需要推翻布局。

首页先建立视觉中心,再安排状态和操作入口。主要数据使用更大的字号和高对比颜色,辅助信息保持清楚但不过度抢占注意力。代码中的布局区域也按职责展开,避免所有内容堆在一个长Column里。

ts 复制代码
import { FeatureController } from '../features/FeatureController'

interface TrackItem { title: string;
    meta: string;
    active: boolean }

@Entry
@Component
struct Index {
  private featureController: FeatureController = new FeatureController()
  private bars: number[] = [22, 48, 34, 76, 58, 92, 44, 68, 86, 52, 74, 38, 64, 88, 46, 70, 32, 54, 80, 42]
  private tracks: TrackItem[] = [
    { title: '城市夜航', meta: '01:28 / 04:12', active: true }, { title: '柔光序章', meta: '03:42', active: false }, { title: '夜色回响', meta: '04:18', active: false }
  ]

  build() {
    Row() {
      Column({ space: 20 }) {
        Row() { Stack() { Rect().width(38).height(38).radius(12).fill('#7C5CFC');
    Text('▶').fontSize(14).fontColor(Color.White) };
    Text('悬停影院').fontSize(18).fontWeight(FontWeight.Bold).fontColor(Color.White).margin({ left: 10 });
    Blank();
    Text('⌁').fontSize(22).fontColor('#8C96AC') }.width('100%')
        Text('FOLDABLE PLAYER').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#D86BFF').width('100%')
        ForEach(this.tracks, (track: TrackItem, index: number) => {
          Row({ space: 11 }) { Stack() { Rect().width(42).height(42).radius(12).fill(track.active ? '#7C5CFC' : '#2A3346');
    Text(index === 0 ? '▶' : '♪').fontSize(15).fontColor(Color.White) };
    Column({ space: 3 }) { Text(track.title).fontSize(15).fontWeight(track.active ? FontWeight.Bold : FontWeight.Normal).fontColor(track.active ? Color.White : '#B4BDCE');
    Text(track.meta).fontSize(12).fontColor('#717C93') }.alignItems(HorizontalAlign.Start).layoutWeight(1);
    if (track.active) { Circle({ width: 7, height: 7 }).fill('#D86BFF') } }.width('100%').padding(10).backgroundColor(track.active ? '#283148' : Color.Transparent).borderRadius(14)
        }, (track: TrackItem) => track.title)
        Blank()
        Column({ space: 8 }) { Text('OUTPUT').fontSize(11).fontColor('#67738B');
    Row() { Text('◉').fontColor('#D86BFF');
    Text('  空间声场').fontSize(14).fontColor('#C6CDDA') }.width('100%') }.width('100%').padding(12).backgroundColor('#222A3B').borderRadius(12)
      }.width(232).height('100%').padding(20).backgroundColor('#151B29')

      Column({ space: 18 }) {
        Row() { Column({ space: 4 }) { Text('FOLDABLE PLAYER').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#D86BFF');

这部分代码负责展示,不负责重新计算业务规则。FeatureController被页面持有以后,后续可以把返回状态接入@State或状态管理对象,视觉层只选择合适的颜色、尺寸和区域。

恢复、失败和资源清理

功能完整并不等于只有成功路径。按姿态重排区域完成后,还要考虑请求过期、输入无效、页面重建和设备离线。每个失败状态都要回答两个问题:当前数据是否还能保留,以及用户可以怎样继续。

ts 复制代码
  seek(targetMs: number): number {
    const next: number = Math.max(0, Math.min(this.state.durationMs, targetMs))
    this.state.positionMs = next
    if (next >= this.state.durationMs) {
      this.state.phase = 'completed'
    }
    return next
  }

  setVolume(value: number): number {
    const volume: number = Math.max(0, Math.min(1, value))
    this.state.volume = volume
    return volume
  }

  release(): void {
    this.requestVersion++
    this.state.phase = 'idle'
    this.state.positionMs = 0
  }

  private fail(message: string): void {
    this.state.phase = 'error'
    this.state.errorMessage = message
  }
}

可重试错误、不可用能力和正常空状态对应不同界面,也决定是否保留上一次成功结果。这样一次网络或设备波动不会直接把首屏清空。

页面销毁时同样需要收尾:取消计时、释放媒体实例、解除传感器监听或让传输版本失效。静态首屏不会暴露这类问题,但反复进入页面时会很快出现重复回调

工程拆分

页面与功能层分离后的目录如下。文章中的主要状态和方法均来自工程文件,设备专用能力可以继续在adapters目录增加实现。

text 复制代码
HoverVideoLab/
├─ entry/src/main/ets/
│  ├─ pages/Index.ets                 # 首屏与响应式布局
│  ├─ features/FeatureController.ets # 模型、动作和边界
│  ├─ entryability/EntryAbility.ets   # 页面入口与生命周期
│  └─ adapters/                       # 真机能力或数据源接口
├─ entry/src/main/resources/          # 颜色、文字和媒体资源
└─ build-profile.json5                # 工程构建配置

Index.ets只组合界面,FeatureController.ets处理媒体场景、播放阶段、进度、音量、请求版本和资源释放。如果后面接入真实设备或网络服务,只需增加适配器并把结果转换成控制器认识的状态。

相关推荐
benchmark_cc23 分钟前
Python量化实战:如何检测并剔除历史数据中的“闪崩/乌龙指”异常价格点?
开发语言·人工智能·python·量化·quantdash·量化数据源
2501_9378609427 分钟前
Java多线程初阶(下)—— synchronized、volatile、wait/notify与经典并发工具
java·开发语言·jvm
笨笨饿44 分钟前
#135_代码中的类型重定义艺术:从基础封装到函数类型设计
开发语言·stm32·单片机·嵌入式硬件·mcu·物联网·嵌入式实时数据库
统计学小王子1 小时前
R语言入门——ggplot2图例增加公式
开发语言·r语言
牛油果子哥q1 小时前
C++内存池与对象池精讲:内存碎片、自定义分配器、对象池实现、STL allocator原理、工程落地与性能对比
java·开发语言·c++
CodeStats1 小时前
《源纹天书》第三百六十三章至第三百六十四章
java·开发语言·源纹天书
万法若空1 小时前
C/C++ 类型别名定义方式对比
java·c语言·c++
alexhilton1 小时前
让架构边界变成可执行的测试
android·kotlin·android jetpack
2601_962072501 小时前
网络安全防护指南:筑牢网络安全防线(510)
安全·web安全·php