Home > OS >  How to scroll underlying widget in Flutter?
How to scroll underlying widget in Flutter?

Time:12-06

I have a simple app with two screens. The first screen is a scrollable ListView and the second screen is basically empty and transparent. If I pushed the second screen with Navigator.push() on top of the first screen I'd like to be able to scroll the underlying first screen.

Here is my code:

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: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
        itemBuilder: (context, index) {
          return Text("$index");
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            PageRouteBuilder<void>(
              opaque: false, // push route with transparency
              pageBuilder: (context, animation, secondaryAnimation) => Foo(),
            ),
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}


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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white.withOpacity(0.5),
      appBar: AppBar(
        title: Text("I'm on top"),
      ),
    );
  }
}

How can scroll the list in the backgound while the second screen is in the foreground?

CodePudding user response:

Although this is not a solution with a second screen, it creates a similar effect using the Stack and IgnorePointer widgets:

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: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  bool _applyOverlay = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: _applyOverlay
            ? IconButton(
                icon: Icon(
                  Icons.arrow_back_ios_sharp,
                ),
                onPressed: () => setState(
                  () => _applyOverlay = false,
                ),
              )
            : null,
        title: Text(_applyOverlay ? 'Overlay active' : widget.title),
      ),
      body: Stack(
        children: [
          ListView.builder(
            itemBuilder: (context, index) {
              return Text("$index");
            },
          ),
          if (_applyOverlay)
          // Wrap container (or your custom widget) with IgnorePointer to ignore any user input 
            IgnorePointer(
              child: Container(
                height: double.infinity,
                width: double.infinity,
                color: Colors.white.withOpacity(0.5),
              ),
            ),
        ],
      ),
      floatingActionButton: _applyOverlay
          ? null
          : FloatingActionButton(
              onPressed: () {
                setState(() => _applyOverlay = true);
              },
              child: Icon(Icons.add),
            ),
    );
  }
}

CodePudding user response:

I found a solution that works even with two screens. The idea is to have two ScrollControllers each in one screen and add a listener the ScrollController in the overlay that triggers the ScrollController of the underlying widget.

import 'package:flutter/material.dart';

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

const INITIAL_OFFSET = 5000.0;

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

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final ScrollController controller = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
        controller: controller,
        itemBuilder: (context, index) {
          return Text("$index");
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            PageRouteBuilder<void>(
              opaque: false, // push route with transparency
              pageBuilder: (context, animation, secondaryAnimation) => Foo(
                controller: controller,
              ),
            ),
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  final ScrollController controller;
  const Foo({required this.controller, Key? key}) : super(key: key);

  @override
  State<Foo> createState() => _FooState();
}

class _FooState extends State<Foo> {
  final ScrollController controller = ScrollController(
    initialScrollOffset: INITIAL_OFFSET,
  );
  
  @override
  void initState(){
    super.initState();

    controller.addListener(() {
      widget.controller.animateTo(
        controller.offset - INITIAL_OFFSET,
        duration: const Duration(milliseconds: 1),
        curve: Curves.linear,
      );
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white.withOpacity(0.5),
      appBar: AppBar(
        title: Text("I'm on top"),
      ),
      body: SingleChildScrollView(
        controller: controller,
        child: Container(
          height: 2 * INITIAL_OFFSET,
          color: Colors.white.withOpacity(0.5),
        ),
      ),
    );
  }
}

There are still a few problems with this workaround:

  1. This does not work for infinite lists.
  2. The scroll behavior is bad because the background is only scrolled when the scroller ends his gesture by put the finger up.
  3. The sizes of the both screens doesn't match. That leads to bad effects like scrolling in areas that doesn't exists in the other screen.

CodePudding user response:

I finally found a satisfying answer that does not contain any kind of dirty workarounds. I use a Listener in the second screen to detect OnPointerMoveEvents which are basically scroll events.

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: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final ScrollController controller = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
        controller: controller,
        itemBuilder: (context, index) {
          return Text("$index");
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            PageRouteBuilder<void>(
              opaque: false, // push route with transparency
              pageBuilder: (context, animation, secondaryAnimation) => Foo(
                controller: controller,
              ),
            ),
          );
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  final ScrollController controller;
  const Foo({required this.controller, Key? key}) : super(key: key);

  @override
  State<Foo> createState() => _FooState();
}

class _FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white.withOpacity(0.5),
      appBar: AppBar(
        title: Text("I'm on top"),
      ),
      body: Listener(
        onPointerMove: (event){
          var newPosition = widget.controller.position.pixels - event.delta.dy;
          widget.controller.jumpTo(newPosition);
        },
        child: Container(
          height: MediaQuery.of(context).size.height,
          width: MediaQuery.of(context).size.width,
          color: Colors.red.withOpacity(0.5),
        ),
      ),
    );
  }
}
  • Related