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 ScrollController
s 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:
- This does not work for infinite lists.
- The scroll behavior is bad because the background is only scrolled when the scroller ends his gesture by put the finger up.
- 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),
),
),
);
}
}