Flutter - PageView - Don't change page if the user still touches the screen - flutter

How to update the PageView to trigger onPageChange only on specific conditions?
Here, I don't want to change the current page if the user is still touching the screen. Apart from that, everything should remain the same (ballistic scroll simulation, page limits)
It seems it has to deal with the ScrollPhysics object attached to PageView, but I don't know how to correctly extends it.
Let me know if you need some code, but the question is very general and can refer to any PageView, so you should not need any context.
Minimum Reproductible Example
Here is the translation in dart of the text above. Feel free to update this code to make it achieve the objective.
// main.dart
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(title: _title, home: MyPageView());
}
}
class MyPageView extends StatefulWidget {
const MyPageView({Key? key}) : super(key: key);
#override
State<MyPageView> createState() => _MyPageViewState();
}
class _MyPageViewState extends State<MyPageView> {
#override
Widget build(BuildContext context) {
final PageController controller = PageController();
return Scaffold(
body: SafeArea(
child: PageView.builder(
onPageChanged: (int index) {
// TODO: Don't trigger this function if you still touch the screen
print('onPageChanged index $index, ${controller.page}');
},
allowImplicitScrolling: false,
controller: controller,
itemBuilder: (BuildContext context, int index) {
print('Build Sliver');
return Center(
child: Text('Page $index'),
);
},
)));
}
}
Example of a (bad) solution
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(title: _title, home: MyPageView());
}
}
class MyPageView extends StatefulWidget {
const MyPageView({Key? key}) : super(key: key);
#override
State<MyPageView> createState() => _MyPageViewState();
}
class _MyPageViewState extends State<MyPageView> {
#override
Widget build(BuildContext context) {
final PageController controller = PageController();
return Scaffold(
body: SafeArea(
child: Listener(
onPointerUp: (PointerUpEvent event) {
if (controller.page == null) {
return;
}
if (controller.page! > 0.5) {
//TODO: update the time so it fits the end of the animation
Future.delayed(const Duration(milliseconds: 700), () {
print('Do your custom action onPageChange action here');
});
}
},
child: PageView.builder(
controller: controller,
itemBuilder: (BuildContext context, int index) {
print('Build Sliver');
return Center(
child: Text('Page $index'),
);
},
),
),
));
}
}
This solution triggers an action on the next page, 700ms after the user stops touching the screen.
It does work, but it is a lousy work.
How to account for different screen sizes? 700ms is the maximum amount of time to animate between 2 pages on an iPhone SE.
How to adjust this arbitrary number (700), so it varies according to controller.page (the closer to the next page, the smaller you have to wait).
It doesn't use onHorizontalDragEnd or a similar drag detector, which can result in unwanted behaviour.

You should disable the scrolling entirely on PageView with physics: NeverScrollableScrollPhysics() and detect the scroll left and right on your own with GestureDetector. The GestureDetector.onHorizontalDragEnd will tell which direction the user dragged, to the left or to the right, checking the parameter's DragEndDetails property primaryVelocity. If the value is negative the user dragged to the right and is positive if the user dragged to the left.
To change the page manually just use the PageController methods nextPage and previousPage.
Take a look at the screenshot below and the live demo on DartPad.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
debugShowCheckedModeBanner: false,
scrollBehavior: MyCustomScrollBehavior(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late PageController _pageController;
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onHorizontalDragEnd: (details) => (details.primaryVelocity ?? 0) < 0
? _pageController.nextPage(
duration: const Duration(seconds: 1), curve: Curves.easeInOut)
: _pageController.previousPage(
duration: const Duration(seconds: 1), curve: Curves.easeInOut),
child: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: [
Container(
color: const Color.fromARGB(255, 0, 91, 187),
),
Container(
color: const Color.fromARGB(255, 255, 213, 0),
),
],
),
),
);
}
}
class MyCustomScrollBehavior extends MaterialScrollBehavior {
#override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}

You can simply use physics: NeverScrollableScrollPhysics() inside PageView() to achieve this kind of behaviour

I struggled with the same solution and built a complex custom gesture controller with drag listeners.
However, your so called bad example seems like the right direction.\
Why have this 700ms at all?\
You already have the onPointerUp event, where you can check the current page by using controller.page.round().\
You can also check that there is a dragging going on at this pointerUp by comparing controller.page==controller.page.floor()

Related

As a common appbar widget how to change appbar color when page is scrolled Flutter

Good morning friends, I'm trying to make the appbar transparent or white in scrollable
parts.
For me, this solution An efficient way in Flutter to change appbar color when scrolled works, but as the person said, I don't want to use setState continuously and do it in every separate component, so I'm trying to do what is mentioned in the comment. For this reason, I created a common appbar widget so that I can use it in other components. I made the CustomAppBar widget statefull, but I don't know where to add the scrollController. Therefore, I see errors. If anyone has time, can you help?
The code below is the widget where I call CustomAppBar.
import ...
const ExtractionBody({Key? key, required this.goal}) : super(key: key);
final Objective goal;
#override
ExtractionBodyState createState() => ExtractionBodyState();
}
class ExtractionBodyState extends ConsumerState<ExtractionBody> {
#override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
child: CustomAppBar(
icon: IconButton(
icon: const Icon(PhosphorIcons.xBold),
onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil(
HomePage.routeName,
(route) => false,
),
),
),
),
body: ExtractionRequestContent(
goal: widget.goal, scrollController: _scrollController),
);
}
}
Finally, this is my CustomAppBar code. Thank you very much in advance. and have a good weekend everyone
class CustomAppBar extends StatefulHookConsumerWidget {
static String routeName = "/extraction_body";
const CustomAppBar({Key? key, this.icon})
: preferredSize = const Size.fromWidth(50),
super(key: key);
final Widget? icon;
#override
final Size preferredSize; // default is 56.0
#override
CustomAppBarState createState() => CustomAppBarState();
}
class CustomAppBarState extends ConsumerState<CustomAppBar> {
bool isAppbarCollapsing = false;
final ScrollController _scrollController = ScrollController();
#override
void initState() {
super.initState();
_initializeController();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _initializeController() {
_scrollController.addListener(() {
if (_scrollController.offset == 0.0 &&
!_scrollController.position.outOfRange) {
//Fully expanded situation
if (!mounted) return;
setState(() => isAppbarCollapsing = false);
}
if (_scrollController.offset >= 9.0 &&
!_scrollController.position.outOfRange) {
//Collapsing situation
if (!mounted) return;
setState(() => isAppbarCollapsing = true);
}
});
}
#override
Widget build(BuildContext context) {
return AppBar(
elevation: 0,
backgroundColor:
isAppbarCollapsing ? AppColors.monochromeWhite : Colors.transparent,
title: Text(context.l10n.buttonCancel),
titleSpacing: 4,
leading: widget.icon,
);
}
}
Thanks!
Instead of define ScrollController in CustomAppBar, pass it in constructor like this:
class CustomAppBar extends StatefulHookConsumerWidget {
static String routeName = "/extraction_body";
const CustomAppBar({Key? key, this.icon, required this.scrollController})
: preferredSize = const Size.fromWidth(50),
super(key: key);
final Widget? icon;
final ScrollController scrollController;
#override
final Size preferredSize; // default is 56.0
#override
CustomAppBarState createState() => CustomAppBarState();
}
and use it like this:
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final ScrollController scrollController = ScrollController(); //<---- define scrollController here
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
child: CustomAppbar(scrollController: scrollController)),
body: ListView.builder(
controller: scrollController,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
width: 100,
color: Colors.red,
margin: EdgeInsets.all(12),
);
},
),
));
}
}

Flutter - The const widget still rebuild when its parent rebuild

I have a question about when and how a const widget will rebuild.
For example, I have a demo project like this:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool isChecked = false;
#override
Widget build(BuildContext context) {
const child = ChildWidget();
return OrientationBuilder(
builder: (context, orientation) {
debugPrint('$orientation');
final isPortrait = orientation == Orientation.portrait;
return Container(
alignment: Alignment.topCenter,
child: isPortrait
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
SizedBox(height: 200, child: child),
Text('Bellow text'),
])
: child,
);
},
);
}
}
class ChildWidget extends StatefulWidget {
const ChildWidget({Key? key}) : super(key: key);
#override
State<ChildWidget> createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_counter += 1;
});
},
child: Container(
color: Colors.blue, child: Center(child: Text('$_counter'))),
);
}
}
As you see, I have a const ChildWidget.
const child = ChildWidget();
When I rotate the device, it will trigger the builder function of the OrientationBuilder and return a new Container. But my question is why the child widget is rebuilt again while it is a const.
The reason why I want the child widget is not rebuilt is that I don't want the counter Text to reset to 0 each time I rotate the device.
Please advice.
Thanks a lot.
While the variable and object assigned are constant, the framework will still call the build method on the child widgets. So the actual ChildWidget class is not recreated, but the build will be called.
This is not really a problem. Flutter is really optimized for rebuilding Widgets. If the data has not changed, the actual cost of rebuilding is negligible.

How to set splash screen time out on flutter

I am new to flutter and am kinda lost on how to set up a time to my splash screen so after this time it goes to the main screen. am using rive for the splash screen
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
void main() {
runApp(const MaterialApp(home: SimpleAnimation()));
}
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key, this.loading}) : super(key: key);
final bool? loading;
#override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: RiveAnimation.asset('assets/splash/splash.riv',
fit: BoxFit.cover)
),
);
}
}
You can set 3 second time in initstate after navigate to which screen you want
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
// TODO: implement initState
super.initState();
// after 3 second it will navigate
Future.delayed(const Duration(seconds: 3)).then((val) {
// Navigation Here
});
}
#override
Widget build(BuildContext context) {
return const Scaffold(
// your code
);
}
}
#override
void initState() {
//set timer for splash screen
Timer(const Duration(seconds: 4), () async {
//add your logic here
Navigator.pushNamedAndRemoveUntil(
context, ScreenRoute.mainScreen, (route) => false);
super.initState();
}
This SimpleAnimation widget shows after the splash screen. While this is StatelessWidget widget, you can define method inside build method. Change Duration(seconds: 2) based on your need.
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
Future.delayed(const Duration(seconds: 2)).then((value) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NextScreen(),
));
});
return const Scaffold(
body: Center(
As folks already mentioned the straighforward way would be to add a delay and do navigation after it:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const MainScreen(),
),
);
}
});
}
#override
Widget build(BuildContext context) {
return const ColoredBox(color: Colors.green);
}
}
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const ColoredBox(color: Colors.yellow);
}
}
Though, with this implementation, you'll have to depend on the animation length. So when you'll update animation - you'll have not to forget to update it inside the splash screen. A more reliable (and complex) solution would be to listen to the animation status and do the navigation after the animation finishes. Like this (warning, change ):
class PlayOneShotAnimation extends StatefulWidget {
const PlayOneShotAnimation({Key? key}) : super(key: key);
#override
_PlayOneShotAnimationState createState() => _PlayOneShotAnimationState();
}
class _PlayOneShotAnimationState extends State<PlayOneShotAnimation> {
late RiveAnimationController _controller;
#override
void initState() {
super.initState();
_controller = OneShotAnimation(
'bounce',
autoplay: true,
onStop: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const MainScreen(),
),
);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RiveAnimation.network(
'https://cdn.rive.app/animations/vehicles.riv',
animations: const ['idle', 'curves'],
controllers: [_controller],
),
),
);
}
}
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const ColoredBox(color: Colors.yellow);
}
}
This is my approach for splash screen, the advantage of this approach is to make sure that the splash screen launch only once when the app starting.
First define a static bool in app home class to indicate the app launch.
static bool launch = true;
Then at the home attribute in your MaterialApp widget at app home class, check if (launch) is true use a FutureBuilder to launch the splash screen, if (launch) is false set home to your second screen. With FutureBuilder you can set a timer for your splash screen, when it done your second screen will start (credit to https://stackoverflow.com/a/68699447/11619215).
home: launch? FutureBuilder(
future: Future.delayed(const Duration(seconds: 3)),
builder: (ctx, timer) =>
timer.connectionState == ConnectionState.done
? const SecondScreen(title: 'Flutter Demo Home Page')
: appSplashScreen(),
): const SecondScreen(title: 'Flutter Demo Home Page'),
In the Second screen, check if (launch) is true then set it to false. This will make sure that the splash screen will only launch once each time your application start.
if(AppHome.launch) {
AppHome.launch = false;
}
Below is the full code with appSplashScreen widget at the bottom:
import 'package:flutter/material.dart';
void main() {
runApp(const AppHome());
}
class AppHome extends StatelessWidget {
const AppHome({Key? key}) : super(key: key);
//static bool to indicate the launching of the app
static bool launch = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: launch? FutureBuilder(
future: Future.delayed(const Duration(seconds: 3)),
builder: (ctx, timer) =>
timer.connectionState == ConnectionState.done
? const SecondScreen(title: 'Flutter Demo Home Page')
: appSplashScreen(),
): const SecondScreen(title: 'Flutter Demo Home Page'),
);
}
}
class SecondScreen extends StatefulWidget {
const SecondScreen({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
#override
Widget build(BuildContext context) {
//mack sure your splash screen only launch once at your app starting
if(AppHome.launch) {
AppHome.launch = false;
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const Center(
child: Text(
'My Second screen',
),
),
);
}
}
Widget appSplashScreen() {
return Container(
decoration: const BoxDecoration(
////you can add background image/color to your splash screen
// image: DecorationImage(
// image: AssetImage('assets/background.png'),
// fit: BoxFit.cover,
// ),
color: Colors.white,
),
child: Center(
child: SizedBox(
//get MediaQuery from instance of window to get height and width (no need of context)
height: MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.height*0.5,
width: MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width*0.5,
child: Column(
children: const [
////you can add image to your splash screen
// Image(
// image: AssetImage('assets/splashscreen_image.png'),
// ),
FittedBox(
child: Text(
'Loading',
textAlign: TextAlign.center,
style: TextStyle(
decoration: TextDecoration.none,
),
)
),
CircularProgressIndicator(),
],
),
),
),
);
}

Flutter ReorderableListView -- resizing item on drag and adjusting layout of other items accordingly

I have a ReorderableListView, and I'm using proxyDecorator to detect when an item starts being dragged and then adjust its height, and the height of the underlying Widget which is actually present in ListView (as I assume the widget returned by proxyDecorator is just used to show what is being dragged.) The issue is that the 'space' in the list where the dragged item was, or is being moved to, remains its original size, rather than reflecting the new size of the widget. Once the dragged item is placed, if I disable the code to return it to it's original size, the rest of the listview bunches up to reflect the new size. But I want this to happen as soon as the drag begins. Is this possible? I have tried using a setState call in the proxyDecorator, but this causes an error with triggering a rebuild during a rebuild.
I'm not sure what you meant by proxy decorator, but if you just want to resize an item when it's being dragged, it can be done.
Obviously this is just a quick demo, lots of edge cases are not tested. But if you are interested, here's the full source code used in the above demo:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final colors = [
Colors.red,
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.cyan
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ReorderableListView'),
),
body: ReorderableListView(
children: colors
.map((color) => Box(
key: ValueKey(color),
color: color,
))
.toList(),
onReorder: (int oldIndex, int newIndex) {
if (newIndex > oldIndex) newIndex--;
setState(() {
final color = colors.removeAt(oldIndex);
colors.insert(newIndex, color);
});
},
),
);
}
}
class Box extends StatefulWidget {
final Color color;
const Box({Key? key, required this.color}) : super(key: key);
#override
_BoxState createState() => _BoxState();
}
class _BoxState extends State<Box> {
bool _big = false;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _big = true),
onTapCancel: () => setState(() => _big = false),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
height: _big ? 100 : 50,
margin: EdgeInsets.all(4.0),
color: widget.color,
),
);
}
}

How to change the page using Navigator.push so that the body and header animation is different, while the footer remains unchanged

I am trying to go to the next page using Navigator.push and at the same time change only the body on the page. I only got this when, for example, I wrap the index of page 2 in materialApp. But when I decided to make the animation (it smoothly pushes the old page to the left and pushes the new page to the right), it turned out that she pushed the old page, but behind it was exactly the same motionless page, which was later blocked by the new one.
I understood this in such a way that the first deleted page was an index 2 page, which is wrapped in MaterialApp, and behind it is exactly the same fixed MaterialApp for the entire application. At the moment, I have no idea how to remove a fixed page. I gave a picture of how I am currently navigating in the application, it may not be perfect, but I do not know better, any help would be appreciated.
In many applications, I see such an animation that the header fades out smoothly and at the same time a new one appears. And the body at this moment is replaced with the old page with a smooth movement, I really like it and I want to do the same.
You can try to use nested Navigator inside your scaffold.
Page index 1,2 and 3 will be inside the root Navigator under material app. Page 2 will contain another Navigator to fit your purpose.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final GlobalKey<NavigatorState> outgoingKey = GlobalKey<NavigatorState>();
return MaterialApp(
title: 'Sample',
home: Scaffold(
body: PageView(
children: <Widget>[
Page1(),
Page2(navigatorKey: outgoingKey,),
Page3(),
],
pageSnapping: false,
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
),
bottomNavigationBar: /*SomeBottomNavigationBar()*/,
),
);
}
}
class Page2 extends StatelessWidget {
Page2({Key key, this.navigatorKey}) : super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
#override
Widget build(BuildContext context) {
return Container(
child: Column(children: [
Expanded(
child: Navigator(
key: navigatorKey, // you need to use this to pop i.e. navigatorKey.currentState.pop()
initialRoute: 'initialPageIndex2',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder _builder;
switch (settings.name) {
case 'nextPageForPageIndex2':
_builder = (context) => /*NextPageForPageIndex2()*/;
break;
case 'initialPageIndex2':
default:
_builder = (context) => /*InitialPageIndex2()*/;
}
return MaterialPageRoute(builder: _builder);
},
transitionDelegate: DefaultTransitionDelegate(),
);
)
],)
);
}
}
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Text('Page1'),
);
}
}
class Page3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Text('Page3'),
);
}
}
You have to use the PageView please check the code below, hope it will help you :
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basic AppBar'),
),
body: PageView(
children: <Widget>[
Page1(),
Page2()
],
pageSnapping: false,
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
),
);
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
Widget build(BuildContext context) {
return Container(
child: Center(child:Text("Page 1")),
color: Colors.red,
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
#override
Widget build(BuildContext context) {
return Container(
child: Center(child:Text("Page 2")),
color: Colors.blueAccent,
);
}
}