Skip to content

Scroll only if item is not fully visible #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rjahn opened this issue Mar 5, 2025 · 11 comments
Closed

Scroll only if item is not fully visible #118

rjahn opened this issue Mar 5, 2025 · 11 comments
Assignees

Comments

@rjahn
Copy link

rjahn commented Mar 5, 2025

Platforms

iOS

Description

I have a CustomScrollView with SliverList.

observeController.animateTo(index: x, ...) works without problems.

Is it possible to find out if index: x is fully shown and don't scroll in this case?
It could be possible that index: x is not fully shown, but a small part. In this case, scroll to the index?

I tried to use onObserve but it won't be called initialy and observerController.dispatchOnceObserve(...) didn't help.

My code

No response

Try do it

No response

@LinXunFeng
Copy link
Member

Try the code below.

final result = await observerController.dispatchOnceObserve(
  isDependObserveCallback: false,
  isForce: true,
);
final resultMap = result.observeResult?.displayingChildModelMap ?? {};
final displayPercentage = resultMap[x]?.displayPercentage ?? 0;
if (displayPercentage == 1) return;
observeController.animateTo(index: x, ...)

@rjahn
Copy link
Author

rjahn commented Mar 5, 2025

My observerController is SliverObserverController

displayingChildModelMap => innerDisplayingChildModelMap

displayPercentage is not available

@LinXunFeng
Copy link
Member

final result = await observerController.dispatchOnceObserve(
  // sliverContext: _sliverGridCtx,
  sliverContext: _sliverListCtx,
  isDependObserveCallback: false,
  isForce: true,
);
final observeResult = result.observeAllResult[_sliverListCtx];
// if (observeResult is! GridViewObserveModel) {
if (observeResult is! ListViewObserveModel) {
  return;
}
final resultMap = observeResult.displayingChildModelMap;
final displayPercentage = resultMap[x]?.displayPercentage ?? 0;
if (displayPercentage == 1) return;
observeController.animateTo(index: x, ...)

@rjahn
Copy link
Author

rjahn commented Mar 10, 2025

Tried your code but

final result = await observerController.dispatchOnceObserve(

does not return a result -> endless wait.
The sliverContext is set.

@LinXunFeng
Copy link
Member

It works fine on my side, please give a reproducible sample

@rjahn
Copy link
Author

rjahn commented Mar 10, 2025

This is clear :)

I guess a standard example will work. My app is "special" (as always) and the app itself is open source, but the configuration is not trivial.

Maybe you have an idea with my code:

TableWrapper - StatefulWidget
initState creates the SliverObserverController and ScrollController
build creates a TableWidget - StatefulWidget

TableWidget gets the controller in constructor
build creates the SliverViewObserver with controller of widget
build tries to scroll to with following code

    SchedulerBinding.instance.addPostFrameCallback((_) {
      _scrollToSelected(context);
    });

and the method _scrollToSelected contains your code:

  Future<void> _scrollToSelected(BuildContext context) async {
    final result = await widget.itemScrollController!.dispatchOnceObserve(
      sliverContext: context,
      isDependObserveCallback: false,
      isForce: true,
    );

    print("Done");

    final observeResult = result.observeAllResult[context];
// if (observeResult is! GridViewObserveModel) {
    if (observeResult is! ListViewObserveModel) {
      return;
    }
    final resultMap = observeResult.displayingChildModelMap;
    final displayPercentage = resultMap[widget.selectedRowIndex]?.displayPercentage ?? 0;

    if (displayPercentage == 1) return;

    widget.itemScrollController!.animateTo(
        index: widget.selectedRowIndex,
        duration: kThemeAnimationDuration,
        alignment: 0,
        curve: Curves.easeInOut
    );

A context problem?

@rjahn
Copy link
Author

rjahn commented Mar 10, 2025

Yes, it's the context!

I had to cache the BuildContext like in your example, e.g. https://github.com/fluttercandies/flutter_scrollview_observer/blob/main/example/lib/features/listview/listview_ctx_demo/listview_ctx_demo_page.dart

I will try it again with right context!

@rjahn
Copy link
Author

rjahn commented Mar 10, 2025

Everything is fine with your code - works.

One additional question: If the item is not fully visible, you scroll until it's fully visible and if possible, you scroll it to top of the list. Is it possible to scroll just as much until the item is fully visible?

A short video to demonstrate what's happening:

scroll_to_last.mov

If I select the last record, it's not fully visible, and after selection the list scrolls until the item is the first shown item. It would be nice to scroll only until the item is fully visible but not until it's the first visible in the list.

@rjahn
Copy link
Author

rjahn commented Mar 10, 2025

... or scroll the item to the middle of the list?

@LinXunFeng
Copy link
Member

LinXunFeng commented Mar 10, 2025

Scroll to the edge

final result = await observerController.dispatchOnceObserve(
  isDependObserveCallback: false,
  isForce: true,
);
final observeResult = result.observeResult;
final resultMap = observeResult?.displayingChildModelMap ?? {};
final targetResult = resultMap[index];
final displayPercentage = targetResult?.displayPercentage ?? 0;
if (displayPercentage == 1) return;

bool isAtTopItem = false;
if (targetResult != null) {
  isAtTopItem = targetResult.leadingMarginToViewport <= 0;
} else {
  final displayingChildModelList =
      observeResult?.displayingChildModelList ?? [];
  if (displayingChildModelList.isNotEmpty) {
    final firstItem = displayingChildModelList.first;
    isAtTopItem = firstItem.index > index;
  }
}
observerController.jumpTo(
  index: index,
  alignment: isAtTopItem ? 0 : 1,
  isFixedHeight: true,
  offset: (targetOffset) {
    if (isAtTopItem) return 0;
    var _obj = ObserverUtils.findRenderObject(
      observerController.sliverContexts.first,
    );
    if (_obj == null || _obj is! RenderSliver) return 0;
    return _obj.geometry?.paintExtent ?? 0;
  },
);

Scroll to the middle

final result = await observerController.dispatchOnceObserve(
  sliverContext: _sliverListCtx,
  isDependObserveCallback: false,
  isForce: true,
);
final observeResult = result.observeAllResult[_sliverListCtx];
if (observeResult is! ListViewObserveModel) {
  return;
}
final resultMap = observeResult.displayingChildModelMap;
final displayPercentage = resultMap[x]?.displayPercentage ?? 0;
if (displayPercentage == 1) return;

observerController.jumpTo(
  index: index,
  alignment: 0.5, // <------ 1
  // Optional, If the size of your item is fixed, it is recommended to set it to true.
  isFixedHeight: true,
  offset: (targetOffset) {
    if (isAtTopItem) return 0;
    var _obj = ObserverUtils.findRenderObject(
      observerController.sliverContexts.first,
    );
    if (_obj == null || _obj is! RenderSliver) return 0;
    return (_obj.geometry?.paintExtent ?? 0) * 0.5;  // <------ 2
  },
);

rjahn added a commit to sibvisions/flutter_jvx that referenced this issue Mar 10, 2025
@rjahn
Copy link
Author

rjahn commented Mar 11, 2025

Verified and works as expected.

The "Scroll to the middle" code should be without if (isAtTopItem) return 0;.

@rjahn rjahn closed this as completed Mar 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants