【HarmonyOS】高仿华为阅读app翻页demo
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