【HarmonyOS】高仿华为阅读app翻页demo

【HarmonyOS】鸿蒙高仿华为阅读翻页_鸿蒙

src/main/ets/entryability/EntryAbility.ets

import { window } from '@kit.ArkUI';
import { UIAbility } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    let windowClass = windowStage.getMainWindowSync()
    let statusBarHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height
    let navigationIndicatorHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height
    AppStorage.setOrCreate('statusBarHeight', statusBarHeight) //保存状态栏高度,单位px
    AppStorage.setOrCreate('navigationIndicatorHeight', navigationIndicatorHeight) //保存底部导航条的高度,单位px
    windowClass.setWindowSystemBarEnable([]); //'status' | 'navigation'
    windowStage.loadContent('pages/Page40');
  }
}

src/main/ets/pages/Page40.ets

import { promptAction } from '@kit.ArkUI'
import { batteryInfo, systemDateTime } from '@kit.BasicServicesKit'

@Entry
@Component
struct Page40 {
  // 页面信息
  @Provide info: string =
    '设计理念\n在万物互联的时代,我们每天都会接触到很多不同形态的设备,每种设备在特定的场景下能够为我们解决一些特定的问题,表面看起来我们能够做到的事情更多了,但每种设备在使用时都是孤立的,提供的服务也都局限于特定的设备,我们的生活并没有变得更好更便捷,反而变得非常复杂。HarmonyOS 的诞生旨在解决这些问题,在纷繁复杂的世界中回归本源,建立平衡,连接万物。\n混沌初开,一生二、二生三、三生万物,我们希望通过 HarmonyOS 为用户打造一个和谐的数字世界——One Harmonious Universe。\nOne\n万物归一,回归本源。我们强调以人为本的设计,通过严谨的实验探究体验背后的人因,并将其结论融入到我们的设计当中。\nHarmonyOS 系统的表现应该符合人的本质需求。结合充分的人因研究,为保障全场景多设备的舒适体验,在整个系统中,各种大小的文字都清晰易读,图标精确而清晰、色彩舒适而协调、动效流畅而生动。同时,界面元素层次清晰,能巧妙地突出界面的重要内容,并能传达元素可交互的感觉。另外,系统的表现应该是直觉的,用户在使用过程中无需思考。因此系统的操作需要符合人的本能,并且使用智能化的技术能力主动适应用户的习惯。\nHarmonious\n一生为二,平衡共生。万物皆有两面,虚与实、阴与阳、正与反... 二者有所不同却可以很好地融合,达至平衡。\n在 HarmonyOS 中,我们希望给用户带来和谐的视觉体验。我们在物理世界中找到在数字世界中的映射,通过光影、材质等设计转化到界面设计中,给用户带来高品质的视觉享受。同时,物理世界中的体验记忆转化到虚拟世界中,熟悉的印象有助于帮助用户快速理解界面元素并完成相应的操作。\nUniverse\n三生万物,演化自如。HarmonyOS 是面向多设备体验的操作系统,因此,给用户提供舒适便捷的多设备操作体验是 HarmonyOS 区别于其他操作系统的核心要点。\n一方面,界面设计/组件设计需要拥有良好的自适应能力,可快速进行不同尺寸屏幕的开发。\n另一方面,我们希望多设备的体验能在一致性与差异性中取得良好的平衡。\n● 一致性:界面中的元素设计以及交互方式尽量保持一致,以便减少用户的学习成本。\n● 差异性:不同类型的设备在屏幕尺寸、交互方式、使用场景、用户人群等方面都会存在一定的差异性,为了给用户提供合适的操作体验,我们需要针对不同类型的设备进行差异化的设计。\n同时,HarmonyOS 作为面向全球用户的操作系统,为了让更多的用户享受便利的科技与愉悦的体验,我们将在数字健康、全球化、无障碍等方面进行积极的探索与思考。'
  @Provide lineHeight: number = 0 // 单行文本的高度
  @Provide pageHeight: number = 0 // 每页的最大高度
  @Provide totalContentHeight: number = 0 // 整个文本内容的高度
  @Provide textContent: string = " " // 文本内容,默认一个空格是为了计算单行文本的高度
  @Provide @Watch('totalPagesChanged') totalPages: number = 1 // 总页数
  //=====页面切换动画=====
  @State currentPage: number = 0 // 当前页数
  private DISPLAY_COUNT: number = 1
  private MIN_SCALE: number = 0.75
  @State pages: string[] = []
  @State opacityList: number[] = []
  @State scaleList: number[] = []
  @State translateList: number[] = []
  @State zIndexList: number[] = []
  //=====定时器=====
  timeIntervalId: number = 0
  @Provide timeStr: string = ""
  @Provide batterySOC: string = ""
  //======左右滑动判断======
  @State screenStartX: number = 0

  totalPagesChanged() { // 总页数变化时更新
    this.pages = new Array(this.totalPages).fill('');
  }

  aboutToDisappear(): void {
    clearInterval(this.timeIntervalId)
  }

  aboutToAppear(): void {
    this.timeIntervalId = setInterval(() => {
      let timestamp = systemDateTime.getTime(true) / 1000000 //因为获取的是纳秒 所以要 / 1000000
      // console.info(`timestamp:${timestamp}`)
      const date = new Date(timestamp);
      const hours = ('0' + date.getHours()).slice(-2);
      const minutes = ('0' + date.getMinutes()).slice(-2);

      this.timeStr = `${hours}:${minutes}`
      this.batterySOC = `电量${batteryInfo.batterySOC}%`
    }, 1000, 0)


    for (let i = 0; i < this.pages.length; i++) {
      this.opacityList.push(1.0)
      this.scaleList.push(1.0)
      this.translateList.push(0.0)
      this.zIndexList.push(0)
    }
  }

  build() {
    Stack() {
      Page40Child()// 自定义动画变化透明度、缩放页面、抵消系统默认位移、渲染层级等
        .width('100%').height('100%').visibility(Visibility.Hidden)

      Swiper() {
        ForEach(this.pages, (item: string, index: number) => {
          Page40Child({ index: index })// 自定义动画变化透明度、缩放页面、抵消系统默认位移、渲染层级等
            .opacity(this.opacityList[index])
            .scale({ x: this.scaleList[index], y: this.scaleList[index] })
            .translate({ x: this.translateList[index] })
            .zIndex(this.zIndexList[index])
        })
      }
      .onTouch((e) => {
        if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置
          this.screenStartX = e.touches[0].x;
        } else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置
          let lastScreenX = e.changedTouches[0].x;
          if (this.screenStartX < lastScreenX && this.currentPage === 0) {
            promptAction.showToast({ message: "没有上一页了" });
          } else if (this.screenStartX > lastScreenX && this.currentPage === this.totalPages - 1) {
            promptAction.showToast({ message: "没有下一页了" });
          }
        }
      })
      .onChange((index: number) => {
        console.info(index.toString())
        this.currentPage = index
      })
      .loop(false)
      // .height(300)
      .layoutWeight(1)
      .indicator(false)
      .displayCount(this.DISPLAY_COUNT, true)
      .customContentTransition({
        // 页面移除视窗时超时1000ms下渲染树
        // timeout: 1000,
        // 对视窗内所有页面逐帧回调transition,在回调中修改opacity、scale、translate、zIndex等属性值,实现自定义动画
        transition: (proxy: SwiperContentTransitionProxy) => {
          if (proxy.position <= proxy.index % this.DISPLAY_COUNT ||
            proxy.position >= this.DISPLAY_COUNT + proxy.index % this.DISPLAY_COUNT) {
            // 同组页面往左滑或往右完全滑出视窗外时,重置属性值
            this.opacityList[proxy.index] = 1.0
            this.scaleList[proxy.index] = 1.0
            this.translateList[proxy.index] = 0.0
            this.zIndexList[proxy.index] = 0
          } else {
            // 同组页面往右滑且未滑出视窗外时,对同组中左右两个页面,逐帧根据position修改属性值,实现两个页面往Swiper中间靠拢并透明缩放的自定义切换动画
            if (proxy.index % this.DISPLAY_COUNT === 0) {
              this.opacityList[proxy.index] = 1 - proxy.position / this.DISPLAY_COUNT
              this.scaleList[proxy.index] =
                this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - proxy.position / this.DISPLAY_COUNT)
              this.translateList[proxy.index] =
                -proxy.position * proxy.mainAxisLength + (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            } else {
              this.opacityList[proxy.index] = 1 - (proxy.position - 1) / this.DISPLAY_COUNT
              this.scaleList[proxy.index] =
                this.MIN_SCALE + (1 - this.MIN_SCALE) * (1 - (proxy.position - 1) / this.DISPLAY_COUNT)
              this.translateList[proxy.index] = -(proxy.position - 1) * proxy.mainAxisLength -
                (1 - this.scaleList[proxy.index]) * proxy.mainAxisLength / 2.0
            }
            this.zIndexList[proxy.index] = -1
          }
        }
      })
      .width('100%')
      .height('100%')
    }.width('100%').height('100%')
  }
}

@Component
struct Page40Child {
  @Consume info: string
  @Consume lineHeight: number // 单行文本的高度
  @Consume pageHeight: number // 每页的最大高度
  @Consume totalContentHeight: number // 整个文本内容的高度
  @Consume textContent: string // 文本内容,默认一个空格是为了计算单行文本的高度
  @Consume totalPages: number // 总页数
  @Consume timeStr: string
  @Consume batterySOC: string
  @State scrollOffset: number = 0 // 当前滚动偏移量
  @Prop index: number = 0
  scroller: Scroller = new Scroller() // 滚动条实例

  resetMaxLineHeight() {
    if (this.lineHeight > 0 && this.pageHeight > 0 && this.totalContentHeight > 0) {
      this.pageHeight = (Math.floor(this.pageHeight / this.lineHeight)) * this.lineHeight
      this.totalPages = Math.ceil(this.totalContentHeight / this.pageHeight) //向上取整得到总页数
    }
  }

  aboutToAppear(): void {
    this.scrollOffset = -(this.pageHeight * this.index)
  }

  build() {
    Column() {
      Text().width('100%').height(`${AppStorage.get('statusBarHeight')}px`) //顶部状态栏高度
      Text('通用设计基础')
        .fontColor("#7a7a7a")
        .fontSize(10)
        .padding({ left: 30, top: 10, bottom: 10 })
        .width('100%')
      Column() {
        Scroll(this.scroller) {
          Column() {
            Text(this.textContent)
              .fontSize(18)
              .lineHeight(36)
              .fontColor(Color.Black)
              .margin({ top: this.scrollOffset })
              .onAreaChange((oldArea: Area, newArea: Area) => {
                if (this.lineHeight == 0 && newArea.height > 0) {
                  this.lineHeight = newArea.height as number
                  this.resetMaxLineHeight()
                  //添加数据测试
                  this.textContent = this.info
                  return
                }
                if (this.totalContentHeight != newArea.height) {
                  console.info(`newArea.height:${newArea.height}`)
                  this.totalContentHeight = newArea.height as number
                  this.resetMaxLineHeight()
                }
              })
          }
          .padding({ left: 25, right: 25 })
        }.scrollBar(BarState.Off)
        .constraintSize({ maxHeight: this.pageHeight == 0 ? 1000 : this.pageHeight })
      }
      .width('100%')
      .layoutWeight(1)

      .onAreaChange((oldArea: Area, newArea: Area) => {
        if (this.pageHeight == 0 && newArea.height > 0) {
          this.pageHeight = newArea.height as number
          this.resetMaxLineHeight()
        }
      })

      Row() {
        Row() {
          Text(this.timeStr)
            .fontColor("#7a7a7a")
            .fontSize(10)
          Text(this.batterySOC)
            .fontColor("#7a7a7a")
            .fontSize(10)
            .margin({ left: 5 })
        }

        Text(`${this.index + 1}/${this.totalPages}`)
          .fontColor("#7a7a7a")
          .fontSize(10)
      }.width('100%').padding({ left: 30, right: 30, top: 30 }).justifyContent(FlexAlign.SpaceBetween)

      Text().width('100%').height(`${AppStorage.get('navigationIndicatorHeight')}px`) //底部导航栏高度

    }
    .width('100%')
    .height('100%')
    .backgroundColor("#CFE6D6")
  }
}

原理参考:https://developer.huawei.com/consumer/cn/forum/topic/0209157903740760273?fid=0109140870620153026