Flutter墙面布局插件flutter_wall_layout的使用

Flutter墙面布局插件flutter_wall_layout的使用

本库提供了一个独特的组件WallLayout,可以将不同形状的子部件布局为垂直或水平列表。

该库主要有两个类:WallLayout 和 Stone。

  • 墙由一组石头组成,这些石头按照特定的方式排列。
  • 一块石头是一个定义其宽度和高度的组件。

墙布局组件

该组件受到ListView的启发,你可以选择滚动方向和方式。此外,你还可以定义墙有多少行或列,以创建响应式设计。

WallLayout(
  stones: _buildStonesList(),
  layersCount: 3,
  scrollDirection: Axis.vertical,
  reverse: false,
);

墙布局组件属性

参数名称 描述 默认值
stones 石头列表,表示墙布局的子部件。 -
layersCount 定义墙的层数。必须大于等于2。当方向为Axis.vertical时,它定义墙有多少列。当方向为Axis.horizontal时,它定义墙有多少行。 -
wallBuilder 定义如何构建墙:每个石头在墙内的位置。允许使用比提供的更少的石头,并且可以创建新的石头(详情见示例)。 WallBuilder.standard()
stonePadding 石头之间的间距。 16.0
scrollController 同ListView.scrollController,控制滚动视图滚动到的位置。 -
physics 同ListView.physics,定义滚动视图如何响应用户输入。 -
restorationId 同ListView.restorationId,用于保存和恢复滚动视图的滚动偏移量。 -
dragStartBehavior 同ListView.dragStartBehavior,确定拖动开始行为的处理方式。 DragStartBehavior.start
clipBehavior 同ListView.clipBehavior,定义如何裁剪小部件的内容。 Clip.hardEdge
primary 同ListView.primary,是否此为与父PrimaryScrollController关联的主要滚动视图。 false
scrollDirection 同ListView.scrollDirection,定义滚动视图沿哪个轴滚动。 Axis.vertical
reverse 同ListView.reverse属性,定义滚动视图是否从阅读方向反向滚动。 false

石头组件

一个石头组件是一个定义石头大小(宽度和高度)的小部件代理。通过其id属性优化渲染对象。

Stone(
    id: 7,
    width: 2,
    height: 2,
    child: Container(
      color: Colors.red,
      child: Center(child: Text("2x2")),
    ),
);

石头组件属性

参数名称 描述 默认值
width 石头的宽度(相对于墙的分割数)。必须大于等于1。 -
height 石头的高度(相对于墙的分割数)。必须大于等于1。 -
id 代理布局标识符:必须唯一。 -
child 渲染的小部件,其尺寸将受石头宽度和高度的约束。 -

完整示例代码

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_wall_layout/flutter_wall_layout.dart';
import 'package:flutter_wall_layout/wall_builder.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Wall Layout Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        backgroundColor: Color(0xFFF5F5F5),
      ),
      home: MyHomePage(title: 'Wall Layout Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  [@override](/user/override)
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late final AnimationController _controller;
  late bool _reversed;
  late Axis _direction;
  late int _nbLayers;
  late bool _wrapedOptions;
  bool _random = false;
  late List<Stone> _stones;

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller =
        AnimationController(duration: Duration(milliseconds: 500), vsync: this);
    _reversed = false;
    _direction = Axis.vertical;
    _nbLayers = 3;
    _controller.forward(from: 0);
    _wrapedOptions = true;
    _stones = _buildStonesList();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    final backgroundColor = Theme.of(context).backgroundColor;
    return Scaffold(
      backgroundColor: backgroundColor,
      appBar: AppBar(
        title: Text(this.widget.title),
        actions: [
          IconButton(
            onPressed: _onRandomClicked,
            icon: Icon(
              _random ? Icons.sync : Icons.sync_disabled,
            ),
            tooltip: "Random stone sizes + custom WallHandler",
          )
        ],
      ),
      body: buildWallLayout(),
      floatingActionButton: _buildOptions(context),
    );
  }

  void _onRandomClicked() {
    setState(() {
      _random = !_random;
      if (_random) {
        _stones = _buildRandomStonesList(_nbLayers);
      } else {
        _stones = _buildStonesList();
      }
    });
  }

  Widget _buildOptions(BuildContext context) {
    return AnimatedSize(
      duration: Duration(milliseconds: 200),
      reverseDuration: Duration(milliseconds: 200),
      alignment: Alignment.bottomRight,
      vsync: this,
      child: Container(
        margin: EdgeInsets.only(left: 32),
        decoration: BoxDecoration(
          color: Theme.of(context).backgroundColor,
          boxShadow: [
            BoxShadow(color: Colors.black26, blurRadius: 6.0),
          ],
          borderRadius: BorderRadius.circular(30.0),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            if (!_wrapedOptions)
              Padding(
                padding: EdgeInsets.symmetric(vertical: 20.0),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    __buildDivisionsOption(),
                    __buildDirectionOption(),
                    __buildReverseOption(),
                  ],
                ),
              ),
            FloatingActionButton(
              elevation: 0.0,
              highlightElevation: 0.0,
              onPressed: () => setState(() => _wrapedOptions = !_wrapedOptions),
              child: Icon(Icons.build),
            ),
          ],
        ),
      ),
    );
  }

  Widget __buildDivisionsOption() {
    return _buildOption(
      "Layers",
      CupertinoSegmentedControl<int>(
        groupValue: _nbLayers,
        children: {2: Text("2"), 3: Text("3"), 4: Text("4")},
        onValueChanged: (value) => setState(() {
          _controller.forward(from: 0.0);
          _nbLayers = value;
          if (_random) {
            _stones = _buildRandomStonesList(_nbLayers);
          }
        }),
      ),
    );
  }

  Widget __buildReverseOption() {
    return _buildOption(
      "Reverse",
      CupertinoSegmentedControl<bool>(
        groupValue: _reversed,
        children: {false: Text("no"), true: Text("yes")},
        onValueChanged: (value) => setState(() {
          _controller.forward(from: 0.0);
          _reversed = value;
        }),
      ),
    );
  }

  Widget __buildDirectionOption() {
    return _buildOption(
      "Direction",
      CupertinoSegmentedControl<Axis>(
        groupValue: _direction,
        children: {
          Axis.vertical: Text("vertical"),
          Axis.horizontal: Text("horizontal")
        },
        onValueChanged: (value) => setState(() {
          _controller.forward(from: 0.0);
          _direction = value;
        }),
      ),
    );
  }

  Widget _buildOption(String text, Widget child) {
    return Padding(
      padding: const EdgeInsets.only(top: 4.0, left: 8.0, bottom: 4.0),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          Flexible(
            child: Text(text),
            flex: 1,
          ),
          Expanded(
            child: child,
            flex: 2,
          ),
        ],
      ),
    );
  }

  Widget buildWallLayout() {
    return WallLayout(
      wallBuilder: _random
          ? FillingHolesWallBuildHandler(
              context: context,
              childBuilder: (_) => DecoratedBox(
                decoration: BoxDecoration(
                    color: Color(0xffececee),
                    borderRadius: BorderRadius.circular(12)),
              ),
            )
          : WallBuilder.standard(),
      scrollDirection: _direction,
      stones: _stones,
      reverse: _reversed,
      layersCount: _nbLayers,
    );
  }

  List<Stone> _buildRandomStonesList(int maxLayer) {
    Random r = Random();
    final next = () => r.nextInt(maxLayer) + 1;
    final colors = [
      Colors.red,
      Colors.greenAccent,
      Colors.lightBlue,
      Colors.purple,
      Colors.yellow,
      Colors.cyanAccent,
      Colors.orange,
      Colors.green,
      Colors.pink,
      Colors.blueAccent,
      Colors.amber,
      Colors.teal,
      Colors.lightGreenAccent,
      Colors.deepOrange,
      Colors.deepPurpleAccent,
      Colors.lightBlueAccent,
      Colors.limeAccent,
    ];
    return colors.map((color) {
      int width = next();
      int height = next();
      return Stone(
        id: colors.indexOf(color),
        width: width,
        height: height,
        child: __buildStoneChild(
          background: color,
          text: "${width}x$height",
          surface: (width * height).toDouble(),
        ),
      );
    }).toList();
  }

  List<Stone> _buildStonesList() {
    final data = [
      {"color": Colors.red, "width": 2, "height": 2},
      {"color": Colors.greenAccent, "width": 1, "height": 1},
      {"color": Colors.lightBlue, "width": 1, "height": 2},
      {"color": Colors.purple, "width": 2, "height": 1},
      {"color": Colors.yellow, "width": 1, "height": 1},
      {"color": Colors.cyanAccent, "width": 1, "height": 1},
      {"color": Colors.orange, "width": 2, "height": 2},
      {"color": Colors.green, "width": 1, "height": 1},
      {"color": Colors.pink, "width": 2, "height": 1},
      {"color": Colors.blueAccent, "width": 1, "height": 1},
      {"color": Colors.amber, "width": 1, "height": 2},
      {"color": Colors.teal, "width": 2, "height": 1},
      {"color": Colors.lightGreenAccent, "width": 1, "height": 1},
      {"color": Colors.deepOrange, "width": 1, "height": 1},
      {"color": Colors.deepPurpleAccent, "width": 2, "height": 2},
      {"color": Colors.lightBlueAccent, "width": 1, "height": 1},
      {"color": Colors.limeAccent, "width": 1, "height": 1},
    ];
    return data.map((d) {
      int width = d["width"] as int;
      int height = d["height"] as int;
      return Stone(
        id: data.indexOf(d),
        width: width,
        height: height,
        child: __buildStoneChild(
          background: d["color"] as Color,
          text: "${width}x$height",
          surface: (width * height).toDouble(),
        ),
      );
    }).toList();
  }

  Widget __buildStoneChild(
      {required Color background,
      required String text,
      required double surface}) {
    return ScaleTransition(
      scale: CurveTween(curve: Interval(0.0, min(1.0, 0.25 + surface / 6.0)))
          .animate(_controller),
      child: Container(
        alignment: Alignment.center,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [
              background,
              Color.alphaBlend(background.withOpacity(0.6), Colors.black)
            ],
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
          ),
          borderRadius: BorderRadius.circular(12),
        ),
        child:
            Text(text, style: TextStyle(color: Colors.white, fontSize: 32.0)),
      ),
    );
  }
}

// 填充空位用1x1大小的石头
class FillingHolesWallBuildHandler extends WallBuilder {
  final WallBuilder proxy = WallBuilder.standard();
  final BuildContext context;
  final WidgetBuilder childBuilder;

  FillingHolesWallBuildHandler(
      {required this.childBuilder, required this.context})
      : super();

  WallBlueprint _buildBlueprint(List<Stone> stones) {
    return proxy.build(
        mainAxisSeparations: mainAxisSeparations,
        reverse: reverse,
        direction: direction,
        stones: stones);
  }

  void _findHoles(WallBlueprint blueprint, Function(int x, int y) onHoleFound) {
    List<Rect> bounds = blueprint.stonesPosition
        .map((key, value) => MapEntry(
            key,
            Rect.fromLTWH(value.x.toDouble(), value.y.toDouble(),
                key.width.toDouble(), key.height.toDouble())))
        .values
        .toList();
    for (int x = 0; x < blueprint.size.width; x++) {
      for (int y = 0; y < blueprint.size.height; y++) {
        Rect area = Rect.fromLTWH(x.toDouble(), y.toDouble(), 1.0, 1.0);
        bounds.firstWhere(
          (element) => area.overlaps(element),
          orElse: () {
            onHoleFound(x, y);
            return area;
          },
        );
        bounds.add(area);
      }
    }
  }

  [@override](/user/override)
  Map<Stone, StoneStartPosition> computeStonePositions(List<Stone> stones) {
    final blueprint = _buildBlueprint(stones);
    Map<Stone, StoneStartPosition> positions = blueprint.stonesPosition;
    int idStart = 10000;
    _findHoles(blueprint, (x, y) {
      final stone = Stone(
        height: 1,
        width: 1,
        id: idStart++,
        child: childBuilder.call(context),
      );
      positions[stone] = StoneStartPosition(x: x, y: y);
    });
    return positions;
  }
}

更多关于Flutter墙面布局插件flutter_wall_layout的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter墙面布局插件flutter_wall_layout的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用flutter_wall_layout插件的一个示例。flutter_wall_layout是一个用于创建墙面布局的Flutter插件,非常适合展示图片网格等场景。

首先,确保你已经在pubspec.yaml文件中添加了flutter_wall_layout依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_wall_layout: ^最新版本号  # 请替换为最新版本号

然后,运行flutter pub get来获取依赖。

接下来,在你的Dart文件中使用flutter_wall_layout。下面是一个完整的示例,展示如何使用WallLayout来创建一个图片网格布局:

import 'package:flutter/material.dart';
import 'package:flutter_wall_layout/flutter_wall_layout.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Wall Layout Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: WallLayoutDemo(),
    );
  }
}

class WallLayoutDemo extends StatelessWidget {
  final List<String> imageUrls = [
    'https://via.placeholder.com/150',
    'https://via.placeholder.com/300',
    'https://via.placeholder.com/450',
    'https://via.placeholder.com/600',
    'https://via.placeholder.com/750',
    'https://via.placeholder.com/900',
    // 添加更多图片URL
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Wall Layout Demo'),
      ),
      body: GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2, // 这里设置列数,可以根据需要调整
          crossAxisSpacing: 4.0,
          mainAxisSpacing: 4.0,
        ),
        itemCount: imageUrls.length,
        itemBuilder: (context, index) {
          return WallLayout(
            crossAxisSpacing: 8.0,
            mainAxisSpacing: 8.0,
            children: _generateWallChildren(imageUrls[index]),
          );
        },
      ),
    );
  }

  List<Widget> _generateWallChildren(String imageUrl) {
    // 这里简单起见,我们为每张图片生成三个子项,每个子项显示同一张图片
    // 你可以根据需要调整生成逻辑
    List<Widget> children = [];
    for (int i = 0; i < 3; i++) {
      children.add(Image.network(
        imageUrl,
        fit: BoxFit.cover,
      ));
    }
    return children;
  }
}

解释

  1. 依赖添加:在pubspec.yaml中添加flutter_wall_layout依赖。
  2. 导入包:在Dart文件中导入flutter_wall_layout包。
  3. 创建演示页面:创建一个包含图片URL列表的WallLayoutDemo页面。
  4. 使用GridView.builder:使用GridView.builder来创建一个网格视图,每个网格项使用WallLayout
  5. 生成子项_generateWallChildren函数为每张图片生成多个子项(在这个示例中为3个),你可以根据需要调整生成逻辑。

请注意,上述代码仅是一个简单的示例,flutter_wall_layout插件提供了更多的配置选项和布局灵活性,你可以根据实际需求进行调整。

此外,由于flutter_wall_layout的具体API可能会随着版本更新而变化,请务必查阅最新的官方文档和示例代码,以确保你的实现是最新的和正确的。

回到顶部