Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GoogleMap's onCameraIdle stops firing when in a SingleChildScrollView

I have a design where a SingleChildScrollView has a GoogleMap on the first 50% on the screen, and a listing of items in the lower 50%. The user can scroll the entire view up to look at all of the listings. However, at times the Map stops firing the onCameraIdle if the users scrolls the page, and it just won't start firing it again.

onCameraMove and onTap works just fine. It is just onCameraIdle that won't fire.

 SingleChildScrollView(
        child: Column(
          children: <Widget>[
            Stack(
              children: <Widget>[
                Container(
                  height: screenSize.height / 2,
                  child: GoogleMap(
                    key: Key("GMap"),
                    mapType: MapType.normal,
                    markers: Set<Marker>.of(markers.values),
                    gestureRecognizers: Set()
                      ..add(Factory<PanGestureRecognizer>(
                          () => PanGestureRecognizer()))
                      ..add(
                        Factory<VerticalDragGestureRecognizer>(
                            () => VerticalDragGestureRecognizer()),
                      )
                      ..add(
                        Factory<HorizontalDragGestureRecognizer>(
                            () => HorizontalDragGestureRecognizer()),
                      )
                      ..add(
                        Factory<ScaleGestureRecognizer>(
                            () => ScaleGestureRecognizer()),
                      ),
                    initialCameraPosition: CameraPosition(
                        target: LatLng(14.551620, 121.053329), zoom: 14.5),
                    onMapCreated: (GoogleMapController controller) {
                      if (!_controller.isCompleted) {
                        _controller.complete(controller);
                        _lastCameraPosition = CameraPosition(
                            target: LatLng(14.551620, 121.053329), zoom: 14.5);
                      }
                    },
                    myLocationEnabled: true,
                    myLocationButtonEnabled: true,
                    onCameraIdle: () {
                      print("547: onCameraIdle");
                      _fetchOffers();
                    },
                    onCameraMove: (value) {
                      print("552: onCameraMove");
                      _lastCameraPosition = value;
                    },
                    onTap: (value) {
                      // Load items for current view if deselecting a marker
                      print('556: Tapped outside');
                    },
                  ),
                ),
                Positioned(
                  top: 50,
                  right: 20,
                  child: Container(
                    height: 30,
                    decoration: BoxDecoration(
                        border: Border.all(
                            color: _userBalance > 0
                                ? globals.themeColor4
                                : globals.themeColor2,
                            width: 2),
                        boxShadow: [
                          BoxShadow(
                            blurRadius: 10.0,
                            color: Colors.black.withOpacity(.5),
                            offset: Offset(3.0, 4.0),
                          ),
                        ],
                        color: Colors.white,
                        borderRadius: BorderRadius.all(Radius.circular(10.0))),
                    child: Center(
                      child: Padding(
                        padding: EdgeInsets.fromLTRB(10, 5, 10, 5),
                        child: Text(
                          "Balance: \u{20B1} ${_userBalance.toStringAsFixed(0)}",
                          style: TextStyle(
                            color: Colors.black,
                            fontSize: 14,
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
            AnimatedContainer(
              color: Colors.white,
              // Use the properties stored in the State class.
              width: double.infinity,
              height: _loaderHeight,
              // Define how long the animation should take.
              duration: Duration(seconds: 1),
              // Provide an optional curve to make the animation feel smoother.
              curve: Curves.fastOutSlowIn,
              child: Center(
                child: Text(
                  "Loading, please wait",
                  style: TextStyle(color: Colors.grey),
                ),
              ),
            ),
            Container(
              color: Colors.white,
              child: _offers == null
                  ? Container(
                      child: Padding(
                        padding: EdgeInsets.all(30),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.start,
                          children: <Widget>[
                            Icon(MdiIcons.foodAppleOutline,
                                size: 60, color: globals.themeColor4),
                            Padding(padding: EdgeInsets.only(right: 20)),
                            Expanded(
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Text("Fetching offers",
                                      style: TextStyle(
                                          color: globals.themeColor4,
                                          fontWeight: FontWeight.bold)),
                                  Padding(padding: EdgeInsets.only(top: 5)),
                                  Text(
                                      "We are fetching offers for you, please hold on...",
                                      style: TextStyle(color: Colors.grey)),
                                ],
                              ),
                            ),
                          ],
                        ),
                      ),
                    )
                  : Column(children: _offers),
            ),
          ],
        ),
      ),

Has anyone encountered this before and have a solution to it?

like image 926
Robert Benedetto Avatar asked Jul 14 '19 11:07

Robert Benedetto


2 Answers

I replicated a simple version of your sample, but I wasn’t able to reproduce an issue with onCameraIdle not firing.

Now, based off of my sample, there were some behaviors that you could have misinterpreted as not working, but is actually the scrollview’s behavior taking over (since this is all inside a scrollview):

  1. Sometimes a downward gesture on the map would pull on the scrollview instead of the map.
  2. And an upward gesture would scroll the scrollview instead of interacting with the map.

But without any further details into the rest of your code, or a mcve that can easily reproduce your issue, it’s hard to say what’s really going on.

However, as Patrick Kelly mentioned, it’s also possible that the lack of a KeepAlive might have eventually led to the temporary disposing of your maps widget. Which is why ListView was suggested because this feature is built into it.

On the other hand, you can also implement AutomaticKeepAliveClientMixin for a similar effect, as seen over at https://stackoverflow.com/a/51738269/6668797 (but beware of the warning for the widget disposing).

Anyways, here’s my sample, and I had to make an educated guess on what your _fetchOffers() is:

class _MyHomePageState extends State<MyHomePage> {

  // testing
  int fetchCount = 0;
  List<Widget> _offers;
  _fetchOffers() {
    fetchCount++;
    // simulate varying data
    var rng = new Random();
    int start = rng.nextInt(10);
    int end = start + 3 + rng.nextInt(30);
    // build sample list
    List<Widget> list = new List();
    for (int i = start; i < end; i++) {
      list.add(Text('offer$i', style: new TextStyle(fontSize: 30.0)));
    }

    // assuming you are using setState()
    setState(() {
      _offers = list;
    });
  }

  // from google maps sample
  Completer<GoogleMapController> _controller = Completer();
  static final CameraPosition _kGooglePlex = CameraPosition(
    target: LatLng(37.42796133580664, -122.085749655962),
    zoom: 14.4746,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            Stack(
              children: <Widget>[
                Container(
                  height: MediaQuery.of(context).size.height / 2,
                  child: GoogleMap(
                    mapType: MapType.normal,
                    initialCameraPosition: _kGooglePlex,
                    gestureRecognizers: Set()
                      ..add(Factory<PanGestureRecognizer>(() => PanGestureRecognizer()))
                      ..add(Factory<VerticalDragGestureRecognizer>(() => VerticalDragGestureRecognizer()))
                      ..add(Factory<HorizontalDragGestureRecognizer>(() => HorizontalDragGestureRecognizer()))
                      ..add(Factory<ScaleGestureRecognizer>(() => ScaleGestureRecognizer())),
                    onMapCreated: (GoogleMapController controller) {
                      _controller.complete(controller);
                    },
                    onCameraIdle: () {
                      _fetchOffers();
                    },
                  ),
                ),
              ]
            ),
            Container(
              child: _offers == null
                  ? Container(child: Text("Fetching offers"))
                  : Column(children: _offers)
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
          // for logging _fetchOffers activity
          child: Text(fetchCount.toString())
      ),
    );
  }
}

onCameraIdle fired every time for me, and I could visually confirm it with the changing offers data as well as the fetchCount log.

like image 87
TWL Avatar answered Nov 07 '22 22:11

TWL


You could use a ListView or a CustomScrollView with KeepAlive

This prevents Widgets from being thrown out when scrolled out of view.

I would alternatively recommend digging into the ScrollController class

like image 41
Patrick Kelly Avatar answered Nov 07 '22 23:11

Patrick Kelly