侧滑菜单是一种比常用的设置界面或配置界面的效果,在主界面左滑或者右滑出现设置界面效果,能方便的进行各种操作。很多优秀的应用都采用了这种界面方案,像文心一言、豆瓣、plex、Google+、网易新闻、知乎日报、有道云笔记等等。 以下是我用鸿蒙 arkTS 实现的侧滑菜单。
侧滑菜单实现原理: 以左侧菜单为例,左侧菜单内容偏移宽度为菜单宽度,以实现菜单初始状态隐藏,内容区域监听滑动手势,根据滑动的速度以及滑动的距离判断展开或者折叠菜单.
实现代码如下 `
@Component
export struct DrawerContainer {
showStyle: number = 1
controller?: DrawerContainerController
@BuilderParam ContentBuilder: () => void
@BuilderParam MenuBuilder: () => void
// onChange:(event: (index: number) => void)
onChange: (open?: boolean) => void
@State menuWidth: number = 300 // 菜单宽度
@State menuOffsetLeft: number = 0 // 菜单左偏移量
@State menuBgOpacity: number = 0 // 菜单背景透明度
@State offsetLeft: number = 0 // 手指滑动距左偏移量
@State pressX: number = 0 // 按下时X坐标
@State lastX: number = 0 // 上次X坐标
@State pressTime: number = 0 // 按下时的时间戳
// 动画
private options: AnimatorOptions = {
duration: 300,
easing: "friction",
delay: 0,
direction: "normal",
iterations: 1,
fill: 'forwards', // 启停模式:保留在动画结束状态
begin: 0, // 起始值
end: 0 // 结束值
};
animator = animator.create(this.options)
aboutToAppear() {
// 初始化控制器
if (this.controller !== null) {
this.controller.listener = {
openMenu: () => {
this.openMenu()
},
closeMenu: () => {
this.closeMenu()
}
} as Listener
}
// showStyle 显示样式:0菜单在下面,1菜单在上面
if (this.showStyle == 0) {
this.offsetLeft = 0
// 设置菜单偏移量为负的菜单宽度,为了解决z-index设置后,菜单界面到内容下面,
// 事件还停留到内容上面,导致点击菜单区域,响应的还是菜单点击事件
this.menuOffsetLeft = -this.menuWidth
} else {
this.offsetLeft = -this.menuWidth
this.menuOffsetLeft = this.offsetLeft
}
}
build() {
Stack() {
// 内容
Stack() {
this.ContentBuilder()
}
.width('100%')
.height('100%')
.offset({ x: this.menuWidth + this.menuOffsetLeft, y: 0 })
// 背景
if (this.menuBgOpacity != 0) {
Stack() {
Text(this.menuBgOpacity.toFixed(2)).fontSize(44)
}
.onClick(() => {
this.closeMenu()
})
.alignContent(Alignment.End)
.opacity(this.menuBgOpacity)
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
}
// 菜单
Stack() {
this.MenuBuilder()
}
.width(this.menuWidth)
.height('100%')
.offset({ x: this.menuOffsetLeft, y: 0 })
// 测试内容
Column() {
Text('menuBgOpacity' + this.menuBgOpacity.toFixed(2)).fontSize(22).onClick(() => {
promptAction.showToast({ message: 'adsf' })
})
Text('offsetLeft' + this.offsetLeft.toFixed(2)).fontSize(22).onClick(() => {
promptAction.showToast({ message: 'adsf' })
})
Text('menuOffsetLeft' + this.menuOffsetLeft.toFixed(2)).fontSize(22).onClick(() => {
promptAction.showToast({ message: 'adsf' })
})
}.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
.alignContent(Alignment.Start)
.gesture(
// 以下组合手势为顺序识别,当长按手势事件未正常触发时则不会触发拖动手势事件
GestureGroup(GestureMode.Sequence,
PanGesture()
.onActionStart((event) => {
// 记录首次按下的x坐标
this.pressX = event.offsetX
this.lastX = this.pressX
// 记录首次按下的时间戳
this.pressTime = event.timestamp
// console.error('pan start tiltX' + event.tiltX)
console.error('onActionStart offsetX' + event.offsetX)
// console.error('pan start pinchCenterX' + event.pinchCenterX)
})
.onActionUpdate((event: GestureEvent) => {
console.error('onActionUpdate offsetX' + event.offsetX)
// 当前x坐标
let localX = event.offsetX
// 计算与上次的x坐标的偏移量
let offsetX = this.lastX - localX;
// 记录上次的x坐标
this.lastX = localX
// 累计偏移量
this.offsetLeft -= offsetX
// showStyle 显示样式:0菜单在下面,1菜单在上面
if (this.showStyle == 0) {
// 设置偏移量的范围
if (this.offsetLeft < 0) {
this.offsetLeft = 0
} else if (this.offsetLeft > this.menuWidth) {
this.offsetLeft = this.menuWidth
}
} else {
// 设置偏移量的范围
if (this.offsetLeft > 0) {
this.offsetLeft = 0
} else if (this.offsetLeft < -this.menuWidth) {
this.offsetLeft = -this.menuWidth
}
}
this.changeMenuOffsetLeft()
})
.onActionEnd((event) => {
// let offsetX = this.pressX - this.lastX;
let offsetX = this.pressX - event.offsetX
if (offsetX == 0) {
return
}
// 滑向的x坐标
var toX = 0
// 快速滑动
if (event.timestamp - this.pressTime < 300) {
if (Math.abs(offsetX) > 10) {
// showStyle 显示样式:0菜单在下面,1菜单在上面
if (this.showStyle == 0) {
if (offsetX > 0) {
toX = 0
} else {
toX = this.menuWidth
}
} else {
if (offsetX > 0) {
toX = -this.menuWidth
} else {
toX = 0
}
}
}
} else {
// showStyle 显示样式:0菜单在下面,1菜单在上面
if (this.showStyle == 0) {
// 当移动偏移量大于菜单一半宽度,完全打开菜单,否则反之
if (this.offsetLeft > this.menuWidth / 2) {
toX = this.menuWidth
} else {
toX = 0
}
} else {
if (this.offsetLeft > -this.menuWidth / 2) {
toX = 0
} else {
toX = -this.menuWidth
}
}
}
// 开启动画
this.startAnimator(toX)
})
)
.onCancel(() => {
console.error('sequence gesture canceled')
})
)
}
/**
* 改变菜单偏移量
*/
changeMenuOffsetLeft() {
// showStyle 显示样式:0菜单在下面,1菜单在上面
if (this.showStyle == 0) {
console.log('1111')
// 设置菜单偏移量
if (this.offsetLeft == 0) {
console.log('0000')
this.menuOffsetLeft = -this.menuWidth
} else {
console.log('3333')
this.menuOffsetLeft = -this.menuWidth + this.offsetLeft
// 改变菜单背景的透明度
this.menuBgOpacity = (1 - Math.abs(this.offsetLeft / this.menuWidth)) / 3
}
} else {
console.log('22222')
// 设置菜单偏移量
this.menuOffsetLeft = this.offsetLeft
// 改变菜单背景的透明度
this.menuBgOpacity = (1 - Math.abs(this.offsetLeft / this.menuWidth)) / 3
}
console.log('设置菜单偏移量', this.menuOffsetLeft)
}
/**
* 开启动画
*/
startAnimator(toX) {
let options: AnimatorOptions = {
duration: 300,
easing: "friction",
delay: 0,
direction: "normal",
iterations: 1,
fill: 'forwards', // 启停模式:保留在动画结束状态
begin: this.offsetLeft, // 起始值
end: toX // 结束值
};
// animator.create(options);
// 更新动画参数
this.animator.reset(options)
// 监听动画值变化事件
this.animator.onframe = (value) => {
console.log('动画', value /**/
)
this.offsetLeft = value
this.changeMenuOffsetLeft()
}
// 开启动画
this.animator.play()
}
/**
* 打开菜单
*/
openMenu() {
// let offsetX = this.pressX - this.lastX;
// if (offsetX == 0) {
// 如果菜单关闭,则开启
if (this.showStyle == 0) {
if (this.offsetLeft == 0) {
this.startAnimator(this.menuWidth);
}
} else {
if (this.offsetLeft == -this.menuWidth) {
this.startAnimator(0);
}
}
// }
}
/**
* 关闭菜单
*/
closeMenu() {
// let offsetX = this.pressX - this.lastX;
// if (offsetX == 0) {
// 如果菜单开启,则关闭
if (this.showStyle == 0) {
if (this.offsetLeft == this.menuWidth) {
this.startAnimator(0);
}
} else {
if (this.offsetLeft == 0) {
this.startAnimator(-this.menuWidth);
}
}
// }
}
}
`