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

1 回复

更多关于Flutter视口管理插件viewport的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter中使用viewport进行视口管理的代码案例。Viewport是Flutter中一个非常有用的组件,特别是在处理大量数据并需要在有限的屏幕上展示时。通常,ViewportListView等滚动组件结合使用。

示例代码

在这个示例中,我们将展示如何使用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),
          );
        },
      ),
    );
  }
}

代码解释

  1. 生成大量数据:在ViewportExample类中,我们生成了一个包含1000个字符串的列表。

  2. 使用LayoutBuilder:我们使用LayoutBuilder来获取父容器的约束,特别是其最大高度,以便我们可以根据屏幕高度来设置视口的高度。

  3. 设置Viewport

    • widthheight属性设置了视口的宽度和高度。
    • crossAxisAlignment属性设置了子项在交叉轴上的对齐方式。
    • slivers属性包含了一个SliverList,它使用SliverChildBuilderDelegate来按需构建列表项。
  4. 缓存范围cacheExtent属性设置了视口的缓存范围。在这个例子中,我们设置为可见项的前后各5项(通过itemHeight * (itemCountPerViewport + 10)计算得出),以提高滚动性能。

这个示例展示了如何在Flutter中使用Viewport来优化大量数据的滚动性能。根据实际需求,你可以调整itemHeightcacheExtent等参数来优化性能。

回到顶部