Flutter视口管理插件viewport的使用
Flutter视口管理插件viewport的使用
描述
该库旨在提供一种使用基于百分比的布局的方法,并且可以轻松地根据视口策略(允许大小)进行更改。
最常见的用例是当一个现有的Flutter项目在移动设备上运行良好时,应该能够原样作为Web和/或桌面应用程序。考虑以下代码:
//...
child: EmailInputField(width: MediaQuery.of(context).size.width * 0.9),
//...
它可能符合移动线框图的设计规范,但对于Web或桌面应用来说,这个尺寸可能会太大,因此一个简单的解决方案是限制Web和桌面渲染区域。问题是这不会改变MediaQuery.of(context).size
的值,所以很可能会在整个运行的应用程序中出现许多“渲染区域溢出”错误。这就是viewport库发挥作用的地方——它支持多种顶级配置来控制渲染区域策略,这样就可以很容易地说“将MediaQuery.of(context).size
的上限设置为这样的值……”:
class ParentWidget extends StatelessWidget {
// ...
[@override](/user/override)
Widget build(BuildContext context) {
return ViewPortWidget.upperBoundedMediaQuery(
maxWidth: 1024.0,
child: const ViewPortUserWidget(),
);
}
}
class ViewPortUserWidget extends StatelessWidget {
// ...
[@override](/user/override)
Widget build(BuildContext context) {
return Container(
width: ViewPort.of(context).width * 0.6,
child: ...,
);
}
}
这样一来,由于“渲染区域”被限制,错误也随之消失。
使用
Widget _buildRenderingAreaPolicyCarryingWidget(Widget child) {
if (kIsWeb) {
// 限制渲染区域的宽度
return ViewPortWidget.upperBoundedMediaQuery(
maxWidth: kBrowserAndDesktopMaxWidth,
child: child,
);
} else {
// 对于移动设备使用默认的MediaQuery
return ViewPortWidget.mediaQuery(child: child);
}
}
更多详情请参阅示例目录和测试目录以及库的文档。
示例代码
以下是使用viewport
库的完整示例代码:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:viewport/viewport.dart';
/// 此示例演示了`viewport`库的动态使用,对于更简单的用例(如限制浏览器渲染区域并忘记它),可以使用非默认的ViewPortWidget构造函数,例如[ViewPortWidget.upperBoundedMediaQuery]
void main() => runApp(const ViewPortApp());
// 用于限制浏览器渲染区域的常量
const double kBrowserMaxWidth = 1024.0;
// 未渲染区域的背景颜色
const Color kBackgroundColor = Colors.black;
class ViewPortApp extends StatelessWidget {
const ViewPortApp({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) => BoundedArea(
width: kIsWeb ? kBrowserMaxWidth : null,
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'viewport demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const BoundedArea(
// TODO: 移除此行以取消浏览器的渲染区域宽度限制
width: kBrowserMaxWidth,
child: DynamicViewportWidget(),
),
),
);
}
class BoundedArea extends StatelessWidget {
final double? width;
final double? height;
final Widget child;
const BoundedArea({
required this.child,
this.width,
this.height,
Key? key,
}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) =>
width != null || height != null ? _buildBoundedAreaTree() : child;
Widget _buildBoundedAreaTree() => Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(color: kBackgroundColor),
Center(
child: Container(
width: width,
height: height,
child: child,
),
)
],
);
[@override](/user/override)
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DoubleProperty('width', width))
..add(DoubleProperty('height', height));
}
}
class DynamicViewportWidget extends StatefulWidget {
const DynamicViewportWidget({Key? key}) : super(key: key);
[@override](/user/override)
_DynamicViewportWidgetState createState() => _DynamicViewportWidgetState();
}
class _DynamicViewportWidgetState extends State<DynamicViewportWidget> {
late WidgetViewPortFactory _factory;
void _updateFactory(WidgetViewPortFactory factory) =>
setState(() => _factory = factory);
[@override](/user/override)
void initState() {
super.initState();
_factory = kIsWeb
// 使用受限的渲染区域用于浏览器
? const UpperBoundedViewPortFactory(
MediaQueryViewPortFactory(),
maxWidth: kBrowserMaxWidth,
)
// 对于移动设备使用默认的MediaQuery
: const MediaQueryViewPortFactory();
}
[@override](/user/override)
Widget build(BuildContext context) => ViewPortWidget(
factory: _factory,
child: LoginFormPage(onFactoryChangedCallback: _updateFactory),
);
}
typedef OnFactoryUpdatedCallback = void Function(WidgetViewPortFactory);
typedef ViewPortChangeIntent = void Function(BuildContext);
class LoginFormPage extends StatelessWidget {
final OnFactoryUpdatedCallback onFactoryChangedCallback;
const LoginFormPage({
required this.onFactoryChangedCallback,
Key? key,
}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('ViewPort Demo'),
actions: <Widget>[
PopupMenuButton<ViewPortChangeIntent>(
onSelected: (fn) => fn(context),
itemBuilder: _buildPopupMenuItems,
),
],
),
body: Center(
child: Container(
width: ViewPort.of(context).width,
height: ViewPort.of(context).height,
color: Colors.red,
alignment: Alignment.center,
child: Text('width: ${ViewPort.of(context).width}, '
'height: ${ViewPort.of(context).height}'),
),
),
);
List<PopupMenuEntry<ViewPortChangeIntent>> _buildPopupMenuItems(
BuildContext context,
) =>
<PopupMenuEntry<ViewPortChangeIntent>>[
PopupMenuItem<ViewPortChangeIntent>(
value: _setFixedViewPort,
child: const Text('固定'),
),
PopupMenuItem<ViewPortChangeIntent>(
value: _setUpperBoundedMediaQueryViewPort,
child: const Text('上限限制的MediaQueryData'),
),
PopupMenuItem<ViewPortChangeIntent>(
value: _setLowerBoundedMediaQueryViewPort,
child: const Text('下限限制的MediaQueryData'),
),
PopupMenuItem<ViewPortChangeIntent>(
value: _setCoercedMediaQueryViewPort,
child: const Text('强制约束的MediaQueryData'),
),
];
Future<void> _setFixedViewPort(BuildContext context) async {
final TextEditingController heightController = TextEditingController();
final TextEditingController widthController = TextEditingController();
final factory = await showDialog<WidgetViewPortFactory>(
context: context,
builder: (BuildContext ctx) => SimpleDialog(
title: const Text('设置固定边界'),
children: [
TextField(
controller: heightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '高度'),
),
TextField(
controller: widthController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '宽度'),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: heightController,
builder: (_, TextEditingValue heightValue, __) =>
ValueListenableBuilder<TextEditingValue>(
valueListenable: widthController,
builder: (_, TextEditingValue widthValue, __) => MaterialButton(
onPressed: heightValue.text.isEmpty || widthValue.text.isEmpty
? null
: () => Navigator.of(ctx).pop(FixedViewPortFactory(
width: double.parse(widthValue.text),
height: double.parse(heightValue.text))),
child: const Text('设置固定视口'),
),
),
),
],
),
);
if (factory != null) onFactoryChangedCallback(factory);
}
Future<void> _setUpperBoundedMediaQueryViewPort(BuildContext context) async {
final TextEditingController heightController = TextEditingController();
final TextEditingController widthController = TextEditingController();
final factory = await showDialog<WidgetViewPortFactory>(
context: context,
builder: (BuildContext ctx) => SimpleDialog(
title: const Text('设置上限'),
children: [
TextField(
controller: heightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '高度'),
),
TextField(
controller: widthController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '宽度'),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: heightController,
builder: (_, TextEditingValue heightValue, __) =>
ValueListenableBuilder<TextEditingValue>(
valueListenable: widthController,
builder: (_, TextEditingValue widthValue, __) => MaterialButton(
onPressed: heightValue.text.isEmpty || widthValue.text.isEmpty
? null
: () => Navigator.of(ctx).pop(UpperBoundedViewPortFactory(
const MediaQueryViewPortFactory(),
maxWidth: double.parse(widthValue.text),
maxHeight: double.parse(heightValue.text))),
child: const Text('设置上限限制的视口'),
),
),
),
],
),
);
if (factory != null) onFactoryChangedCallback(factory);
}
Future<void> _setLowerBoundedMediaQueryViewPort(BuildContext context) async {
final TextEditingController heightController = TextEditingController();
final TextEditingController widthController = TextEditingController();
final factory = await showDialog<WidgetViewPortFactory>(
context: context,
builder: (BuildContext ctx) => SimpleDialog(
title: const Text('设置下限'),
children: [
TextField(
controller: heightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '高度'),
),
TextField(
controller: widthController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '宽度'),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: heightController,
builder: (_, TextEditingValue heightValue, __) =>
ValueListenableBuilder<TextEditingValue>(
valueListenable: widthController,
builder: (_, TextEditingValue widthValue, __) => MaterialButton(
onPressed: heightValue.text.isEmpty || widthValue.text.isEmpty
? null
: () => Navigator.of(ctx).pop(LowerBoundedViewPortFactory(
const MediaQueryViewPortFactory(),
minWidth: double.parse(widthValue.text),
minHeight: double.parse(heightValue.text))),
child: const Text('设置下限限制的视口'),
),
),
),
],
),
);
if (factory != null) onFactoryChangedCallback(factory);
}
Future<void> _setCoercedMediaQueryViewPort(BuildContext context) async {
final TextEditingController minHeightController = TextEditingController();
final TextEditingController minWidthController = TextEditingController();
final TextEditingController maxHeightController = TextEditingController();
final TextEditingController maxWidthController = TextEditingController();
final factory = await showDialog<WidgetViewPortFactory>(
context: context,
builder: (BuildContext ctx) => SimpleDialog(
title: const Text('设置强制约束的边界'),
children: [
TextField(
controller: minHeightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '最小高度'),
),
TextField(
controller: minWidthController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '最小宽度'),
),
TextField(
controller: maxHeightController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '最大高度'),
),
TextField(
controller: maxWidthController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(hintText: '最大宽度'),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: minHeightController,
builder: (_, TextEditingValue minHeightValue, __) =>
ValueListenableBuilder<TextEditingValue>(
valueListenable: minWidthController,
builder: (_, TextEditingValue minWidthValue, __) =>
ValueListenableBuilder<TextEditingValue>(
valueListenable: maxHeightController,
builder: (_, TextEditingValue maxHeightValue, __) =>
ValueListenableBuilder<TextEditingValue>(
valueListenable: maxWidthController,
builder: (_, TextEditingValue maxWidthValue, __) => MaterialButton(
onPressed: minHeightController.text.isEmpty ||
minWidthValue.text.isEmpty ||
maxHeightController.text.isEmpty ||
maxWidthValue.text.isEmpty
? null
: () => Navigator.of(ctx).pop(CoercedViewPortFactory(
const MediaQueryViewPortFactory(),
minWidth: double.parse(minWidthValue.text),
minHeight: double.parse(minHeightValue.text),
maxWidth: double.parse(maxWidthValue.text),
maxHeight: double.parse(maxHeightValue.text),
)),
child: const Text('设置强制约束的视口'),
),
),
),
),
),
],
),
);
if (factory != null) onFactoryChangedCallback(factory);
}
[@override](/user/override)
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ObjectFlagProperty<OnFactoryUpdatedCallback>.has(
'onFactoryChangedCallback', onFactoryChangedCallback));
}
}
更多关于Flutter视口管理插件viewport的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter视口管理插件viewport的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter中使用viewport
进行视口管理的代码案例。Viewport
是Flutter中一个非常有用的组件,特别是在处理大量数据并需要在有限的屏幕上展示时。通常,Viewport
与ListView
等滚动组件结合使用。
示例代码
在这个示例中,我们将展示如何使用Viewport
来管理一个包含大量项目的列表。为了简单起见,我们将创建一个包含1000个文本项的列表,并使用Viewport
来优化滚动性能。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Viewport Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ViewportExample(),
);
}
}
class ViewportExample extends StatelessWidget {
final List<String> items = List.generate(1000, (index) => "Item $index");
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Viewport Example'),
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// 设置视口的大小
final double viewportHeight = constraints.maxHeight;
final double itemHeight = 50.0; // 每个项目的高度
final int itemCountPerViewport = (viewportHeight / itemHeight).ceil();
return Viewport(
// 设置视口的宽度和高度
width: constraints.maxWidth,
height: viewportHeight,
// 设置子项的交叉轴对齐方式
crossAxisAlignment: CrossAxisAlignment.start,
// 设置视口的sliver
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text(items[index]),
);
},
childCount: items.length,
),
),
],
// 设置缓存范围,这里设置为可见项的前后各5项
cacheExtent: itemHeight * (itemCountPerViewport + 10),
);
},
),
);
}
}
代码解释
-
生成大量数据:在
ViewportExample
类中,我们生成了一个包含1000个字符串的列表。 -
使用
LayoutBuilder
:我们使用LayoutBuilder
来获取父容器的约束,特别是其最大高度,以便我们可以根据屏幕高度来设置视口的高度。 -
设置
Viewport
:width
和height
属性设置了视口的宽度和高度。crossAxisAlignment
属性设置了子项在交叉轴上的对齐方式。slivers
属性包含了一个SliverList
,它使用SliverChildBuilderDelegate
来按需构建列表项。
-
缓存范围:
cacheExtent
属性设置了视口的缓存范围。在这个例子中,我们设置为可见项的前后各5项(通过itemHeight * (itemCountPerViewport + 10)
计算得出),以提高滚动性能。
这个示例展示了如何在Flutter中使用Viewport
来优化大量数据的滚动性能。根据实际需求,你可以调整itemHeight
、cacheExtent
等参数来优化性能。