GreenTigerEye
GreenTigerEye

Reputation: 6637

Flutter: Scrolling to a widget in ListView

How can I scroll to a special widget in a ListView? For instance I want to scroll automatically to some Container in the ListView if I press a specific button.

ListView(children: <Widget>[
  Container(...),
  Container(...), // scroll for example to this container 
  Container(...)
]);

Upvotes: 213

Views: 263163

Answers (22)

nathanfranke
nathanfranke

Reputation: 991

This approach:

  • does not require hard coding the height of each element
  • assumes all elements are the same (or similar) height, and
  • assumes we know the itemCount.

Flutter estimates the ScrollPosition.maxScrollExtent based on visible elements, and ListView.builder only renders visible elements (lazy loading).

Adding ScrollPosition.viewportDimension and ScrollPosition.maxScrollExtent yields the full content height, which is used to estimate the position of the element at a given index. If all elements are the same height, the estimation is perfect.

// Get the full content height.
final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
// Index to scroll to.
final index = 100;
// Estimate the target scroll position.
final target = contentSize * index / itemCount;
// Scroll to that position.
controller.position.animateTo(
  target,
  duration: const Duration(seconds: 2),
  curve: Curves.easeInOut,
);

And a full example:

user clicks a button to scroll to the one hundredth element of a long list

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter Test",
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = ScrollController();
    final itemCount = 1000;
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter Test"),
      ),
      body: Column(
        children: [
          ElevatedButton(
            child: Text("Scroll to 100th element"),
            onPressed: () {
              final contentSize = controller.position.viewportDimension + controller.position.maxScrollExtent;
              final index = 100;
              final target = contentSize * index / itemCount;
              controller.position.animateTo(
                target,
                duration: const Duration(seconds: 2),
                curve: Curves.easeInOut,
              );
            },
          ),
          Expanded(
            child: ListView.builder(
              controller: controller,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text("Item at index $index."),
                );
              },
              itemCount: itemCount,
            ),
          )
        ],
      ),
    );
  }
}

Upvotes: 30

Rishita Joshi
Rishita Joshi

Reputation: 425

you can refer following code to the scroll-specific position in the listview builder that is perfectly working for me.

   ListView.builder(
              controller: controller,
              itemCount: 15,
              itemBuilder: (BuildContext context, int index) {
               return children[index);
              },
            ),
             ElevatedButton(
                    onPressed: () {
    oneScrollController .animateTo(10 * 10, 
       duration: const Duration(milliseconds: 10),
        curve: Curves.bounceIn);                                                              
    },
                    child: Text(
                      "Contact me",);

);

Upvotes: -1

St&#233;phane de Luca
St&#233;phane de Luca

Reputation: 13603

You perfectly can use ensureVisibility() but need to convert your ListView into a SingleChildScrollView so that you are not affected by the ListView own widget visibility optimization process (not all the widget are actually rendered at a given time).

Here is an example that I'm sure will benefit many of you: An interactive debugger that let you follow a program being ran in live.

Say you have a list of Widgets that materializes the program instructions to be executed and you want to follow the execution by looking at the running instruction no matter where in the program it is located.

  1. Define a unique GlobalKey that designates you running instruction.

  2. In the build, you set the key to the currently running instruction (aka the widget you always want to be visible) while let null to the others.

  3. Then simply add a WidgetsBinding.instance.addPostFrameCallback within which you call the Scrollable.ensureVisible passing the running instruction widget context.

And voilà.

Here is a working DartPad

class Debugger extends StatelessWidget {

  
  const Debugger(Simulator simulator);

  /// The key that designates the visible widget
  final currentIpKey = GlobalKey();

  List<Widget> get steps {
    final l = <Widgets>[];

    for(int i=0; i<simulator.program.length; i++) {

      // == Assign the key to the widget we wanna see, while
      // leaving null to others 
      final key = i == simulator.ip ? currentIpKey : null;

      l.add(Text(key:key, simulator.program[i]);
    }

    return l;
  }

  @override Widget build(BuildContext context) {

  return Scaffold(
      appBar: AppBar(
          title: Text("Battery BMS simulator"),
          
      body: Card(
        child: AnimatedBuilder(
            animation: widget.simulator,
            builder: (context, child) {

              final allSteps = steps;

              // == Add the post build call to scroll to the visible
              // widget.

              WidgetsBinding.instance.addPostFrameCallback((_) {
                if (currentIpKey.currentContext != null)
                  Scrollable.ensureVisible(
                    currentIpKey.currentContext!,
                    duration: Duration(seconds: 1),
                  );
              });

              // New code:
              return SingleChildScrollView(
                physics: const BouncingScrollPhysics(),
                child: Column(children: allSteps),
              );

              // Former code
              //return ListView(
              //  physics: const BouncingScrollPhysics(),
              //  children: allSteps,
              //);
            }),
      ),
    );
  }

}

Upvotes: 3

George Freire
George Freire

Reputation: 47

My Solution

class SitePage extends StatelessWidget {

const SitePage({super.key});

@override Widget build(BuildContext context) {

GlobalKey<State<StatefulWidget>> dataKey = new GlobalKey();

return Scaffold(
  backgroundColor: SiteColors.backgroundColor,
  body: SingleChildScrollView(
    child: Column(
      children: [
        HeaderNavigation(dataKey: dataKey),
        HeaderModule(),
        CategoriesWidget(dataKey: dataKey),
        58.verticalSpaceFromWidth,            
      ],
    ),
  ),
);

}

}

Upvotes: -1

gio maroon
gio maroon

Reputation: 11

Studying some of the other approaches I came up to a solution with the use of ListView and trying to get the height of each list item through its context.

First, I wrap each list item with this widget:

class _ListItem extends StatelessWidget {
  const _ListItem(
      {Key? key,
      required this.onItemBuilt,
      required this.index,
      required this.child})
      : super(key: key);

  final int index;
  final Widget child;
  final void Function(double) onItemBuilt;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (context.mounted && context.size != null) {
        onItemBuilt(context.size!.height);
      }
    });
    return child;
  }
}

addPostFrameCallback is used to obtain item's height after the item is built. Then the item's height is exposed through the onItemBuilt callback.

Second, I get each item's height when ListView builds the items.

ListView(
    children: items.map((e) {
        final index = widget.children.indexOf(e);
        return _ListItem(
            index: index,
            child: e,
            onItemBuilt: (height) => _setItemHeights(index, height));
      }).toList(),
    )

Then I need to store the heights of the items through the method _setItemHeights:

final _itemHeights = <double>[];

void _setItemHeights(int index, double height) {
    // Assure that item's height is not already in _itemHeights list
    if (index >= _itemHeights.length) {
      _itemHeights.add(height);
    }
  }

Third, to scroll to a specific item we need to calculate the offset. Since we have the heights of the items stored to _itemHeights, we call the method:

double _getOffset(int index) {
    double offset = 0;
    for (int i = 0; i < index; i++) {
      if (i < _itemHeights.length) {
        offset = offset + _itemHeights[i];
      }
    }
    return offset;
  }

and then call:

_scrollController.animateTo(_getOffset(index),
            duration: const Duration(milliseconds: 160),
            curve: Curves.linear)

BUT, ListView does not build all the items from the beggining (ListView builts the items that are visible from Viewport and some of the next items and when user scrolls, then ListView builds the following items that are going to be visible from Viewport).

So, when we want to scroll to an item that has not been built yet, then we should wait for the ListView to build all the preceding items. That is achieved by scrolling to the last item of our _itemHeights list in order to trigger ListView to build more items until the desired item is built. To implement that we create the method:

int _tempItemHeightsLength = 0;

Future<void> _animateToItem(int index) async {
    final itemHeightsLength = _itemHeights.length;
    if (_scrollController.hasClients) {
      if (index <= itemHeightsLength) {
        await _scrollController.animateTo(
            _getOffset(index),
            duration: const Duration(milliseconds: 160),
            curve: Curves.linear);
      } else if (_tempItemHeightsLength < itemHeightsLength) {
        _tempItemHeightsLength = itemHeightsLength;
        await _scrollController.animateTo(
            _getOffset(itemHeightsLength - 1),
            duration: const Duration(milliseconds: 160),
            curve: Curves.linear);
        await _animateToItem(index);
      }
    }
  }

Finally, I created a custom controller (it is shown below) to manipulate scrolling with an extra property extraPixels in order to position the item some pixels lower or higher regarding Viewport's top position.

That's it. For me this solution works very well. I also added a property padding for ListView.

The entire code is below:

import 'package:flutter/material.dart';

/// A wrapper around ListView with a controller (CustomListViewController)
/// which features method animateToItem(int index) that scrolls the ListView
/// to the item through its index
class CustomListView extends StatefulWidget {
  const CustomListView(
      {Key? key,
      required this.controller,
      required this.children,
      this.scrollAnimationDuration, this.padding})
      : super(key: key);

  final CustomListViewController controller;
  final List<Widget> children;
  final Duration? scrollAnimationDuration;
  final EdgeInsets? padding;

  @override
  State<CustomListView> createState() => _CustomListViewState();
}

class _CustomListViewState extends State<CustomListView> {
  late final ScrollController _scrollController;

  final _itemHeights = <double>[];

  int _tempItemHeightsLength = 0;

  Future<void> _initialize(int index, double? extraPixels) async {
    assert(index >= 0 && index < widget.children.length,
        'Index of item to animate to is out of list\'s range');
    if (_itemHeights.isNotEmpty) {
      await _animateToItem(index, extraPixels);
    } else {
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
        if (_itemHeights.isNotEmpty) {
          _animateToItem(index, extraPixels);
        }
      });
    }
  }

  Future<void> _animateToItem(int index, double? extraPixels) async {
    final itemHeightsLength = _itemHeights.length;
    if (_scrollController.hasClients) {
      if (index <= itemHeightsLength) {
            await _scrollController.animateTo(
            _getOffset(index) - (extraPixels ??= 0),
            duration: widget.scrollAnimationDuration ??
                const Duration(milliseconds: 160),
            curve: Curves.linear);
      } else if (_tempItemHeightsLength < itemHeightsLength &&
          itemHeightsLength > 0) {
        _tempItemHeightsLength = itemHeightsLength;
        await _scrollController.animateTo(_getOffset(itemHeightsLength - 1),
            duration: widget.scrollAnimationDuration ??
                const Duration(milliseconds: 160),
            curve: Curves.linear);
        await _animateToItem(index, extraPixels);
      }
    }
  }

  void _setItemHeights(int index, double height) {
    if (index >= _itemHeights.length) {
      _itemHeights.add(height);
    }
  }

  double _getOffset(int index) {
    double offset = 0;
    for (int i = 0; i < index; i++) {
      if (i < _itemHeights.length) {
        offset = offset + _itemHeights[i];
      }
    }
    return offset + (widget.padding?.top?? 0);
  }

  @override
  void initState() {
    _scrollController = widget.controller.scrollController;
    widget.controller._state = this;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      controller: _scrollController,
      padding: widget.padding,
      children: widget.children.map((e) {
        final index = widget.children.indexOf(e);
        return _ListItem(
            index: index,
            child: e,
            onItemBuilt: (height) => _setItemHeights(index, height));
      }).toList(),
    );
  }
}

/// Wrapper around the items of ListView.
/// The CallBack onItemBuilt exposes the height of the item to the CutomListView,
/// so that the offset of the scroll position can be calculated later on.
class _ListItem extends StatelessWidget {
  const _ListItem(
      {Key? key,
      required this.onItemBuilt,
      required this.index,
      required this.child})
      : super(key: key);

  final int index;
  final Widget child;
  final void Function(double) onItemBuilt;

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (context.mounted && context.size != null) {
        onItemBuilt(context.size!.height);
      }
    });
    return child;
  }
}

/// Controller of CustomListView.
/// It includes a ScrollController that is attached to the ListView and
/// can be used at will (e.g. to add a ScrollBar)
/// and the method animateToItem to scroll the ListView to a specific item.
class CustomListViewController {

  _CustomListViewState? _state;

  /// ScrollController that is attached to ListView
  final scrollController = ScrollController();

  /// Method to scroll ListView to specific item given the item's index.
  /// The item appears first in ListView's Viewport.
  /// With extraPixels, pixels can be added/subtracted in order to position the
  /// item lower or higher in Viewport.
  /// If ListView is built before calling this method, the Future of this
  /// method is returned when ListView completes the scrolling to specific item.
  /// Otherwise this method is scheduled for next Frame,
  /// therefore the Future is returned earlier and it is not bound to the
  /// completion of the scrolling
  Future<void> animateToItem(int index, {double? extraPixels}) async {
    if (_state != null) {
      await _state!._initialize(index, extraPixels);
    } else {
      WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
        _state?._initialize(index, extraPixels);
      });
    }
  }
}

I hope this solution is helpful :)

Upvotes: 0

Trịnh Đức Duy
Trịnh Đức Duy

Reputation: 253

Well this same answer https://stackoverflow.com/a/49154882/8422048

But I came up with a solution to fix this problem. this is a trick. If you want to scroll to unbuilt widget. You can use 2 or 3 times Scrollable.ensureVisible

Example:

final _datakey = GlobalKey();
final _datakey2 = GlobalKey();

============================
await Scrollable.ensureVisible(dataKey.currentContext!,
                    curve: Curves.easeIn,
                    duration: const Duration(milliseconds: 250)),
await Future.delayed(const Duration(milliseconds: 200)),
await Scrollable.ensureVisible(dataKey2.currentContext!,
                    curve: Curves.easeOut,
                    duration: const Duration(milliseconds: 250)),

===============

SingleChildScrollView(
  child: Column(
    children: [
      Container(
        key: dataKey,
        width: 500,
        height: 500,
        color: Colors.yellow,
      ),
      Container(
        width: 500,
        height: 500,
        color: Colors.red,
      ),
      Container( // this is a widget, which u want to scroll
        key: dataKey2,
        width: 500,
        height: 500,
        color: Colors.blue,
      ),
    ],
    
  ),
);

Upvotes: 0

Tony Blanco
Tony Blanco

Reputation: 770

The simplest way is to call this method inside your InitState method. (not the build to evict unwanted errors)

WidgetsBinding.instance.addPostFrameCallback((_) => Scrollable.ensureVisible(targetKey.currentContext!))

WidgetsBinding.instance.addPostFrameCallback will guarantee that the list is builded and the this automatic search for your target and move the scroll to it. You can then customize the animation of the scroll effect on the Scrollable.ensureVisible method

Note: Remember to add the targetKey (a GlobalKey) to the widget you want to scroll to.

Upvotes: 2

BIS Tech
BIS Tech

Reputation: 19494

You can use GlobalKey to access buildercontext.

I use GlobalObjectKey with Scrollable.

Define GlobalObjectKey in item of ListView

ListView.builder(
itemCount: category.length,
itemBuilder: (_, int index) {
return Container(
    key: GlobalObjectKey(category[index].id),

You can navigate to item from anywhere

InkWell(
  onTap: () {
Scrollable.ensureVisible(GlobalObjectKey(category?.id).currentContext);

You add scrollable animation changing property of ensureVisible

Scrollable.ensureVisible(
  GlobalObjectKey(category?.id).currentContext,
  duration: Duration(seconds: 1),// duration for scrolling time
  alignment: .5, // 0 mean, scroll to the top, 0.5 mean, half
  curve: Curves.easeInOutCubic);

Upvotes: 50

F Perroch
F Perroch

Reputation: 2235

You can also simply use the FixedExtentScrollController for same size items with the index of your initialItem :

controller: FixedExtentScrollController(initialItem: itemIndex);

The documentation : Creates a scroll controller for scrollables whose items have the same size.

Upvotes: 0

CopsOnRoad
CopsOnRoad

Reputation: 268114

Screenshot (Fixed height content)

enter image description here


If your items have fixed height, then you can use the following approach.

class HomePage extends StatelessWidget {
  final ScrollController _controller = ScrollController();
  final double _height = 100.0;

  void _animateToIndex(int index) {
    _controller.animateTo(
      index * _height,
      duration: Duration(seconds: 2),
      curve: Curves.fastOutSlowIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.arrow_downward),
        onPressed: () => _animateToIndex(10),
      ),
      body: ListView.builder(
        controller: _controller,
        itemCount: 20,
        itemBuilder: (_, i) {
          return SizedBox(
            height: _height,
            child: Card(
              color: i == 10 ? Colors.blue : null,
              child: Center(child: Text('Item $i')),
            ),
          );
        },
      ),
    );
  }
}

Upvotes: 58

TWL
TWL

Reputation: 6654

Unfortunately, ListView has no built-in approach to a scrollToIndex() function. You’ll have to develop your own way to measure to that element’s offset for animateTo() or jumpTo(), or you can search through these suggested solutions/plugins or from other posts like flutter ListView scroll to index not available

(the general scrollToIndex issue is discussed at flutter/issues/12319 since 2017, but still with no current plans)


But there is a different kind of ListView that does support scrollToIndex:

You set it up exactly like ListView and works the same, except you now have access to a ItemScrollController that does:

  • jumpTo({index, alignment})
  • scrollTo({index, alignment, duration, curve})

Simplified example:

ItemScrollController _scrollController = ItemScrollController();

ScrollablePositionedList.builder(
  itemScrollController: _scrollController,
  itemCount: _myList.length,
  itemBuilder: (context, index) {
    return _myList[index];
  },
)

_scrollController.scrollTo(index: 150, duration: Duration(seconds: 1));

Please not that although the scrollable_positioned_list package is published by google.dev, they explicitly state that their packages are not officially supported Google products. - Source

Upvotes: 97

anakin.jin
anakin.jin

Reputation: 61

I found a perfect solution to it using ListView.
I forgot where the solution comes from, so I posted my code. This credit belongs to other one.

21/09/22:edit. I posted a complete example here, hope it is clearer.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class CScrollToPositionPage extends StatefulWidget {

CScrollToPositionPage();

@override
State<StatefulWidget> createState() => CScrollToPositionPageState();
}

class CScrollToPositionPageState extends State<CScrollToPositionPage> {
static double TEXT_ITEM_HEIGHT = 80;
final _formKey = GlobalKey<FormState>();
late List _controls;
List<FocusNode> _lstFocusNodes = [];

final __item_count = 30;

@override
void initState() {
    super.initState();

    _controls = [];
    for (int i = 0; i < __item_count; ++i) {
        _controls.add(TextEditingController(text: 'hello $i'));

        FocusNode fn = FocusNode();
        _lstFocusNodes.add(fn);
        fn.addListener(() {
            if (fn.hasFocus) {
                _ensureVisible(i, fn);
            }
        });
    }
}

@override
void dispose() {
    super.dispose();

    for (int i = 0; i < __item_count; ++i) {
        (_controls[i] as TextEditingController).dispose();
    }
}

@override
Widget build(BuildContext context) {
    List<Widget> widgets = [];
    for (int i = 0; i < __item_count; ++i) {
        widgets.add(TextFormField(focusNode: _lstFocusNodes[i],controller: _controls[i],));
    }

    return Scaffold( body: Container( margin: const EdgeInsets.all(8),
        height: TEXT_ITEM_HEIGHT * __item_count,
        child: Form(key: _formKey, child: ListView( children: widgets)))
    );
}

Future<void> _keyboardToggled() async {
    if (mounted){
        EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
        while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
            await Future.delayed(const Duration(milliseconds: 10));
        }
    }

    return;
}
Future<void> _ensureVisible(int index,FocusNode focusNode) async {
    if (!focusNode.hasFocus){
        debugPrint("ensureVisible. has not the focus. return");
        return;
    }

    debugPrint("ensureVisible. $index");
    // Wait for the keyboard to come into view
    await Future.any([Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);


    var renderObj = focusNode.context!.findRenderObject();
    if( renderObj == null ) {
      return;
    }
    var vp = RenderAbstractViewport.of(renderObj);
    if (vp == null) {
        debugPrint("ensureVisible. skip. not working in Scrollable");
        return;
    }
    // Get the Scrollable state (in order to retrieve its offset)
    ScrollableState scrollableState = Scrollable.of(focusNode.context!)!;

    // Get its offset
    ScrollPosition position = scrollableState.position;
    double alignment;

    if (position.pixels > vp.getOffsetToReveal(renderObj, 0.0).offset) {
        // Move down to the top of the viewport
        alignment = 0.0;
    } else if (position.pixels < vp.getOffsetToReveal(renderObj, 1.0).offset){
        // Move up to the bottom of the viewport
        alignment = 1.0;
    } else {
        // No scrolling is necessary to reveal the child
        debugPrint("ensureVisible. no scrolling is necessary");
        return;
    }

    position.ensureVisible(
        renderObj,
        alignment: alignment,
        duration: const Duration(milliseconds: 300),
    );

}

}

Upvotes: 6

Jitesh Mohite
Jitesh Mohite

Reputation: 34250

Output:

Use Dependency:

dependencies:
    scroll_to_index: ^1.0.6

Code: (Scroll will always perform 6th index widget as its added below as hardcoded, try with scroll index which you required for scrolling to specific widget)

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final scrollDirection = Axis.vertical;

  AutoScrollController controller;
  List<List<int>> randomList;

  @override
  void initState() {
    super.initState();
    controller = AutoScrollController(
        viewportBoundaryGetter: () =>
            Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
        axis: scrollDirection);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView(
        scrollDirection: scrollDirection,
        controller: controller,
        children: <Widget>[
          ...List.generate(20, (index) {
            return AutoScrollTag(
              key: ValueKey(index),
              controller: controller,
              index: index,
              child: Container(
                height: 100,
                color: Colors.red,
                margin: EdgeInsets.all(10),
                child: Center(child: Text('index: $index')),
              ),
              highlightColor: Colors.black.withOpacity(0.1),
            );
          }),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _scrollToIndex,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
  // Scroll listview to the sixth item of list, scrollling is dependent on this number
  Future _scrollToIndex() async {
    await controller.scrollToIndex(6, preferPosition: AutoScrollPosition.begin);
  }
}

Upvotes: 5

Shahid Bangash
Shahid Bangash

Reputation: 41

I am posting a solution here in which List View will scroll 100 pixel right and left . you can change the value according to your requirements. It might be helpful for someone who want to scroll list in both direction

import 'package:flutter/material.dart';

class HorizontalSlider extends StatelessWidget {
 HorizontalSlider({Key? key}) : super(key: key);

// Dummy Month name
List<String> monthName = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"July",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
 ];
ScrollController slideController = new ScrollController();

@override
Widget build(BuildContext context) {
 return Container(
  child: Flex(
    direction: Axis.horizontal,
    crossAxisAlignment: CrossAxisAlignment.center,
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      InkWell(
        onTap: () {
          // Here monthScroller.position.pixels represent current postion 
          // of scroller
           slideController.animateTo(
            slideController.position.pixels - 100, // move slider to left
             duration: Duration(
              seconds: 1,
            ),
            curve: Curves.ease,
          );
        },
        child: Icon(Icons.arrow_left),
      ),
      Container(
        height: 50,
        width: MediaQuery.of(context).size.width * 0.7,
        child: ListView(
          scrollDirection: Axis.horizontal,
          controller: slideController,
          physics: ScrollPhysics(),
          children: monthName
              .map((e) => Padding(
                    padding: const EdgeInsets.all(12.0),
                    child: Text("$e"),
                  ))
              .toList(),
        ),
      ),
      GestureDetector(
        onTap: () {
          slideController.animateTo(
            slideController.position.pixels +
                100, // move slider 100px to right
            duration: Duration(
              seconds: 1,
            ),
            curve: Curves.ease,
          );
        },
        child: Icon(Icons.arrow_right),
      ),
    ],
  ),
);
 }
 }

Upvotes: 2

Shaik Mohammed
Shaik Mohammed

Reputation: 15

Simply use page view controller. Example:

   var controller = PageController();  
     
    ListView.builder(
      controller: controller,
      itemCount: 15,
      itemBuilder: (BuildContext context, int index) {
       return children[index);
      },
    ),
     ElevatedButton(
            onPressed: () {
              controller.animateToPage(5,   //any index that you want to go
    duration: Duration(milliseconds: 700), curve: Curves.linear);
              },
            child: Text(
              "Contact me",), 

       

Upvotes: -2

madhan gannarapu
madhan gannarapu

Reputation: 41

  1. To achieve initial scrolling at a particular index in a list of items
  2. on tap of the floating action button you will be scrolled to an index of 10 in a list of items
    class HomePage extends StatelessWidget {
      final _controller = ScrollController();
      final _height = 100.0;
    
      @override
      Widget build(BuildContext context) {
        
        // to achieve initial scrolling at particular index
        SchedulerBinding.instance.addPostFrameCallback((_) {
          _scrollToindex(20);
        });
    
        return Scaffold(
          appBar: AppBar(),
          floatingActionButton: FloatingActionButton(
            onPressed: () => _scrollToindex(10),
            child: Icon(Icons.arrow_downward),
          ),
          body: ListView.builder(
            controller: _controller,
            itemCount: 100,
            itemBuilder: (_, i) => Container(
              height: _height,
              child: Card(child: Center(child: Text("Item $i"))),
            ),
          ),
        );
      }
    // on tap, scroll to particular index
      _scrollToindex(i) => _controller.animateTo(_height * i,
          duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
    }

Upvotes: 3

Nhật Trần
Nhật Trần

Reputation: 2838

For people are trying to jump to widget in CustomScrollView. First, add this plugin to your project.

Then look at my example code below:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  AutoScrollController _autoScrollController;
  final scrollDirection = Axis.vertical;

  bool isExpaned = true;
  bool get _isAppBarExpanded {
    return _autoScrollController.hasClients &&
        _autoScrollController.offset > (160 - kToolbarHeight);
  }

  @override
  void initState() {
    _autoScrollController = AutoScrollController(
      viewportBoundaryGetter: () =>
          Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom),
      axis: scrollDirection,
    )..addListener(
        () => _isAppBarExpanded
            ? isExpaned != false
                ? setState(
                    () {
                      isExpaned = false;
                      print('setState is called');
                    },
                  )
                : {}
            : isExpaned != true
                ? setState(() {
                    print('setState is called');
                    isExpaned = true;
                  })
                : {},
      );
    super.initState();
  }

  Future _scrollToIndex(int index) async {
    await _autoScrollController.scrollToIndex(index,
        preferPosition: AutoScrollPosition.begin);
    _autoScrollController.highlight(index);
  }

  Widget _wrapScrollTag({int index, Widget child}) {
    return AutoScrollTag(
      key: ValueKey(index),
      controller: _autoScrollController,
      index: index,
      child: child,
      highlightColor: Colors.black.withOpacity(0.1),
    );
  }

  _buildSliverAppbar() {
    return SliverAppBar(
      brightness: Brightness.light,
      pinned: true,
      expandedHeight: 200.0,
      backgroundColor: Colors.white,
      flexibleSpace: FlexibleSpaceBar(
        collapseMode: CollapseMode.parallax,
        background: BackgroundSliverAppBar(),
      ),
      bottom: PreferredSize(
        preferredSize: Size.fromHeight(40),
        child: AnimatedOpacity(
          duration: Duration(milliseconds: 500),
          opacity: isExpaned ? 0.0 : 1,
          child: DefaultTabController(
            length: 3,
            child: TabBar(
              onTap: (index) async {
                _scrollToIndex(index);
              },
              tabs: List.generate(
                3,
                (i) {
                  return Tab(
                    text: 'Detail Business',
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        controller: _autoScrollController,
        slivers: <Widget>[
          _buildSliverAppbar(),
          SliverList(
              delegate: SliverChildListDelegate([
            _wrapScrollTag(
                index: 0,
                child: Container(
                  height: 300,
                  color: Colors.red,
                )),
            _wrapScrollTag(
                index: 1,
                child: Container(
                  height: 300,
                  color: Colors.red,
                )),
            _wrapScrollTag(
                index: 2,
                child: Container(
                  height: 300,
                  color: Colors.red,
                )),
          ])),
        ],
      ),
    );
  }
}

Yeah it's just a example, use your brain to make it this idea become true enter image description here

Upvotes: 31

Michał Dobi Dobrzański
Michał Dobi Dobrzański

Reputation: 2000

Here is the solution for StatefulWidget if you want to made widget visible right after building the view tree.

By extending Remi's answer, you can achieve it with this code:

class ScrollView extends StatefulWidget {
  // widget init
}

class _ScrollViewState extends State<ScrollView> {

  final dataKey = new GlobalKey();

  // + init state called

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      primary: true,
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: _renderBody(),
    );
  }

  Widget _renderBody() {
    var widget = SingleChildScrollView(
        child: Column(
          children: <Widget>[
           SizedBox(height: 1160.0, width: double.infinity, child: new Card()),
           SizedBox(height: 420.0, width: double.infinity, child: new Card()),
           SizedBox(height: 760.0, width: double.infinity, child: new Card()),
           // destination
           Card(
              key: dataKey,
              child: Text("data\n\n\n\n\n\ndata"),
            )
          ],
        ),
      );
    setState(() {
        WidgetsBinding.instance!.addPostFrameCallback(
              (_) => Scrollable.ensureVisible(dataKey.currentContext!));
    });
    return widget;
  }
}

Upvotes: 7

Saikat halder
Saikat halder

Reputation: 618

Adding with Rémi Rousselet's answer,

If there is a case you need to scroll past to end scroll position with addition of keyboard pop up, this might be hided by the keyboard. Also you might notice the scroll animation is a bit inconsistent when keyboard pops up(there is addition animation when keyboard pops up), and sometimes acts weird. In that case wait till the keyboard finishes animation(500ms for ios).

BuildContext context = key.currentContext;
  Future.delayed(const Duration(milliseconds: 650), () {
    Scrollable.of(context).position.ensureVisible(
        context.findRenderObject(),
        duration: const Duration(milliseconds: 600));
  });

Upvotes: 0

R&#233;mi Rousselet
R&#233;mi Rousselet

Reputation: 277447

By far, the easiest solution is to use Scrollable.ensureVisible(context). As it does everything for you and work with any widget size. Fetching the context using GlobalKey.

The problem is that ListView won't render non-visible items. Meaning that your target most likely will not be built at all. Which means your target will have no context ; preventing you from using that method without some more work.

In the end, the easiest solution will be to replace your ListView by a SingleChildScrollView and wrap your children into a Column. Example :

class ScrollView extends StatelessWidget {
  final dataKey = new GlobalKey();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      primary: true,
      appBar: new AppBar(
        title: const Text('Home'),
      ),
      body: new SingleChildScrollView(
        child: new Column(
          children: <Widget>[
            new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
            new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
            new SizedBox(height: 160.0, width: double.infinity, child: new Card()),
            // destination
            new Card(
              key: dataKey,
              child: new Text("data\n\n\n\n\n\ndata"),
            )
          ],
        ),
      ),
      bottomNavigationBar: new RaisedButton(
        onPressed: () => Scrollable.ensureVisible(dataKey.currentContext),
        child: new Text("Scroll to data"),
      ),
    );
  }
}

NOTE : While this allows to scroll to the desired item easily, consider this method only for small predefined lists. As for bigger lists you'll get performance problems.

But it's possible to make Scrollable.ensureVisible work with ListView ; although it will require more work.

Upvotes: 277

Maddy Leo
Maddy Leo

Reputation: 1

You can use the controller.jumpTo(100) after the loading finish

Upvotes: -5

Hemanth Raj
Hemanth Raj

Reputation: 34809

You can just specify a ScrollController to your listview and call the animateTo method on button click.

A mininmal example to demonstrate animateTo usage :

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => new _ExampleState();
}

class _ExampleState extends State<Example> {
  ScrollController _controller = new ScrollController();

  void _goToElement(int index){
    _controller.animateTo((100.0 * index), // 100 is the height of container and index of 6th element is 5
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(),
      body: new Column(
        children: <Widget>[
          new Expanded(
            child: new ListView(
              controller: _controller,
              children: Colors.primaries.map((Color c) {
                return new Container(
                  alignment: Alignment.center,
                  height: 100.0,
                  color: c,
                  child: new Text((Colors.primaries.indexOf(c)+1).toString()),
                );
              }).toList(),
            ),
          ),
          new FlatButton(
            // on press animate to 6 th element
            onPressed: () => _goToElement(6),
            child: new Text("Scroll to 6th element"),
          ),
        ],
      ),
    );
  }
}

Upvotes: 11

Related Questions