Scott Clements
Scott Clements

Reputation: 173

Flutter: Override 'PageView' scroll gesture with child's GestureDetector

I am using a combination of BottomNavigationBar and PageView for navigation in my app. The user can either swipe to the next page or use the navigation bar.

On one of my pages, I would like to use a gesture detector that handles pan gestures, both vertically and horizontally.

I can't find a way to override PageView's gesture detection with the nested GestureDetector. This means only vertical pan gestures are handled, as the horizontal ones are occupied by the PageView.

How can I disable/override the PageViews gesture detection for only that page or only the widget, without completely disabling the PageViews scroll physics?

I have created a simplified version of my App to isolate the issue, and attached a video of the problem below.

Any help would be greatly appreciated!

'GestureDetector' only recieves vertical drag gestures...

Here is the code inside my main.dart:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: GestureIssueExample(),
    );
  }
}

class GestureIssueExample extends StatefulWidget {
  GestureIssueExample({Key key}) : super(key: key);

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

class _GestureIssueExampleState extends State<GestureIssueExample> {
  int _navigationIndex;
  double _xLocalValue;
  double _yLocalValue;
  PageController _pageController;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: null,
      bottomNavigationBar: _buildBottomNavigationBar(),
      backgroundColor: Colors.white,
      body: PageView(
        controller: _pageController,
        onPageChanged: _onNavigationPageChanged,
        children: [
          //Just a placeholder to represent a page to the left of the "swipe cards" widget
          _buildSamplePage("Home"),

          //Center child of 'PageView', contains a GestureDetector that handles Pan Gestures
          //Thanks to the page view however, only vertical pan gestures are detected, while both horizontal and vertical gestures
          //need to be handled...
          Container(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(
                    "Local X: ${_xLocalValue.toString()}\nLocal Y: ${_yLocalValue.toString()}"),
                GestureDetector(
                  onPanStart: (details) => setState(
                    () {
                      this._xLocalValue = details.localPosition.dx;
                      this._yLocalValue = details.localPosition.dy;
                    },
                  ),
                  onPanUpdate: (details) => setState(
                    () {
                      this._xLocalValue = details.localPosition.dx;
                      this._yLocalValue = details.localPosition.dy;
                    },
                  ),
                  child: Container(
                    width: MediaQuery.of(context).size.width * 0.9,
                    height: 100.0,
                    color: Colors.red,
                    alignment: Alignment.center,
                    child: Text("Slidable Surface",
                        style: TextStyle(color: Colors.white)),
                  ),
                ),
              ],
            ),
          ),

          //Just a placeholder to represent a page to the right of the "swipe cards" widget
          _buildSamplePage("Settings"),
        ],
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    this._navigationIndex = 0;
    this._pageController = PageController(
      initialPage: _navigationIndex,
    );
  }

  Widget _buildSamplePage(String text) {
    // This simply returns a container that fills the page,
    // with a text widget in its center.

    return Container(
      width: MediaQuery.of(context).size.width,
      height: MediaQuery.of(context).size.height,
      color: Colors.grey[900],
      alignment: Alignment.center,
      child: Text(
        text,
        style: TextStyle(
            color: Colors.white, fontSize: 30.0, fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget _buildBottomNavigationBar() {
    //Returns the bottom navigation bar for the scaffold
    return BottomNavigationBar(
      backgroundColor: Colors.grey[900],
      selectedItemColor: Colors.redAccent,
      unselectedItemColor: Colors.white,
      items: [
        BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: "Home"),
        BottomNavigationBarItem(
            icon: Icon(Icons.check_box_outline_blank), label: "Cards"),
        BottomNavigationBarItem(
            icon: Icon(Icons.settings_outlined), label: "Settings"),
      ],
      currentIndex: _navigationIndex,
      onTap: _onNavigationPageChanged,
    );
  }

  void _onNavigationPageChanged(int newIndex) {
    //Set the new navigation index for the nav bar
    setState(() => this._navigationIndex = newIndex);

    //Animate to the selected page
    _pageController.animateToPage(
      newIndex,
      curve: Curves.easeInOut,
      duration: Duration(microseconds: 100),
    );
  }
}

Upvotes: 6

Views: 2249

Answers (2)

Valkyrie
Valkyrie

Reputation: 109

The easiest way to achieve this is to override the gesture that is handled by the ancestor widget with the same function that you are using for your pan gesture. So in this case

...
GestureDetector(
  onPanStart: _panStart,
  onPanUpdate: _panUpdate,
  onHorizontalDragStart:  _panStart,
  onHorizontalDragUpdate: _panUpdate,
  child: ...
);
...

void _panStart(details){
  setState(
        () {
      this._xLocalValue = details.localPosition.dx;
      this._yLocalValue = details.localPosition.dy;
    },
  );
}

void _panUpdate(details){
  setState(
        () {
      this._xLocalValue = details.localPosition.dx;
      this._yLocalValue = details.localPosition.dy;
    },
  );
}

This way, the swipe gestures will be handled by the child widget the same way regular pan gestures are handled.

Upvotes: 1

Andrej
Andrej

Reputation: 3225

Can you try something like this:

Add this line to your PageView:

PageView(
...
physics: _navigationIndex == 1 ? NeverScrollableScrollPhysics() : AlwaysScrollableScrollPhysics(),
...
)

Note: the number 1 is because the page with the GestureDetector is on index 1.

Upvotes: 1

Related Questions