| 2446 | forceLayout, |
| 2447 | /* isFromLazyLayout */ true); |
| 2448 | |
| 2449 | _lazyLayoutData->availableWidth = _calculatedFrame.width; |
| 2450 | _lazyLayoutData->availableHeight = _calculatedFrame.height; |
| 2451 | return updated; |
| 2452 | } |
| 2453 | |
| 2454 | void ViewNode::updateStickyHeaders(bool refreshCache) { |
| 2455 | if (_scrollState == nullptr || !_scrollState->getNativeStickyEnabled()) { |
| 2456 | return; |
| 2457 | } |
| 2458 | if (_viewNodeTree == nullptr) { |
| 2459 | return; |
| 2460 | } |
| 2461 | |
| 2462 | float scrollY = _scrollState->getDirectionAgnosticContentOffset().y; |
| 2463 | // Pixels of visual overhang above sticky headers. Extends the effective header |
| 2464 | // height for the clamp so a header rendering a bar above its yoga bounds |
| 2465 | // (SectionList.stickyCover) stops sliding before the next section arrives. |
| 2466 | // Mirrors SectionList.tsx's `headerHeight = getHeaderHeight() + coverHeight`. |
| 2467 | float stickyCover = _scrollState->getNativeStickyCover(); |
| 2468 | // Pixels below scroll viewport top where headers pin. Matches CSS `top: N`. |
| 2469 | // Effectively shifts the pin position down so headers aren't clipped behind |
| 2470 | // a floating page header with visual footprint past its Yoga bounds. |
| 2471 | float stickyOffset = _scrollState->getNativeStickyOffset(); |
| 2472 | |
| 2473 | // We push translationY updates through the attribute-set path so both the C++ state |
| 2474 | // and the platform-side transform binder fire in the same frame. withLock acquires |
| 2475 | // the tree mutex (recursive -- safe if already held on the Android scroll path) and |
| 2476 | // opens a ViewTransactionScope. Nested calls piggyback on the outer transaction; a |
| 2477 | // top-level call submits at end-of-scope, pushing transforms in the same VSYNC as |
| 2478 | // the scroll gesture. |
| 2479 | _viewNodeTree->withLock([&]() { |
| 2480 | auto& scope = _viewNodeTree->getCurrentViewTransactionScope(); |
| 2481 | |
| 2482 | // Bounded-depth subtree walk. Header nesting in SubscreenSections is deep: |
| 2483 | // scroll -> PullToRefresh -> SubscreenContent -> paddingRight layout -> |
| 2484 | // SectionList root -> SectionListItem root -> column-reverse layout -> header |
| 2485 | // (~8 levels). 16 covers that plus any consumer wrapping without unbounded cost. |
| 2486 | // Skip into nested scrolls: each scroll owns its own sticky pass. |
| 2487 | static constexpr int kStickyMaxDepth = 16; |
| 2488 | auto* owner = AttributeOwner::getNativeOverridenAttributeOwner(); |
| 2489 | std::function<void(ViewNode*, int)> walk = [&](ViewNode* node, int depth) { |
| 2490 | if (depth <= 0) { |
| 2491 | return; |
| 2492 | } |
| 2493 | for (auto* child : *node) { |
| 2494 | if (child->getStickyPosition() == StickyPositionTop) { |
| 2495 | if (refreshCache) { |
| 2496 | auto* parent = child->getParent().get(); |
| 2497 | // parent Y relative to this scroll node (sum yoga Top edges up the chain). |
| 2498 | float parentY = 0.0f; |
| 2499 | float parentH = 0.0f; |
| 2500 | if (parent == this) { |
| 2501 | // Direct child of the scroll: no parent section to clamp against. |
| 2502 | // Treat the sticky track as unbounded so the header stays pinned once |
| 2503 | // scrolled past its natural Y, matching CSS `position: sticky` |
| 2504 | // semantics for elements whose containing block is the scroll itself. |
| 2505 | parentH = std::numeric_limits<float>::max(); |
nothing calls this directly
no test coverage detected