目录:
- 1、列表布局
- 1.1、基础列表
- 1.2、水平滑动的列表
- 1.3、网格列表
- 1.3、不同列表项的列表
- 1.4、包含间隔的列表
- 1.6、长列表
- 2、滚动
- 2.1、浮动的顶栏
- 2.2、平衡错位滚动
1、列表布局
1.1、基础列表
import 'package:flutter/material.dart';void main() => runApp(const MyApp());class MyApp extends StatelessWidget {const MyApp({super.key}); Widget build(BuildContext context) {const title = 'Basic List';return MaterialApp(title: title,home: Scaffold(appBar: AppBar(title: const Text(title)),body: ListView(children: const <Widget>[ListTile(leading: Icon(Icons.map), title: Text('Map')),ListTile(leading: Icon(Icons.photo_album), title: Text('Album')),ListTile(leading: Icon(Icons.phone), title: Text('Phone')),],),),);}
}
1.2、水平滑动的列表
import 'package:flutter/material.dart';void main() => runApp(const MyApp());class MyApp extends StatelessWidget {const MyApp({super.key}); Widget build(BuildContext context) {const title = 'Horizontal List';return MaterialApp(title: title,home: Scaffold(appBar: AppBar(title: const Text(title)),body: Container(margin: const EdgeInsets.symmetric(vertical: 20),height: 200,child: ListView(// This next line does the trick.scrollDirection: Axis.horizontal,children: <Widget>[Container(width: 160, color: Colors.red),Container(width: 160, color: Colors.blue),Container(width: 160, color: Colors.green),Container(width: 160, color: Colors.yellow),Container(width: 160, color: Colors.orange),],),),),);}
}
1.3、网格列表
import 'package:flutter/material.dart';void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key}); Widget build(BuildContext context) {const title = 'Grid List';return MaterialApp(title: title,home: Scaffold(appBar: AppBar(title: const Text(title)),body: GridView.count(// Create a grid with 2 columns.// If you change the scrollDirection to horizontal,// this produces 2 rows.crossAxisCount: 2,// Generate 100 widgets that display their index in the list.children: List.generate(100, (index) {return Center(child: Text('Item $index',style: TextTheme.of(context).headlineSmall,),);}),),),);}
}
1.3、不同列表项的列表
import 'package:flutter/material.dart';void main() {runApp(MyApp(items: List<ListItem>.generate(1000,(i) =>i % 6 == 0? HeadingItem('Heading $i'): MessageItem('Sender $i', 'Message body $i'),),),);
}class MyApp extends StatelessWidget {final List<ListItem> items;const MyApp({super.key, required this.items}); Widget build(BuildContext context) {const title = 'Mixed List';return MaterialApp(title: title,home: Scaffold(appBar: AppBar(title: const Text(title)),body: ListView.builder(// Let the ListView know how many items it needs to build.itemCount: items.length,// Provide a builder function. This is where the magic happens.// Convert each item into a widget based on the type of item it is.itemBuilder: (context, index) {final item = items[index];return ListTile(title: item.buildTitle(context),subtitle: item.buildSubtitle(context),);},),),);}
}/// The base class for the different types of items the list can contain.
abstract class ListItem {/// The title line to show in a list item.Widget buildTitle(BuildContext context);/// The subtitle line, if any, to show in a list item.Widget buildSubtitle(BuildContext context);
}/// A ListItem that contains data to display a heading.
class HeadingItem implements ListItem {final String heading;HeadingItem(this.heading); Widget buildTitle(BuildContext context) {return Text(heading, style: Theme.of(context).textTheme.headlineSmall);} Widget buildSubtitle(BuildContext context) => const SizedBox.shrink();
}/// A ListItem that contains data to display a message.
class MessageItem implements ListItem {final String sender;final String body;MessageItem(this.sender, this.body); Widget buildTitle(BuildContext context) => Text(sender); Widget buildSubtitle(BuildContext context) => Text(body);
}
1.4、包含间隔的列表
import 'package:flutter/material.dart';void main() => runApp(const SpacedItemsList());class SpacedItemsList extends StatelessWidget {const SpacedItemsList({super.key}); Widget build(BuildContext context) {const items = 4;return MaterialApp(title: 'Flutter Demo',debugShowCheckedModeBanner: false,theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),cardTheme: CardTheme(color: Colors.blue.shade50),),home: Scaffold(body: LayoutBuilder(builder: (context, constraints) {return SingleChildScrollView(child: ConstrainedBox(constraints: BoxConstraints(minHeight: constraints.maxHeight),child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween,crossAxisAlignment: CrossAxisAlignment.stretch,children: List.generate(items,(index) => ItemWidget(text: 'Item $index'),),),),);},),),);}
}class ItemWidget extends StatelessWidget {const ItemWidget({super.key, required this.text});final String text; Widget build(BuildContext context) {return Card(child: SizedBox(height: 100, child: Center(child: Text(text))));}
}
1.6、长列表
import 'package:flutter/material.dart';void main() {runApp(MyApp(items: List<String>.generate(10000, (i) => 'Item $i'),),);
}class MyApp extends StatelessWidget {final List<String> items;const MyApp({super.key, required this.items}); Widget build(BuildContext context) {const title = 'Long List';return MaterialApp(title: title,home: Scaffold(appBar: AppBar(title: const Text(title)),body: ListView.builder(itemCount: items.length,prototypeItem: ListTile(title: Text(items.first)),itemBuilder: (context, index) {return ListTile(title: Text(items[index]));},),),);}
}
2、滚动
2.1、浮动的顶栏
import 'package:flutter/material.dart';void main() => runApp(const MyApp());class MyApp extends StatelessWidget {const MyApp({super.key}); Widget build(BuildContext context) {const title = 'Floating App Bar';return MaterialApp(title: title,home: Scaffold(// No appbar provided to the Scaffold, only a body with a// CustomScrollView.body: CustomScrollView(slivers: [// Add the app bar to the CustomScrollView.const SliverAppBar(// Provide a standard title.title: Text(title),// Allows the user to reveal the app bar if they begin scrolling// back up the list of items.floating: true,// Display a placeholder widget to visualize the shrinking size.flexibleSpace: Placeholder(),// Make the initial height of the SliverAppBar larger than normal.expandedHeight: 200,),// Next, create a SliverListSliverList(// Use a delegate to build items as they're scrolled on screen.delegate: SliverChildBuilderDelegate(// The builder function returns a ListTile with a title that// displays the index of the current item.(context, index) => ListTile(title: Text('Item #$index')),// Builds 1000 ListTileschildCount: 1000,),),],),),);}
}
2.2、平衡错位滚动
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';const Color darkBlue = Color.fromARGB(255, 18, 32, 47);void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key}); Widget build(BuildContext context) {return MaterialApp(theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),debugShowCheckedModeBanner: false,home: const Scaffold(body: Center(child: ExampleParallax())),);}
}class ExampleParallax extends StatelessWidget {const ExampleParallax({super.key}); Widget build(BuildContext context) {return SingleChildScrollView(child: Column(children: [for (final location in locations)LocationListItem(imageUrl: location.imageUrl,name: location.name,country: location.place,),],),);}
}class LocationListItem extends StatelessWidget {LocationListItem({super.key,required this.imageUrl,required this.name,required this.country,});final String imageUrl;final String name;final String country;final GlobalKey _backgroundImageKey = GlobalKey(); Widget build(BuildContext context) {return Padding(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),child: AspectRatio(aspectRatio: 16 / 9,child: ClipRRect(borderRadius: BorderRadius.circular(16),child: Stack(children: [_buildParallaxBackground(context),_buildGradient(),_buildTitleAndSubtitle(),],),),),);}Widget _buildParallaxBackground(BuildContext context) {return Flow(delegate: ParallaxFlowDelegate(scrollable: Scrollable.of(context),listItemContext: context,backgroundImageKey: _backgroundImageKey,),children: [Image.network(imageUrl, key: _backgroundImageKey, fit: BoxFit.cover),],);}Widget _buildGradient() {return Positioned.fill(child: DecoratedBox(decoration: BoxDecoration(gradient: LinearGradient(colors: [Colors.transparent, Colors.black.withValues(alpha: 0.7)],begin: Alignment.topCenter,end: Alignment.bottomCenter,stops: const [0.6, 0.95],),),),);}Widget _buildTitleAndSubtitle() {return Positioned(left: 20,bottom: 20,child: Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment: CrossAxisAlignment.start,children: [Text(name,style: const TextStyle(color: Colors.white,fontSize: 20,fontWeight: FontWeight.bold,),),Text(country,style: const TextStyle(color: Colors.white, fontSize: 14),),],),);}
}class ParallaxFlowDelegate extends FlowDelegate {ParallaxFlowDelegate({required this.scrollable,required this.listItemContext,required this.backgroundImageKey,}) : super(repaint: scrollable.position);final ScrollableState scrollable;final BuildContext listItemContext;final GlobalKey backgroundImageKey; BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {return BoxConstraints.tightFor(width: constraints.maxWidth);}void paintChildren(FlowPaintingContext context) {// Calculate the position of this list item within the viewport.final scrollableBox = scrollable.context.findRenderObject() as RenderBox;final listItemBox = listItemContext.findRenderObject() as RenderBox;final listItemOffset = listItemBox.localToGlobal(listItemBox.size.centerLeft(Offset.zero),ancestor: scrollableBox,);// Determine the percent position of this list item within the// scrollable area.final viewportDimension = scrollable.position.viewportDimension;final scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0.0,1.0,);// Calculate the vertical alignment of the background// based on the scroll percent.final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);// Convert the background alignment into a pixel offset for// painting purposes.final backgroundSize =(backgroundImageKey.currentContext!.findRenderObject() as RenderBox).size;final listItemSize = context.size;final childRect = verticalAlignment.inscribe(backgroundSize,Offset.zero & listItemSize,);// Paint the background.context.paintChild(0,transform:Transform.translate(offset: Offset(0.0, childRect.top)).transform,);} bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {return scrollable != oldDelegate.scrollable ||listItemContext != oldDelegate.listItemContext ||backgroundImageKey != oldDelegate.backgroundImageKey;}}class Parallax extends SingleChildRenderObjectWidget {const Parallax({super.key, required Widget background}): super(child: background); RenderObject createRenderObject(BuildContext context) {return RenderParallax(scrollable: Scrollable.of(context));}void updateRenderObject(BuildContext context,covariant RenderParallax renderObject,) {renderObject.scrollable = Scrollable.of(context);}
}class ParallaxParentData extends ContainerBoxParentData<RenderBox> {}class RenderParallax extends RenderBoxwith RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin {RenderParallax({required ScrollableState scrollable}): _scrollable = scrollable;ScrollableState _scrollable;ScrollableState get scrollable => _scrollable;set scrollable(ScrollableState value) {if (value != _scrollable) {if (attached) {_scrollable.position.removeListener(markNeedsLayout);}_scrollable = value;if (attached) {_scrollable.position.addListener(markNeedsLayout);}}}void attach(covariant PipelineOwner owner) {super.attach(owner);_scrollable.position.addListener(markNeedsLayout);}void detach() {_scrollable.position.removeListener(markNeedsLayout);super.detach();}void setupParentData(covariant RenderObject child) {if (child.parentData is! ParallaxParentData) {child.parentData = ParallaxParentData();}}void performLayout() {size = constraints.biggest;// Force the background to take up all available width// and then scale its height based on the image's aspect ratio.final background = child!;final backgroundImageConstraints = BoxConstraints.tightFor(width: size.width,);background.layout(backgroundImageConstraints, parentUsesSize: true);// Set the background's local offset, which is zero.(background.parentData as ParallaxParentData).offset = Offset.zero;}void paint(PaintingContext context, Offset offset) {// Get the size of the scrollable area.final viewportDimension = scrollable.position.viewportDimension;// Calculate the global position of this list item.final scrollableBox = scrollable.context.findRenderObject() as RenderBox;final backgroundOffset = localToGlobal(size.centerLeft(Offset.zero),ancestor: scrollableBox,);// Determine the percent position of this list item within the// scrollable area.final scrollFraction = (backgroundOffset.dy / viewportDimension).clamp(0.0,1.0,);// Calculate the vertical alignment of the background// based on the scroll percent.final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);// Convert the background alignment into a pixel offset for// painting purposes.final background = child!;final backgroundSize = background.size;final listItemSize = size;final childRect = verticalAlignment.inscribe(backgroundSize,Offset.zero & listItemSize,);// Paint the background.context.paintChild(background,(background.parentData as ParallaxParentData).offset +offset +Offset(0.0, childRect.top),);}
}class Location {const Location({required this.name,required this.place,required this.imageUrl,});final String name;final String place;final String imageUrl;
}const urlPrefix ='https://docs.flutter.dev/cookbook/img-files/effects/parallax';
const locations = [Location(name: 'Mount Rushmore',place: 'U.S.A',imageUrl: '$urlPrefix/01-mount-rushmore.jpg',),Location(name: 'Gardens By The Bay',place: 'Singapore',imageUrl: '$urlPrefix/02-singapore.jpg',),Location(name: 'Machu Picchu',place: 'Peru',imageUrl: '$urlPrefix/03-machu-picchu.jpg',),Location(name: 'Vitznau',place: 'Switzerland',imageUrl: '$urlPrefix/04-vitznau.jpg',),Location(name: 'Bali',place: 'Indonesia',imageUrl: '$urlPrefix/05-bali.jpg',),Location(name: 'Mexico City',place: 'Mexico',imageUrl: '$urlPrefix/06-mexico-city.jpg',),Location(name: 'Cairo', place: 'Egypt', imageUrl: '$urlPrefix/07-cairo.jpg'),
];