📑往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)

✒️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

✒️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

✒️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

✒️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

✒️ 记录一场鸿蒙开发岗位面试经历~

✒️ 持续更新中……


长视频应用视频详情页提供视频播放、评论互动和提供视频相关信息的功能。观察视频详情页在不同设备上的UX设计图,可以进行如下设计(图中为包括可滑动区域的内容):

  • 将视频详情页划分为5个区域,效果图如下:
示意图 sm md lg
效果图
  • 视频详情页在sm、md和lg断点下有较大差异,在sm和md断点下相关列表、全部评论和写评论区域均显示在视频下方,无视频简介区域;在lg断点下分为侧边栏和主内容区两部分,全部评论和写评论区域显示在右侧边栏,相关列表和视频简介区域显示在主内容区视频下方。
  • 视频详情页区域1为视频播放相关组件,占满整个主内容区。
  • 视频详情页区域2容器组件尺寸变化时,增加或减小的空间全部分配给标题栏的空白区域;内容按照先后顺序随容器组件尺寸变化显示或隐藏。
  • 视频详情页区域3中的图片在sm、md断点下大小不同,在lg断点下随侧边栏尺寸改变大小发生变化。
  • 视频详情页区域4的评论框均占满容器组件剩余空间。
  • 视频详情页区域5只在lg断点下显示,容器组件尺寸变化时,增加或减小的空间,均匀分配给视频简介的图标、全部分配给周边视频标题的空白区域;选集列表内容按照先后顺序随容器组件尺寸变化显示或隐藏。

长视频应用视频详情页的5个基础区域介绍及实现方案如下表所示:

区域编号 简介 实现方案
1 视频播放 使用视频播放 AVPlayer 和 XComponent 等相关组件,不详细介绍具体功能实现,读者可以自行查看代码。
2 相关列表 标题栏的空白区域使用Blank实现拉伸能力,视频列表通过 List组件 实现延伸能力,同 搜索页搜索发现。
3 全部评论 在不同断点下修改评论区图片的宽高和aspectRatio属性实现自适应布局的缩放能力。
4 写评论 layoutWeight实现自适应布局的拉伸能力,同 首页搜索框。
5 视频简介 图标 Row组件 的justifyContent属性设置为FlexAlign.SpaceBetween实现均分能力,选集列表通过 List组件实现延伸能力,周边视频使用layoutWeight实现拉伸能力。

在实际开发中,sm和md断点下的设计中,区域1-3和区域4为并列的视频详情页内容,所以对应的开发顺序为区域4,区域1-3;lg断点下的设计中,区域1、2、5和区域3-4是并列的视频详情页内容,所以对应的开发顺序为区域3-4,区域1、2、5。另外,视频详情页设计了额外的功能:边看边评的交互逻辑。

  • 全部评论

根据ux设计,全部评论在sm、md断点下显示在视频下方,在lg断点下显示在右侧边栏,实现侧边悬浮面板,使用SideBarContainer组件+断点控制组件显示的位置,同时侧边栏宽度可通过拖拽变宽。全部评论中的图片,在sm、md断点下使用不同的宽高固定值;lg断点下,在SideBarContainer的onAreaChange方法中按侧边栏的宽度变化百分比改变图片宽高,配合图片的aspectRatio属性控制等比放大或缩小,实现自适应布局的缩放能力。

效果图如下:

// features/VideoDetail/src/main/ets/view/VideoDetail.ets
// 侧边栏及主内容区
SideBarContainer() {
  Column() {
    Scroll() {
      // lg断点下的全部评论区
      AllComments({ commentImgHeight: $commentImgHeight, commentImgWidth: $commentImgWidth })
        .visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.Visible :
          Visibility.None)
    }

    SelfComment()
      .visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.Visible :
        Visibility.None)
  }
  // lg断点下,修改图片宽高
  .onAreaChange((newValue: Area) => {
    if (newValue.width !== 0) {
      let height: number = DetailConstants.COMMENT_IMAGE_MIN_HEIGHT_NUMBER + (Number(newValue.width) -
        DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) / (this.windowWidth * DetailConstants.COMMENTS_AREA_PERCENT -
        DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) * (DetailConstants.COMMENT_IMAGE_MAX_HEIGHT_NUMBER -
        DetailConstants.COMMENT_IMAGE_MIN_HEIGHT_NUMBER);
      let width: number = DetailConstants.COMMENT_IMAGE_MIN_WIDTH_NUMBER + (Number(newValue.width) -
        DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) / (this.windowWidth * DetailConstants.COMMENTS_AREA_PERCENT -
        DetailConstants.SIDE_BAR_MIN_WIDTH_NUMBER) * (DetailConstants.COMMENT_IMAGE_MAX_WIDTH_NUMBER -
        DetailConstants.COMMENT_IMAGE_MIN_WIDTH_NUMBER);
      this.commentImgHeight = JSON.stringify(height);
      this.commentImgWidth = JSON.stringify(width);
    }
  })
  Column() {
    // sm、md断点下全部评论区
    VideoDetail({ screenHeight: this.screenHeight, avPlayerUtil: this.avPlayerUtil, relatedVideoHeight:
      $relatedVideoHeight, videoHeight: $videoHeight })
      .layoutWeight(1)
    SelfComment()
      .visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.None :
        Visibility.Visible)
  }
}
// 控制右侧边栏只在lg断点下显示
.showSideBar(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? true : false)
// 右侧边栏的最小、最大宽度
.minSideBarWidth($r('app.float.side_bar_min_width'))
.maxSideBarWidth(px2vp(this.windowWidth * DetailConstants.COMMENTS_AREA_PERCENT))

// features/VideoDetail/src/main/ets/view/AllComments.ets
// 全部评论区中图片
Image(item.getCommentImageSrc())
  // 设置不同断点下的固定值或可变宽高
  .width(new BreakpointType(DetailConstants.COMMENT_IMAGE_WIDTH[0], DetailConstants.COMMENT_IMAGE_WIDTH[1],
    this.commentImgWidth).getValue(this.currentBreakpoint))
  .height(new BreakpointType(DetailConstants.COMMENT_IMAGE_HEIGHT[0],
    DetailConstants.COMMENT_IMAGE_HEIGHT[1], this.commentImgHeight).getValue(this.currentBreakpoint))
  // 控制lg断点下图片等比放大或缩小,实现缩放能力
  .aspectRatio(DetailConstants.COMMENT_IMAGE_ASPECT_RATIO)
  • 视频简介

视频简介区域只在lg断点下显示,其中视频简介的图标设置Row组件的justifyContent属性为FlexAlign.SpaceBetween实现均分能力。选集列表使用List组件实现自适应布局的延伸能力,随容器组件尺寸变大,可以看到的集数相应增加,用户也可以通过手指滑动触发列表滑动,查看被隐藏的子节点。周边视频标题栏的空白区域随容器组件尺寸变化而伸缩,使用Blank实现自适应布局的拉伸能力。

效果图如下:

示意图 lg
效果图
// features/VideoDetail/src/main/ets/view/RelatedList.ets
// 视频简介区域
@Builder
VideoIntroduction() {
  Column() {
    ...
    Row() {
      ForEach(this.iconList, (item: Resource, index: number) => {
        Image(item)
          .height($r('app.float.introduction_icon_size'))
          .width($r('app.float.introduction_icon_size'))
      }, (item: Resource, index: number) => index + JSON.stringify(item))
    }
    // 图标行实现均分能力
    .justifyContent(FlexAlign.SpaceBetween)
    ...
    // 选集列表实现延伸能力
    List({ space: CommonConstants.LIST_SPACE }) {
      ForEach(this.episodes, (item: string, index: number) => {
        ListItem() {
          ...
        }
      }, (item: string, index: number) => index + JSON.stringify(item))
    }
    .scrollBar(BarState.Off)
    .listDirection(Axis.Horizontal)
    // 周边视频标题栏
    this.SubTitleAndMore(DetailConstants.SUB_TITLES[5])
    ...
  }
  // 控制只在lg断点下显示
  .visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.Visible : Visibility.None)
}

@Builder
SubTitleAndMore(subtitle: string) {
  Row() {
    ...
    // 空白区域实现拉伸能力
    Blank()
    ...
  }
}
  • 边看边评的交互逻辑

视频详情页中,不同断点下页面下滑效果不同。在sm和md断点下,下滑时先隐藏相关列表区域,完全隐藏后视频区域等比缩小,缩至最小时固定,改为下滑全部评论区域;上滑时先上滑全部评论区,再等比放大视频区域,最后显示相关列表区域。在lg断点下,下滑时等比缩小视频区域,最小时固定改为下滑全部评论区域;上滑时先上滑全部评论区,再等比放大视频区域。这一效果通过 Scroll组件 的onScrollFrameBegin回调方法中控制相关组件的高度和偏移量实现。当组件高度变化时,返回偏移量为0;当全部评论区滑动时,返回实际偏移量。

// features/videoDetail/src/main/ets/view/RelatedList.ets
build() {
  Scroll(this.scroller) {
    Column() {
      this.RelatedVideoComponent()
      this.VideoIntroduction()
      AllComments({commentImgHeight: $commentImgHeight, commentImgWidth: $commentImgWidth})
        .visibility(this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG ? Visibility.None :
          Visibility.Visible)
    }
    .width(CommonConstants.FULL_PERCENT)
    .alignItems(HorizontalAlign.Start)
  }
  .layoutWeight(1)
  .scrollBar(BarState.Off)
  .onScrollFrameBegin((offset: number) => {
    if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_LG) {
      if ((offset > 0) && (this.videoHeight > DetailConstants.MIN_VIDEO_PERCENT)) {
        // 视频缩小逻辑
        // 滑动占屏幕高度百分比
        let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
        // 视频缩小百分比
        let heightOffset = offsetPercent < this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT ? offsetPercent :
          this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT;
        this.videoHeight = this.videoHeight - heightOffset;
        // 视频缩小百分比
        return { offsetRemain: 0 };
      } else if ((offset < 0) && (this.videoHeight < DetailConstants.MAX_VIDEO_PERCENT) &&
        (CurrentOffsetUtil.scrollToTop(JSON.stringify(this.scroller.currentOffset())))) {
        // 视频缩小百分比
        let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
        let heightOffset = offsetPercent < DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight ? offsetPercent :
          DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight;
        this.videoHeight = this.videoHeight + heightOffset;
        // 返回实际偏移量0
        return { offsetRemain: 0 };
      }
      return { offsetRemain: offset };
    } else {
      if ((offset > 0) && (this.videoHeight === DetailConstants.MAX_VIDEO_PERCENT) && (this.relatedVideoHeight > 0)) {
        // 相关列表缩小逻辑
        // 滑动占屏幕高度百分比
        let offsetPercent = (Math.abs(offset) * DetailConstants.TEN) / this.screenHeight;
        this.relatedVideoHeight = (this.relatedVideoHeight - offsetPercent * DetailConstants.RELATED_LIST_HEIGHT) <
          0 ? 0 : (this.relatedVideoHeight - offsetPercent * DetailConstants.RELATED_LIST_HEIGHT);
        // 返回实际偏移量0
        return { offsetRemain: 0 };
      } else if ((offset > 0) && (this.videoHeight > DetailConstants.MIN_VIDEO_PERCENT) &&
        (this.relatedVideoHeight === 0)) {
        // 视频缩小逻辑
        // 滑动占屏幕高度百分比
        let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
        // 视频缩小百分比
        let heightOffset = offsetPercent < this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT ? offsetPercent :
          this.videoHeight - DetailConstants.MIN_VIDEO_PERCENT;
        this.videoHeight = this.videoHeight - heightOffset;
        // 返回实际偏移量0
        return { offsetRemain: 0 };
      } else if ((offset > 0) && (this.videoHeight === DetailConstants.MIN_VIDEO_PERCENT) &&
        (this.relatedVideoHeight === 0)) {
        return { offsetRemain: offset };
      } else if ((offset < 0) && (this.videoHeight < DetailConstants.MAX_VIDEO_PERCENT) &&
        (this.relatedVideoHeight === 0)) {
        if (CurrentOffsetUtil.scrollToTop(JSON.stringify(this.scroller.currentOffset()))) {
          // 返回实际偏移量0
          let offsetPercent = (Math.abs(offset) * DetailConstants.MAX_VIDEO_PERCENT) / this.screenHeight;
          let heightOffset = offsetPercent < DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight ? offsetPercent :
            DetailConstants.MAX_VIDEO_PERCENT - this.videoHeight;
          this.videoHeight = this.videoHeight + heightOffset;
          // 返回实际偏移量0
          return { offsetRemain: 0 };
        }
        return { offsetRemain: offset };
      } else if ((offset < 0) && (this.videoHeight === DetailConstants.MAX_VIDEO_PERCENT) &&
        (this.relatedVideoHeight >= 0)) {
        // 相关列表放大逻辑
        // 滑动占屏幕高度百分比
        let offsetPercent = (Math.abs(offset) * DetailConstants.TEN) / this.screenHeight;
        this.relatedVideoHeight = (this.relatedVideoHeight + offsetPercent * DetailConstants.RELATED_LIST_HEIGHT) >
          DetailConstants.RELATED_LIST_HEIGHT ? DetailConstants.RELATED_LIST_HEIGHT : (this.relatedVideoHeight +
          offsetPercent * DetailConstants.RELATED_LIST_HEIGHT);
        // 返回实际偏移量0
        return { offsetRemain: 0 };
      }
      return { offsetRemain: 0 };
    }
  })
}
  • 全屏播放不旋转

在折叠屏展开态这种方形尺寸屏幕的设备上,点击“全屏播放”按钮,视频画面不旋转。在折叠屏展开态下,视频详情页进入全屏播放页不旋转,但是跟随系统传感器方向自动旋转,且受控制中心的旋转开关控制。使用window.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED)设置。首页和视频详情页在其他设备上或折叠屏其他折叠态时竖屏显示,使用window.setPreferredOrientation(window.Orientation.PORTRAIT)设置。

// features/videoDetail/src/main/ets/view/VideoDetail.ets
@Component
export struct VideoDetail {
  aboutToAppear() {
    this.windowUtil = WindowUtil.getInstance();
    if (this.windowUtil === undefined) {
      return;
    }
    // 判断为折叠屏展开态,设置窗口跟随传感器自动旋转,且受旋转开关控制
    if (this.currentBreakpoint === BreakpointConstants.BREAKPOINT_MD && display.isFoldable()) {
      this.windowUtil.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
    }
  }
}

// products/phone/src/main/ets/entryability/EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage) {
  let windowUtil: WindowUtil | undefined = WindowUtil.getInstance();
  if (windowUtil !== undefined) {
    windowUtil.setWindowStage(windowStage);
    // 设置竖屏显示
    windowUtil.setMainWindowPortrait();
  }
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐