Как создать собственный виджет временной шкалы во Flutter

Как создать собственную временную шкалу во флаттере, как показано ниже:

Это код, который я пробовал до сих пор:

IntrinsicHeight(
    child: Row(
      children: [
        Container(
          width: 8.w,
          height: double.infinity,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(16.r),
            gradient: const LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color(0XFF402883),
                Color(0XFF6A55EA),
                Color(0XFF8F84D2),
                Color(0XFFADA6D5),
              ],
            ),
          ),
        ),
        SizedBox(width: 10.w),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                "Sign Up",
                style: TextStyle(
                  fontSize: 14.sp,
                  fontWeight: FontWeight.w700,
                ),
              ),
              Text(
                "Create an account to invest & Trade",
                style: TextStyle(
                  fontSize: 12.sp,
                  fontWeight: FontWeight.w400,
                ),
              ),
              SizedBox(height: 10.h),
              Text(
                "Investment Order Submitted",
                style: TextStyle(
                  fontSize: 14.sp,
                  fontWeight: FontWeight.w700,
                ),
              ),
              Text(
                "You will be charged when the company's Escrow account opens(if it hasn't already)",
                style: TextStyle(
                  fontSize: 12.sp,
                  fontWeight: FontWeight.w400,
                ),
              ),
              SizedBox(height: 10.h),
              Text(
                "Funds In Transit",
                style: TextStyle(
                  fontSize: 14.sp,
                  fontWeight: FontWeight.w700,
                ),
              ),
              Text(
                "Your Payment method has been charged,funds should arrive in escrow in 3-5 business days.",
                style: TextStyle(
                  fontSize: 12.sp,
                  fontWeight: FontWeight.w400,
                ),
              ),
              SizedBox(height: 10.h),
              Text(
                "Funds Received",
                style: TextStyle(
                  fontSize: 14.sp,
                  fontWeight: FontWeight.w700,
                ),
              ),
              Text(
                "Your investment finalization occurs at campaign close, potentially earlier with disbursement.",
                style: TextStyle(
                  fontSize: 12.sp,
                  fontWeight: FontWeight.w400,
                ),
              ),
              SizedBox(height: 10.h),
              Text(
                "Funds Invested",
                style: TextStyle(
                  fontSize: 14.sp,
                  fontWeight: FontWeight.w700,
                ),
              ),
              Text(
                "Your investment is complete and can find the details and ownership docs in your portfolio.",
                style: TextStyle(
                  fontSize: 12.sp,
                  fontWeight: FontWeight.w400,
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  )

Я получил этот пользовательский интерфейс с помощью приведенного выше кода:

Теперь добавьте эти точки на вертикальную линию, которая идеально совмещена с заголовком перед ней.

Спасибо.

Просто интересно, имеет ли значение цвет или только пуля? Если цвет на самом деле не имеет значения, то. Используйте стек и создайте эту линию с помощью контейнера и линейного градиента. После этого поместите на него список с помощью виджета Positioned. Используйте представление списка. Или выделите строку и добавьте маркер. После этого расположите его так, чтобы пуля всегда находилась над контейнером. Таким образом, вы можете выровнять заголовок по маркеру.

Ultranmus 02.05.2024 21:37

А если цвет действительно имеет значение, используйте listTile для каждого элемента. Установите ведущий виджет в качестве контейнера с градиентом. Используйте длину элементов в списке и разбивайте цвет на различные значения цвета, используя индекс. Для каждого списка используйте рассчитанное значение начального и конечного цвета для его проектирования.

Ultranmus 02.05.2024 21:43

Можете ли вы привести пример кода, чтобы было более понятно?

Munsif Ali 03.05.2024 07:11
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
3
218
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

(Имейте в виду, что я удалил 10.sp 12.w 8.w и другие подобные вещи, поэтому не забудьте добавить их в свой код)

Поскольку описание первого элемента состоит только из 1 строки, установить фиксированную высоту сложно. Используйте этот код:

 IntrinsicHeight(
            child: Row(
              children: [
                Container(
                  width: 8,
                  height: double.infinity,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(16),
                    gradient: const LinearGradient(
                      begin: Alignment.topCenter,
                      end: Alignment.bottomCenter,
                      colors: [
                        Color(0XFF402883),
                        Color(0XFF6A55EA),
                        Color(0XFF8F84D2),
                        Color(0XFFADA6D5),
                      ],
                    ),
                  ),
                  child: Container(
                    height: 100,
                    margin: const EdgeInsets.only(top: 6),
                    child: ListView.builder(
                      shrinkWrap: true,
                      itemCount: 5,
                      itemBuilder: (context, index) => Container(
                        margin: EdgeInsets.only(
                            top: index == 0
                                ? 2
                                : index == 1
                                    ? 51
                                    : index + 6 + 52,
                            right: 2,
                            left: 2),
                        height: 4,
                        decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(100),
                            color: Colors.white),
                      ),
                    ),
                  ),
                ),
                const SizedBox(width: 10),
                const Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      SizedBox(
                        height: 40,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              "Sign Up",
                              style: TextStyle(
                                fontSize: 14,
                                fontWeight: FontWeight.w700,
                              ),
                            ),
                            Text(
                              "Create an account to invest & Trade",
                              style: TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                      SizedBox(height: 14),
                      SizedBox(
                        height: 55,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              "Investment Order Submitted",
                              style: TextStyle(
                                fontSize: 14,
                                fontWeight: FontWeight.w700,
                              ),
                            ),
                            Text(
                              "You will be charged when the company's Escrow account opens(if it hasn't already)",
                              style: TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                      SizedBox(height: 10),
                      SizedBox(
                        height: 55,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              "Funds In Transit",
                              style: TextStyle(
                                fontSize: 14,
                                fontWeight: FontWeight.w700,
                              ),
                            ),
                            Text(
                              "Your Payment method has been charged,funds should arrive in escrow in 3-5 business days.",
                              style: TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                      SizedBox(height: 10),
                      SizedBox(
                        height: 55,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              "Funds Received",
                              style: TextStyle(
                                fontSize: 14,
                                fontWeight: FontWeight.w700,
                              ),
                            ),
                            Text(
                              "Your investment finalization occurs at campaign close, potentially earlier with disbursement.",
                              style: TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                      SizedBox(height: 10),
                      SizedBox(
                        height: 55,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              "Funds Invested",
                              style: TextStyle(
                                fontSize: 14,
                                fontWeight: FontWeight.w700,
                              ),
                            ),
                            Text(
                              "Your investment is complete and can find the details and ownership docs in your portfolio.",
                              style: TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w400,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),

Вот результат:

точки не совпадают с заголовками.

Munsif Ali 29.04.2024 16:52

Подожди секунду, я исправлю

AmirHossein 29.04.2024 16:54

это было бы прекрасно! Заранее спасибо. Но убедитесь, что он динамически выровнен, поскольку заголовок и описание поступают из серверной части.

Munsif Ali 29.04.2024 17:04

поэтому длину элемента можно изменить?

AmirHossein 29.04.2024 17:05

да, я получаю эти данные из серверной части.

Munsif Ali 29.04.2024 17:06

Ждать. я справлюсь с этим

AmirHossein 29.04.2024 17:07

Давайте продолжим обсуждение в чате.

AmirHossein 29.04.2024 17:15

Готово Проверьте это.

AmirHossein 29.04.2024 18:07

И пользователь может изменить размер шрифта текста. Когда вы жестко закодируете значения, такие как height: 55, это не очень хорошая идея.

Iaroslav Siniugin 29.04.2024 18:56

Поскольку API отсутствует, мне приходит на ум вот что: если бы были доступны программные API, то itemcount был бы длиной полученного списка.

AmirHossein 29.04.2024 19:17
Ответ принят как подходящий

Класс IntrinsicHeight относительно дорог. По возможности избегайте его использования, как, например, в этом случае. См. документацию Flutter по этому.

Вы можете добиться того же результата, не жертвуя производительностью, используя другие собственные компоненты Flutter. Вот полный пример:

Код:

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        textTheme: Theme.of(context).textTheme.apply(
              bodyColor: Colors.white,
              displayColor: Colors.white,
            ),
      ),
      home: const TimelinePage(),
    );
  }
}

class TimelinePage extends StatelessWidget {
  const TimelinePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: SafeArea(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Timeline(
              timelineTiles: [
                TimelineTile(
                  title: "Sign Up",
                  description: "Create an account to invest & Trade",
                ),
                TimelineTile(
                  title: "Investment Order Submitted",
                  description:
                      "You will be charged when the company's Escrow account opens(if it hasn't already)",
                ),
                TimelineTile(
                  title: "Funds In Transit",
                  description:
                      "Your Payment method has been charged,funds should arrive in escrow in 3-5 business days.",
                ),
                TimelineTile(
                  title: "Funds Received",
                  description:
                      "Your investment finalization occurs at campaign close, potentially earlier with disbursement.",
                ),
                TimelineTile(
                  title: "Funds Invested",
                  description:
                      "Your investment is complete and can find the details and ownership docs in your portfolio.",
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class Timeline extends StatefulWidget {
  const Timeline({
    super.key,
    required this.timelineTiles,
    this.sliderWidth = 12,
    this.circleSize = 8,
    this.gapBetweenSliderAndText = 6,
    this.gapTimelineTile = 12,
    this.sliderDecoration,
    this.hasAnimation = true,
  });

  final List<TimelineTile> timelineTiles;
  final double sliderWidth;
  final double circleSize;
  final double gapBetweenSliderAndText;
  final double gapTimelineTile;
  final BoxDecoration? sliderDecoration;
  final bool hasAnimation;

  @override
  State<Timeline> createState() => _TimelineState();
}

class _TimelineState extends State<Timeline> {
  double? widgetHeight;
  GlobalKey textColumnKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) calculateHeight();
    });
  }

  void calculateHeight() {
    if (textColumnKey.currentContext != null) {
      RenderBox renderBox =
          textColumnKey.currentContext!.findRenderObject() as RenderBox;
      setState(() => widgetHeight = renderBox.size.height);
    }
  }

  @override
  void didUpdateWidget(covariant Timeline oldWidget) {
    super.didUpdateWidget(oldWidget);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) calculateHeight();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AnimatedContainer(
          duration: widget.hasAnimation
              ? const Duration(milliseconds: 300)
              : Duration.zero,
          curve: Curves.decelerate,
          width: widget.sliderWidth,
          height: widgetHeight ?? 0,
          margin: EdgeInsets.only(
            left: widget.sliderWidth < widget.circleSize
                ? (widget.circleSize - widget.sliderWidth) / 2
                : 0,
          ),
          decoration: widget.sliderDecoration ??
              BoxDecoration(
                borderRadius: BorderRadius.circular(16),
                gradient: const LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [
                    Color(0XFF402883),
                    Color(0XFF6A55EA),
                    Color(0XFF8F84D2),
                    Color(0XFFADA6D5),
                  ],
                ),
              ),
        ),
        Column(
          key: textColumnKey,
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: List.generate(
            widget.timelineTiles.length,
            (index) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    SizedBox(
                      width:
                          max((widget.sliderWidth - widget.circleSize) / 2, 0),
                    ),
                    Container(
                      width: widget.circleSize,
                      height: widget.circleSize,
                      decoration: const BoxDecoration(
                        color: Colors.white,
                        shape: BoxShape.circle,
                      ),
                    ),
                    SizedBox(
                      width:
                          max((widget.sliderWidth - widget.circleSize) / 2, 0),
                    ),
                    SizedBox(width: widget.gapBetweenSliderAndText),
                    Text(
                      widget.timelineTiles[index].title,
                      style: const TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                  ],
                ),
                Padding(
                  padding: EdgeInsets.only(
                    left: max(widget.sliderWidth, widget.circleSize) +
                        widget.gapBetweenSliderAndText,
                    bottom: widget.timelineTiles.length - 1 == index
                        ? 0
                        : widget.gapTimelineTile,
                  ),
                  child: Text(
                    widget.timelineTiles[index].description,
                    style: const TextStyle(
                      fontSize: 12,
                      fontWeight: FontWeight.w400,
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      ],
    );
  }
}

class TimelineTile {
  final String title;
  final String description;

  TimelineTile({
    required this.title,
    required this.description,
  });
}

Обратите внимание, что в этом примере вы можете изменить интервал и даже расположение элементов по своему усмотрению.

Вы просто меняете список timelineTiles внутри Timeline с помощью своего контроллера состояния и генерируете данные, предоставленные API. Я взял на себя смелость добавить небольшую анимацию, чтобы обеспечить лучшее визуальное восприятие.

Выходы:

Тот же пример, что и вопрос

или

Альтернативная планировка

Большое спасибо. высоко ценю ваши усилия. Это прекрасно сработало.

Munsif Ali 07.05.2024 06:12

Используйте CustomScrollView вместе с SliverStack из этого пакета под названием sliver_tools. Сложите контейнер так, чтобы закрыть вертикальное пространство, и соответствующим образом выровняйте его так, чтобы ведущий виджет ListTile всегда находился вверху контейнера. Ниже приведен пример кода:

class DotListWidget extends StatefulWidget {
  const DotListWidget({super.key});

  @override
  State<DotListWidget> createState() => _DotListWidgetState();
}

class _DotListWidgetState extends State<DotListWidget> {
  List<String> titles = [
    "The Art of War",
    "To Kill a Mockingbird",
    "1984",
    "Pride and Prejudice",
    "The Great Gatsby",
    "Moby-Dick",
    "The Catcher in the Rye",
    "Brave New World",
    "One Hundred Years of Solitude",
    "The Lord of the Rings",
    "Harry Potter and the Philosopher's Stone",
    "The Hobbit",
    "The Alchemist",
    "The Da Vinci Code",
    "The Hunger Games",
    "The Shining",
    "The Hitchhiker's Guide to the Galaxy",
    "The Catcher in the Rye",
    "The Adventures of Tom Sawyer",
    "Crime and Punishment",
    "War and Peace: A Novel by Leo Tolstoy",
    "The Brothers Karamazov: A Novel by Fyodor Dostoevsky",
    "The Count of Monte Cristo: A Novel by Alexandre Dumas",
    "Anna Karenina: A Novel by Leo Tolstoy",
    "Gone with the Wind: A Novel by Margaret Mitchell",
    "Atlas Shrugged: A Novel by Ayn Rand",
    "In Search of Lost Time: A Novel by Marcel Proust",
    "The Stand: A Novel by Stephen King",
    "The Grapes of Wrath: A Novel by John Steinbeck",
    "Les Misérables: A Novel by Victor Hugo"
  ];

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverStack(
          insetOnOverlap: false, // defaults to false
          children: <Widget>[
            SliverPositioned(
              left: 16,
              top: 0,
              bottom: 0,
              child: Container(
                width: 10,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(16),
                  gradient: const LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [
                      Color(0XFF402883),
                      Color(0XFF6A55EA),
                      Color(0XFF8F84D2),
                      Color(0XFFADA6D5),
                    ],
                  ),
                ),
              ),
            ),
            SliverList.builder(
              itemBuilder: (ctx, index) {
                return ListTile(
                  leading: const CircleAvatar(
                    backgroundColor: Colors.black,
                    radius: 5,
                  ),
                  title: Text(titles[index]),
                );
              },
              itemCount: titles.length,
            ),
          ],
        ),
      ],
    );
  }
}

Таким образом, заголовок всегда будет совмещен с маркером, а элемент в списке может быть динамическим.

Другие вопросы по теме