dart Flutter:创建时间轴用户界面

thtygnil  于 2023-03-10  发布在  Flutter
关注(0)|答案(7)|浏览(120)

我试图使时间轴用户界面如下:

但我最终会执行以下操作:

我想增加垂直分隔符的高度时,没有我的描述文本增加行。我应该怎么做?
这是link for the code

pgx2nnw8

pgx2nnw81#

我也喜欢Osama的答案,但这里是我的快速自定义实现。它使用CustomPainter来画线。

import 'package:flutter/material.dart';

class Timeline extends StatelessWidget {
  const Timeline({
    Key? key,
    required this.children,
    this.indicators,
    this.isLeftAligned = true,
    this.itemGap = 12.0,
    this.gutterSpacing = 4.0,
    this.padding = const EdgeInsets.all(8),
    this.controller,
    this.lineColor = Colors.grey,
    this.physics,
    this.shrinkWrap = true,
    this.primary = false,
    this.reverse = false,
    this.indicatorSize = 30.0,
    this.lineGap = 4.0,
    this.indicatorColor = Colors.blue,
    this.indicatorStyle = PaintingStyle.fill,
    this.strokeCap = StrokeCap.butt,
    this.strokeWidth = 2.0,
    this.style = PaintingStyle.stroke,
  })  : itemCount = children.length,
        assert(itemGap >= 0),
        assert(lineGap >= 0),
        assert(indicators == null || children.length == indicators.length),
        super(key: key);

  final List<Widget> children;
  final double itemGap;
  final double gutterSpacing;
  final List<Widget>? indicators;
  final bool isLeftAligned;
  final EdgeInsets padding;
  final ScrollController? controller;
  final int itemCount;
  final ScrollPhysics? physics;
  final bool shrinkWrap;
  final bool primary;
  final bool reverse;

  final Color lineColor;
  final double lineGap;
  final double indicatorSize;
  final Color indicatorColor;
  final PaintingStyle indicatorStyle;
  final StrokeCap strokeCap;
  final double strokeWidth;
  final PaintingStyle style;

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      padding: padding,
      separatorBuilder: (_, __) => SizedBox(height: itemGap),
      physics: physics,
      shrinkWrap: shrinkWrap,
      itemCount: itemCount,
      controller: controller,
      reverse: reverse,
      primary: primary,
      itemBuilder: (context, index) {
        final child = children[index];
        final _indicators = indicators;

        Widget? indicator;
        if (_indicators != null) {
          indicator = _indicators[index];
        }

        final isFirst = index == 0;
        final isLast = index == itemCount - 1;

        final timelineTile = <Widget>[
          CustomPaint(
            foregroundPainter: _TimelinePainter(
              hideDefaultIndicator: indicator != null,
              lineColor: lineColor,
              indicatorColor: indicatorColor,
              indicatorSize: indicatorSize,
              indicatorStyle: indicatorStyle,
              isFirst: isFirst,
              isLast: isLast,
              lineGap: lineGap,
              strokeCap: strokeCap,
              strokeWidth: strokeWidth,
              style: style,
              itemGap: itemGap,
            ),
            child: SizedBox(
              height: double.infinity,
              width: indicatorSize,
              child: indicator,
            ),
          ),
          SizedBox(width: gutterSpacing),
          Expanded(child: child),
        ];

        return IntrinsicHeight(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.start,
            children:
                isLeftAligned ? timelineTile : timelineTile.reversed.toList(),
          ),
        );
      },
    );
  }
}

class _TimelinePainter extends CustomPainter {
  _TimelinePainter({
    required this.hideDefaultIndicator,
    required this.indicatorColor,
    required this.indicatorStyle,
    required this.indicatorSize,
    required this.lineGap,
    required this.strokeCap,
    required this.strokeWidth,
    required this.style,
    required this.lineColor,
    required this.isFirst,
    required this.isLast,
    required this.itemGap,
  })  : linePaint = Paint()
          ..color = lineColor
          ..strokeCap = strokeCap
          ..strokeWidth = strokeWidth
          ..style = style,
        circlePaint = Paint()
          ..color = indicatorColor
          ..style = indicatorStyle;

  final bool hideDefaultIndicator;
  final Color indicatorColor;
  final PaintingStyle indicatorStyle;
  final double indicatorSize;
  final double lineGap;
  final StrokeCap strokeCap;
  final double strokeWidth;
  final PaintingStyle style;
  final Color lineColor;
  final Paint linePaint;
  final Paint circlePaint;
  final bool isFirst;
  final bool isLast;
  final double itemGap;

  @override
  void paint(Canvas canvas, Size size) {
    final indicatorRadius = indicatorSize / 2;
    final halfItemGap = itemGap / 2;
    final indicatorMargin = indicatorRadius + lineGap;

    final top = size.topLeft(Offset(indicatorRadius, 0.0 - halfItemGap));
    final centerTop = size.centerLeft(
      Offset(indicatorRadius, -indicatorMargin),
    );

    final bottom = size.bottomLeft(Offset(indicatorRadius, 0.0 + halfItemGap));
    final centerBottom = size.centerLeft(
      Offset(indicatorRadius, indicatorMargin),
    );

    if (!isFirst) canvas.drawLine(top, centerTop, linePaint);
    if (!isLast) canvas.drawLine(centerBottom, bottom, linePaint);

    if (!hideDefaultIndicator) {
      final Offset offsetCenter = size.centerLeft(Offset(indicatorRadius, 0));

      canvas.drawCircle(offsetCenter, indicatorRadius, circlePaint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

你可以这样称呼它:

Timeline(
  children: <Widget>[
    Container(height: 100, color: color),
    Container(height: 50, color: color),
    Container(height: 200, color: color),
    Container(height: 100, color: color),
  ],
  indicators: <Widget>[
    Icon(Icons.access_alarm),
    Icon(Icons.backup),
    Icon(Icons.accessibility_new),
    Icon(Icons.access_alarm),
  ],
),

mo49yndu

mo49yndu2#

new ListView.builder(
                  itemBuilder: (BuildContext context, int index) {
                    return new Stack(
                      children: <Widget>[
                        new Padding(
                          padding: const EdgeInsets.only(left: 50.0),
                          child: new Card(
                            margin: new EdgeInsets.all(20.0),
                            child: new Container(
                              width: double.infinity,
                              height: 200.0,
                              color: Colors.green,
                            ),
                          ),
                        ),
                        new Positioned(
                          top: 0.0,
                          bottom: 0.0,
                          left: 35.0,
                          child: new Container(
                            height: double.infinity,
                            width: 1.0,
                            color: Colors.blue,
                          ),
                        ),
                        new Positioned(
                          top: 100.0,
                          left: 15.0,
                          child: new Container(
                            height: 40.0,
                            width: 40.0,
                            decoration: new BoxDecoration(
                              shape: BoxShape.circle,
                              color: Colors.white,
                            ),
                            child: new Container(
                              margin: new EdgeInsets.all(5.0),
                              height: 30.0,
                              width: 30.0,
                              decoration: new BoxDecoration(
                                  shape: BoxShape.circle,
                                  color: Colors.red),
                            ),
                          ),
                        )
                      ],
                    );
                  },
                  itemCount: 5,
                )

输出将类似于这个图像

bxpogfeg

bxpogfeg3#

对于那些滚动到这里寻找实现时间线的简单方法的人来说,现在可以使用timeline_tile轻松地实现这一点。
查看此特定交付布局:

或者这个天气时间轴:

此外,beautiful_timelines存储库包含一些使用此包构建的示例。
网络演示

xxb16uws

xxb16uws4#

class MyTimeLine extends StatefulWidget {
 @override
 _TimeLineState createState() => _TimeLineState();
 }

class _TimeLineState extends State<MyTimeLine> {
@override
Widget build(BuildContext context) {
return new Padding(
  padding: new EdgeInsets.symmetric(horizontal: 10.0),
  child: new Column(
    children: <Widget>[
         IntrinsicHeight(
        child: new Row(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Wrap(
              direction: Axis.vertical,
              children: <Widget>[
                new Container(
                  width: 30.0,
                  child: new Center(
                    child: new Stack(
                      children: <Widget>[
                        new Padding(
                          padding: new EdgeInsets.only(left: 12.0),
                          child: new Container(
                              margin:
                                  new EdgeInsets.symmetric(vertical: 4.0),
                              height: double.infinity,
                              width: 1.0,
                              color: Colors.deepOrange),
                        ),
                        new Container(
                          padding: new EdgeInsets.only(),
                          child: new Icon(Icons.star, color: Colors.white),
                          decoration: new BoxDecoration(
                              color: new Color(0xff00c6ff),
                              shape: BoxShape.circle),
                        )
                      ],
                    ),
                  ),
                ),
              ],
            ),
            new Expanded(
              child: new Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  new Padding(
                    padding: new EdgeInsets.only(left: 20.0, top: 5.0),
                    child: new Text(
                      'Header Text',
                      style: new TextStyle(
                          fontWeight: FontWeight.w500,
                          color: Colors.deepOrange,
                          fontSize: 16.0),
                    ),
                  ),
                  new Padding(
                    padding: new EdgeInsets.only(left: 20.0, top: 5.0),
                    child: new Text(
                        'Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here Lorem ipsum description here description here '),
                  )
                ],
              ),
            )
          ],
        ),
      )
    ],
  ),
);
} 
}
w1jd8yoj

w1jd8yoj5#

看看这个酒吧。https://pub.dev/packages/timeline_widget
它有助于创建美丽的时间轴用户界面与一切定制。也可在左,右和中心对齐选项。这是一个如何使用它的例子!

TimelineView(
  align: TimelineAlign.rightAlign,
  lineWidth: 4,
  lineColor: Colors.deepOrange,
  imageBorderColor: Colors.deepOrange,
  image: [
    Container(
        padding: EdgeInsets.all(15),
        child: Image.asset("assets/pre-breakfast-image.png")),
    Container(
        padding: EdgeInsets.all(15),
        child: Image.asset("assets/breakfast-image.png")),
    Container(
        padding: EdgeInsets.all(15),
        child: Image.asset("assets/pre-lunch-image.png")),
    Container(
        padding: EdgeInsets.all(15),
        child: Image.asset("assets/lunch-image.png")),
    Container(
        padding: EdgeInsets.all(15),
        child: Image.asset("assets/evening-snack-image.png")),
    Container(
        padding: EdgeInsets.all(20),
        child: Image.asset("assets/dinner-image.png")),
  ],
  height: 150,
  width: MediaQuery.of(context).size.width,
  imageHeight: 50,
  children: [
    Container(
        margin: EdgeInsets.fromLTRB(20, 0, 0, 0),
        child: _widgetWeather(20, 71, 31)),
    Container(
        margin: EdgeInsets.fromLTRB(20, 0, 0, 0),
        child: _widgetWeather(15, 75, 55)),
    Container(
        margin: EdgeInsets.fromLTRB(20, 0, 0, 0),
        child: _widgetWeather(25, 73, 30)),
    Container(
        margin: EdgeInsets.fromLTRB(20, 0, 0, 0),
        child: _widgetWeather(22, 65, 35)),
    Container(
        margin: EdgeInsets.fromLTRB(20, 0, 0, 0),
        child: _widgetWeather(21, 55, 32)),
    Container(
        margin: EdgeInsets.fromLTRB(20, 0, 0, 0),
        child: _widgetWeather(20, 65, 35)),
  ],
),
b09cbbtk

b09cbbtk6#

你可以使用flutter_时间轴,无论是主体还是指标,线条都是可定制的。
https://github.com/softmarshmallow/flutter-timeline
这是flutter支持的最新软件包。

7cwmlq89

7cwmlq897#

还有其他更原生和可扩展的方式。一些解决方案提出了固定的位置/大小值,如果我们想在不同屏幕分辨率的多个设备上获得一致的体验,这并不是真正可扩展的。
让我给予你一个解决这个问题的思维模型。让我们选择这个UI设计,我们想用Flutter框架编码:

看起来很吓人?我们应该使用哪一个Widget?这些都是非常有效的问题。经过调查和检查Flutter文档,很明显Flutter的创建者们专注于提供简单UI解决方案的好的、简单的例子。这是不对的。时间轴不是一个简单的时间表,我们需要自己找到工具。
让我们把这个问题分解成几个子问题,首先,让我们只关注一个单项,我们知道,如果我们解决了这个问题,我们可以借助ListView.builderColumn重复这个项目。
单项方法:

项目可以分为两个部分。我们可以创建一个Column,高度为.min,并将两个Row作为子项。行如下:

*红色-顶部包含圆圈(Container/Icon)和文本(Text
*蓝色-包含垂直线Container和项目(Card/Material/Row)的底部部分

Red很简单,它是一个Row,其中crossAxisAlignment: CrossAxisAlignment.center具有以下子级:

  • 一个圆点
  • 卧式分离器
  • 正文

这个问题出现在一个Blue上。让我们使用类似的方法,将它编码为Row,其中垂直线作为第一项,Card作为第二项。
现在我们遇到了一个Flutter开发人员不愿意与我们分享的问题。
问题是:

绿色部分是我们要放入时间轴的内容。我们当然知道,如果我们放入一些文本,它将在较小的设备上缩放得更大,或者如果最终用户增加字体。显然,我们无法使此绿色项目具有固定的高度。

现在问题来了
如何使紫色线与动态变化的绿色部分高度相同?
一个解决方案是IntrinsicHeight。它是一个完美的小部件为这种情况。

/// This class is useful, for example, when unlimited height is available and
/// you would like a child that would otherwise attempt to expand infinitely to
/// instead size itself to a more reasonable height.

对他们来说什么是合理的?很难说。不过:

return IntrinsicHeight(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _renderLine(),
          _renderHorizontalSeparator(),
          _renderTimelineItem(),
        ],
      ),
    );
  }

这样的方法使得IntrinsicHeight子元素(即Row)和其他子元素“顺流”而不是填充整个高度。
所以对于_renderTimelineItem()来说很简单-〉只是一个带item的常规Card
但是_renderLine()应该是这样的:

Widget _renderLine(Color color) => Container(
        color: color,
        height: double.infinity, // this is an equivalent's to Android' MATCH_PARENT
        width: 1,
      );

就这样,问题简化为“如何将行扩展到某个同级动态高度Widget“。
有一点值得注意,他们在IntrinsicHeight的文档中写道:

/// This class is relatively expensive, because it adds a speculative layout
/// pass before the final layout phase. Avoid using it where possible. In the
/// worst case, this widget can result in a layout that is O(N²) in the depth of
/// the tree.

真的吗?尽可能避免?那么我们应该坚持固定值(最糟糕的解决方案)吗?又到2008年了吗?我们生活在一个其他框架已经解决了很多年这样的问题的世界里。ConstraintLayout已经在Android框架中存在了很长时间,它可以轻松地解决这个问题。
它清楚地展示了Flutter框架需要发展得更成熟一点的问题。

相关问题