Context
When developing applications with sidebar navigation in HarmonyOS, the standard SideBarContainer component has limitations. Developers often need more advanced behaviors such as having the bottom content translate smoothly when the sidebar appears, implementing gesture-based sliding where the sidebar follows finger movement, and automatically collapsing the sidebar when the user releases with a leftward swipe tendency. The built-in SideBarContainer only supports three display modes (Embed, Overlay, and AUTO) which cannot achieve these custom interaction patterns.
Description
The standard SideBarContainer component provides three SideBarContainerType styles:
-
Embed: The sidebar is embedded within the component, displayed alongside the content area. When component size is less than
minContentWidth + minSideBarWidthandshowSideBaris not set, the sidebar automatically hides. After auto-hiding, clicking the control button makes the sidebar float over the content area. - Overlay: The sidebar floats over the content area.
-
AUTO: Uses Embed mode when component size is greater than or equal to
minSideBarWidth + minContentWidth, otherwise uses Overlay mode. If the calculated value is less than 600vp, 600vp is used as the mode switching breakpoint.
These built-in modes cannot satisfy requirements for:
- Bottom content translating with sidebar appearance
- Sidebar following finger gestures during drag
- Automatic collapse based on swipe velocity and direction
To achieve these advanced interactions, developers need to leverage:
- Stack: A stacking container where child components are stacked in order, with later children covering earlier ones
- transition: Component transition animations configured through the transition attribute for smooth insertion and deletion effects
- PanGesture: Pan gesture events triggered when the minimum sliding distance reaches the set threshold
Solution
Since SideBarContainer cannot meet these requirements, the solution uses a Stack layout to layer three elements: the main interface (bottom), a semi-transparent gray mask (middle), and the sidebar (top). The implementation uses TransitionEffect.move for bottom content translation and PanGesture for gesture-based sliding effects.
Complete Implementation:
@Component
export struct SideBar {
build() {
Column() {
Text('Side Bar')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
})
}
.justifyContent(FlexAlign.Center)
.backgroundColor('#F1F3F5')
.height('100%')
.width('100%')
}
}
@Entry
@Component
struct Index {
@State showSideBar: boolean = false;
@State isLeft: boolean = false;
sideBarWidth = 290;
duration = 250;
@State xValue: number = 0;
maxX: number = this.sideBarWidth;
minX: number = 0;
build() {
Stack({ alignContent: this.isLeft ? Alignment.TopStart : Alignment.TopEnd }) {
Column() {
Text('Open Side Open Side Open Side Open Side Open Side')
.fontSize(30)
.onClick(() => {
this.getUIContext().animateTo({
duration: this.duration,
curve: Curve.EaseOut,
}, () => {
this.showSideBar = true
this.xValue = this.sideBarWidth
})
})
.backgroundColor('#FBC803')
}
.justifyContent(FlexAlign.Center)
.margin({
right: !this.isLeft ? this.xValue : 0,
left: this.isLeft ? this.xValue : 0,
})
.backgroundColor('#0A59F7')
.height('100%')
.width('100%')
if (this.showSideBar) {
Column()
.backgroundColor('#000')
.opacity(0.4)
.height('100%')
.width('100%')
.onClick(() => {
this.getUIContext().animateTo({
duration: this.duration,
curve: Curve.EaseOut,
}, () => {
this.showSideBar = false
this.xValue = 0
})
})
}
if (this.showSideBar) {
Column() {
SideBar()
}
.margin({
left: this.isLeft ? this.xValue - this.sideBarWidth : 0,
right: !this.isLeft ? this.xValue - this.sideBarWidth : 0,
})
.width(this.sideBarWidth)
.visibility(this.showSideBar ? Visibility.Visible : Visibility.Hidden)
.transition(TransitionEffect.OPACITY.animation({ duration: this.duration, curve: Curve.EaseInOut })
.combine(TransitionEffect.move(this.isLeft ? TransitionEdge.START : TransitionEdge.END)))
.gesture(PanGesture(new PanGestureOptions({ direction: PanDirection.Left | PanDirection.Right }))
.onActionUpdate((event: GestureEvent) => {
if (event) {
let a = this.isLeft ? this.maxX + event.offsetX : this.maxX - event.offsetX
if (this.xValue >= this.maxX) {
this.xValue = a >= this.maxX ? this.maxX : a;
} else if (this.xValue <= this.minX) {
this.xValue = a <= this.minX ? this.minX : a;
} else {
this.xValue = a
}
}
})
.onActionEnd((event: GestureEvent) => {
if (event.velocityX > 0) {
this.getUIContext().animateTo({
duration: 250,
curve: Curve.EaseOut,
}, () => {
this.xValue = this.isLeft ? this.maxX : this.minX;
this.showSideBar = this.isLeft ? true : false;
})
} else {
this.getUIContext().animateTo({
duration: 250,
curve: Curve.EaseOut,
}, () => {
this.xValue = this.isLeft ? this.minX : this.maxX;
this.showSideBar = this.isLeft ? false : true;
})
}
})
)
}
}
.height('100%')
.width('100%')
}
}
Implementation Details:
- Stack Layout Structure: Three layers are stacked - main content (bottom), semi-transparent mask (middle), and sidebar (top)
-
Content Translation: The main content uses margin with
xValueto translate left or right based on sidebar position - Gesture Handling:
-
onActionUpdate: UpdatesxValuebased on finger drag position, clamped betweenminXandmaxX -
onActionEnd: ChecksvelocityXto determine swipe direction and animates sidebar to fully open or closed state
-
-
Direction Control: Set
isLefttotruefor left-side sidebar orfalsefor right-side sidebar - Mask Interaction: Clicking the semi-transparent mask closes the sidebar with animation
Key Takeaways
- Use
Stacklayout to layer multiple components whenSideBarContainercannot meet custom interaction requirements - Combine
TransitionEffect.movewith margin adjustments to create smooth content translation effects when sidebar appears - Implement
PanGesturewithonActionUpdateto make UI follow finger movement in real-time during drag gestures - Use
event.velocityXinonActionEndto determine swipe intention and automatically complete the open/close action - Clamp gesture values between min and max bounds to prevent content from moving beyond desired limits
- Add a semi-transparent mask layer to improve visual hierarchy and provide an intuitive close interaction
- The
isLeftboolean flag makes the component flexible for both left and right sidebar implementations - Use
animateTowith appropriate duration and curve for smooth, professional-looking transitions
Top comments (0)