You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Discussions] How to get/calculate alignment inside onObserve? #154
Your formula is actually correct — it is the standard inverse of Flutter's ensureVisible alignment. Two separate things are tripping you up:
You apply it to model.firstChild, which is almost always clipped by the leading edge of the viewport, so leadingMarginToViewport <= 0 and the result can only ever be <= 0.
Even with a correct value, that is not the quantity this package's alignment parameter takes. Here alignment is relative to the item's own main-axis size, not to the viewport.
Vertical ListView, viewport 300 tall, every item 100 tall — item i occupies scroll range [100i, 100i + 100). One text row below = 50px, so one item = 2 rows.
jumpTo(index: 3, alignment: 0.0) → pixels = 300 — item 3's leading edge on the viewport's leading edge:
Scrolling through the same range, watching item 3 (firstChild is the first displaying child):
offset.pixels
firstChild.index
leadingMarginToViewport
package alignment -leading / mainAxisSize
viewport-relative leading / (vpExtent - size)
300
3
0
0.00
0.000
325
3
-25
0.25
-0.125
350
3
-50
0.50
-0.250
375
3
-75
0.75
-0.375
400
4
0
0.00
0.000
The right-hand column is your formula. It is not wrong — it is just measuring something that is always <= 0 for firstChild. And feeding it into jumpTo would trip assert(alignment.clamp(0, 1) == alignment) at observer_controller.dart#L405.
Your formula, applied to a specific item
If viewport-relative alignment is what you want, use displayingChildModelMap to pick the item you care about instead of firstChild. Dropped into the CustomScrollView demo, watching item 12:
// Item 12's viewport-relative alignment, i.e. the semantics of// Flutter's `ensureVisible`. It is null when item 12 is not displaying.final item12 = model.displayingChildModelMap[12];
if (item12 ==null) {
debugPrint('item12 alignment(viewport) -- not displaying');
} else {
final maxDistance = item12.viewportMainAxisExtent - item12.mainAxisSize;
final alignment12 = maxDistance <=0?0.0: item12.leadingMarginToViewport / maxDistance;
debugPrint('item12 alignment(viewport) -- $alignment12');
}
Scroll and you'll see it go ~1.0 (item 12 entering from the bottom) → 0.0 (its top edge on the viewport's top edge) → negative (scrolling further, item 12 clipped by the top edge). That last stretch is exactly the range firstChild lives in, which is why your original version only ever printed <= 0.
Note mainAxisSize instead of size.height — it already picks the right axis, so the same code works for horizontal lists.
The package's own alignment, inverted
If the goal is saving and restoring the scroll position via (index, alignment), this is the one you want:
onObserve: (model) {
final first = model.firstChild;
if (first ==null|| first.mainAxisSize ==0) return;
final index = first.index;
final alignment =
(-first.leadingMarginToViewport / first.mainAxisSize).clamp(0.0, 1.0);
// restore later:// observerController.jumpTo(index: index, alignment: alignment);
}
(leadingMarginToViewport is layoutOffset + precedingScrollExtent - viewportPixels, see ObserveDisplayingChildModelMixin.)
For the child sitting at the leading edge — the only one whose package alignment falls in [0, 1] — the two are related by
so alignmentItem ∈ [0, 1] maps to alignmentViewport <= 0; they only agree at 0. For any other child (like item 12 above, sitting fully inside the viewport) there is no package alignment to convert from — compute the viewport-relative value directly from leadingMarginToViewport.
Two last notes:
Near the end of the list jumpTo is clamped to maxScrollExtent, so an (index, alignment) round trip is not exact there.
As the sketches show, (index: 3, alignment: 1.0) and (index: 4, alignment: 0.0) are the same position — expected, not a bug.
Activity
LinXunFeng commentedon Sep 7, 2026
Your formula is actually correct — it is the standard inverse of Flutter's
ensureVisiblealignment. Two separate things are tripping you up:model.firstChild, which is almost always clipped by the leading edge of the viewport, soleadingMarginToViewport <= 0and the result can only ever be<= 0.alignmentparameter takes. Herealignmentis relative to the item's own main-axis size, not to the viewport.What
alignmentmeans in this packageSee
observer_controller.dart#L986:Vertical
ListView, viewport 300 tall, every item 100 tall — itemioccupies scroll range[100i, 100i + 100). One text row below = 50px, so one item = 2 rows.jumpTo(index: 3, alignment: 0.0)→pixels = 300— item 3's leading edge on the viewport's leading edge:alignment: 0.5→pixels = 350— half of item 3 scrolled past the top edge:alignment: 1.0→pixels = 400— item 3's trailing edge on the viewport's leading edge, i.e. fully scrolled out:For contrast,
alignment: 0.5in Flutter'sensureVisible(viewport-relative) centres item 3,pixels = 200. Same number, completely different result:The two values side by side
Scrolling through the same range, watching item 3 (
firstChildis the first displaying child):offset.pixelsfirstChild.indexleadingMarginToViewport-leading / mainAxisSizeleading / (vpExtent - size)The right-hand column is your formula. It is not wrong — it is just measuring something that is always
<= 0forfirstChild. And feeding it intojumpTowould tripassert(alignment.clamp(0, 1) == alignment)atobserver_controller.dart#L405.Your formula, applied to a specific item
If viewport-relative alignment is what you want, use
displayingChildModelMapto pick the item you care about instead offirstChild. Dropped into theCustomScrollViewdemo, watching item 12:Scroll and you'll see it go
~1.0(item 12 entering from the bottom) →0.0(its top edge on the viewport's top edge) → negative (scrolling further, item 12 clipped by the top edge). That last stretch is exactly the rangefirstChildlives in, which is why your original version only ever printed<= 0.Note
mainAxisSizeinstead ofsize.height— it already picks the right axis, so the same code works for horizontal lists.The package's own alignment, inverted
If the goal is saving and restoring the scroll position via
(index, alignment), this is the one you want:(
leadingMarginToViewportislayoutOffset + precedingScrollExtent - viewportPixels, seeObserveDisplayingChildModelMixin.)For the child sitting at the leading edge — the only one whose package
alignmentfalls in[0, 1]— the two are related byso
alignmentItem ∈ [0, 1]maps toalignmentViewport <= 0; they only agree at0. For any other child (like item 12 above, sitting fully inside the viewport) there is no packagealignmentto convert from — compute the viewport-relative value directly fromleadingMarginToViewport.Two last notes:
jumpTois clamped tomaxScrollExtent, so an(index, alignment)round trip is not exact there.(index: 3, alignment: 1.0)and(index: 4, alignment: 0.0)are the same position — expected, not a bug.yerzhant commentedon Sep 8, 2026
I appreciate you for the comprehensive answer.