Preserving Scroll Position When Toggling Direction (Updated: Flex+Wrap Version with Scroller.currentOffset())
Requirement Description
When switching a Scroll container’s direction at runtime (Vertical ↔ Horizontal), the inner content resets to the start. Using your updated Flex+Wrap layout, keep the current scroll position and restore it immediately after the direction change.
Background Knowledge
-
Scroll & direction toggling: Changing direction re-layouts children and typically resets position.
Docs: Scroll • ScrollDirection -
Scroller API:
-
currentOffset()→ read absolutexOffset/yOffset. -
scrollTo()→ restore to a specific offset (optionally with a short animation). - Hook via
onDidScroll(oronWillScroll) to keep the latest position.
-
Implementation Steps
Capture the absolute offset just before changing the direction:
const cur = scroller.currentOffset(); lastX = cur.xOffset; lastY = cur.yOffset;Flip the direction state (
Vertical↔Horizontal).-
Restore the offset after layout commits using a microtask:
setTimeout(() => scroller.scrollTo(...), 0).- If switching to Vertical, restore
{ xOffset: 0, yOffset: lastY }. - If switching to Horizontal, restore
{ xOffset: lastX, yOffset: 0 }.
- If switching to Vertical, restore
Keep the
Scrollerinstance stable across toggles (don’t recreate) so offsets persist.With Flex +
wrap, your items fill rows/columns naturally in both directions; restoration still works because we use absolute offsets, not manual deltas.
Code Snippet / Configuration (Your Updated Version)
@Entry
@Component
struct TabsExample {
@State fontColor: string = '#182431'
@State selectedFontColor: string = '#007DFF'
@State currentIndex: number = 0
private controller: TabsController = new TabsController()
private scroller: Scroller = new Scroller()
private scroller1: Scroller = new Scroller()
@State numbers1: String[] = ['0', '1', '2', '3', '4']
@State numbers2: String[] = ['0', '1', '2', '3', '4', '5']
curScrollOffset1: number = 0
@State clickedContent: string = ''
layoutOptions: GridLayoutOptions = {
regularSize: [1, 1],
onGetRectByIndex: (index: number) => {
if (index === 0) {
return [0, 0, 1, 1]
} else if (index === 1) {
return [0, 1, 2, 2]
} else if (index === 2) {
return [0, 3, 3, 3]
} else if (index === 3) {
return [3, 0, 3, 3]
} else if (index === 4) {
return [4, 3, 2, 2]
} else {
return [5, 5, 1, 1]
}
}
}
@Builder
tabBuilder(index: number, name: string) {
Column() {
Text(name)
.fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor)
.fontSize(16)
.fontWeight(this.currentIndex === index ? 500 : 400)
.lineHeight(22)
.margin({ top: 1, bottom: 1 })
Divider()
.strokeWidth(2)
.color('#36D')
.opacity(this.currentIndex === index ? 1 : 0)
}
.width('100%')
.height('90%')
}
build() {
Column() {
Blank().height("25vp")
Button('Stop tabs scrolling and save position')
.width(190).height(30)
.onClick(() => {
AppStorage.setOrCreate('this.currentIndex',
this.currentIndex); // Record the index of the switched tab. Here index===1 is used as an example to record scroll offset, you can set this.currentIndex to 1.
AppStorage.setOrCreate('this.curScrollOffset1', this.curScrollOffset1); // Record scroll offset using index===1 as an example
})
.margin({top:"7vp"})
Button('Click to scroll to previous position')
.width(190).height(30)
.onClick(() => {
this.currentIndex = Number(AppStorage.get('this.currentIndex')) // Get the recorded index of the switched tab
this.curScrollOffset1 = Number(AppStorage.get('this.curScrollOffset1')) // Get the recorded scroll offset using index===1 as an example
this.scroller.scrollTo({
xOffset: 0,
yOffset: this.curScrollOffset1,
animation: { duration: 100, curve: Curve.Ease }
}) // Since we can only move up and down, just change yOffset.
})
.margin({top:"7vp"})
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.controller }) {
TabContent() {
Scroll(this.scroller) {
Grid() {
ForEach(this.numbers1, (day: string) => {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#ffeca636')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
}, (day: string) => day)
}, (day: string) => day)
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('100%')
.height('150%')
}.backgroundColor('#ffffed00')
}
.tabBar(this.tabBuilder(0, 'green'))
TabContent() {
Scroll(this.scroller1) {
Grid() {
ForEach(this.numbers1, (day: string) => {
ForEach(this.numbers2, (day: string) => {
GridItem() {
Text(day)
.fontSize(16)
.backgroundColor('#ffa4b4d7')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
}, (day: string) => day)
}, (day: string) => day)
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('100%')
.height('150%')
}
.backgroundColor('#00ef2323')
.onWillScroll((xOffset: number, yOffset: number, scrollState: ScrollState) => {
this.curScrollOffset1 += yOffset // Scrolling up and down, only need to record yOffset.
console.info('this.curScrollOffset1' + ' ' + this.curScrollOffset1)
})
}
.tabBar(this.tabBuilder(1, 'blue'))
}
.vertical(false)
.barMode(BarMode.Fixed)
.barWidth(360)
.barHeight(30)
.animationDuration(400)
.onChange((index: number) => {
if (index === 0) {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: { duration: 100, curve: Curve.Ease }
}) // When index switches, scroll data to the top.
} else {
this.scroller1.scrollTo({ xOffset: 0, yOffset: 0, animation: { duration: 100, curve: Curve.Ease } })
}
this.currentIndex = index
console.info('this.currentIndex' + this.currentIndex)
})
.width(360)
.height('100%')
.margin({ top: 5 })
.backgroundColor('#F1F3F5')
}
.width('100%').height('100%')
}
}
ArkTSCheck tips (if needed):
• If generics on
Array.fromcause lint errors, use:
private items: string[] = Array.from({ length: 36 }, (_: unknown, i: number) => \#${i + 1});• If your SDK expects.scrollDirection(...)instead of.scrollable(...), switch to:.scrollDirection(this.dir).
Test Results
- Works on API Version 19 Release with HarmonyOS 5.1.1 Release SDK in DevEco Studio 5.1.1 Release.
- After toggling direction, the scroll position is restored on the correct axis.
Limitations or Considerations
- When item sizes/layout differ greatly between directions, clamp restored offsets to content bounds to avoid jumping to empty space.
- Keep
Scrollerstable; recreating it drops state. - On wearables, consider a very short animation (
≤120ms) or none for comfort.

Top comments (0)