DEV Community

Cover image for Как мы делали Custom Category Picker на Flutter
Dima
Dima

Posted on

4 1

Как мы делали Custom Category Picker на Flutter

Сидишь ты такой, никого не трогаешь, работаешь себе спокойно, и потом тебе прилетает дизайн на очередной проект. Начинаешь смотреть, разбираться.вроде все хорошо и прикольно, только вот мы не задумывались что бывают элементы дизайна которые и в правду могут быть тяжелыми в реализации.
Смысл приложения был очень прост, это всего лишь новостник, только вот в нем была одна часть, которая сразу выглядела очень сложно, это категории.

Какой был дизайн:Alt Text
И что получилось:Alt Text

А теперь все сначала. Изначально эта задача не выглядела сложно, так как мы все знаем что существует GridView или же такие библиотеки как https://pub.dartlang.org/packages/flutter_staggered_grid_view. После того как мы протестировали и поняли что анимация в этих случаях работать не будет, так как view обновляется и даже с использованием всяких Hero и других анимаций, у нас не получается нормально привязать элемент для и связать с анимацией плавного перехода, мы начали себе ломать голову в плоть до того чтобы использовать какие-то Wrap, Flexible, FlowLayout..виджеты.

В конечном счёте я пришел к тому что пора думать самому как реализовать эту систему.
Сначала мы выбрали виджет родителя на котором все должно было происходить, и это был Stack. Так как у нас много виджетов, то нам нужно понимать изначальное состояние каждого из них и конечно же их количество.

Первое решение, почему стоит думать сначала об виджете

Изначально, первым решением было, построить матрицу и привязать каждый элемент по крайней верхней\левой точке, и вышло что-то вроде этого:Alt Text

Так как мы работали изначально с массивом данных нужно было построить наши виджеты и заполнить матрицу начальными значениями:

suggestionMatrix = Map.from(suggestionMatrix.map((key, value) {
      final endIndex = (rowCount * (key + 1));
      return MapEntry(
          key,
          widget.items
              .getRange(
                  rowCount * key,
                  endIndex < widget.items.length
                      ? endIndex
                      : widget.items.length)
              .toList()
              .asMap()
              .entries
              .map((element) {
            final val = element.value;
            final i = element.key;
            //data to use
            return SuggestionItem(
              data: val,
              width: startSize,
              height: startSize,
              currentWeight: 1,
              x: (i) * startSize,
              y: (key) * startSize,
            );
          }).toList());
    }));
Enter fullscreen mode Exit fullscreen mode

После добавить в наш список и отрисовать их:

 List<Widget> childerCards() {
    List<Widget> cardsMatrixWidgets = [];

    suggestionMatrix.entries.forEach((columns) {
      int iColumn = columns.key;
      List<SuggestionItem> rowsList = columns.value;
      rowsList.asMap().entries.forEach((rows) {
        ///get all widgets
        cardsMatrixWidgets.add(AnimatedPositioned.fromRect(
          duration: Duration(milliseconds: widget.stackAnimatedDuration),
          child: item(rows.value),
          rect: currentRow.rect,
        ));
      });
    });

    return cardsMatrixWidgets;
  }
Enter fullscreen mode Exit fullscreen mode

А при клике на какой-то элемент соответственно обновлять остальные, если наш текущий элемент пересекается с другим, то подвинуть остальные:

rowsList.asMap().entries.forEach((rows) {
        //нам нужно определить пересекается ли текущий элемент с элементом который слева или же сверху, если пересекается,
        //тогда текущий элемент мы двигаем вниз или справо в зависимости
        //mark: left 
        if (currentRow.iRow != 0) {
          calcOverflowLeft(rowsList.elementAt(currentRow.iRow - 1), currentRow);
        }
        //mark: top
        if (currentRow.iColumn != 0) {
          calcOverflowTop(
              suggestionMatrix[currentRow.iColumn - 1]
                  .elementAt(currentRow.iRow),
              currentRow);
        }

        //mark: left top first
        if (currentRow.iRow != 0 &&
            currentRow.iRow + 1 < rowsList.length &&
            currentRow.iColumn >= 1) {
          caclLeftTopElementOverflow(
              suggestionMatrix[iColumn - 1].elementAt(currentRow.iRow),
              rowsList.elementAt(currentRow.iRow + 1));
        }
Enter fullscreen mode Exit fullscreen mode

и так же:

caclLeftTopElementOverflow(
      SuggestionCategory prev, SuggestionCategory current) {
    final currentYStartPosition = current.y;
    final prevYEndPosition = prev.y + prev.height;

    final currentXStartPosition = current.x;
    final prevXEndPosition = prev.x + prev.width;
    if (prevYEndPosition >= currentYStartPosition && prev.isExpanded) {
      current.y += (prev.y + prev.height) - current.y;
    }
  }

  void calcOverflowLeft(SuggestionCategory prev, SuggestionCategory current) {
    final currentStartPosition = current.x;
    final prevEndPosition = prev.x + prev.width;
    if (prevEndPosition > currentStartPosition) {
      current.x += prevEndPosition - currentStartPosition;
    } else if (prevEndPosition < currentStartPosition) {
      current.x -= currentStartPosition - prevEndPosition;
    } else if (prevEndPosition != currentStartPosition) {
      print(
          "prevEndPosition = $prevEndPosition currentStartPosition=$currentStartPosition");
    }
  }

  void calcOverflowTop(SuggestionCategory prev, SuggestionCategory current) {
    final currentStartPosition = current.y;
    final prevEndPosition = prev.y + prev.height;
    if (prevEndPosition > currentStartPosition) {
      current.y += prevEndPosition - currentStartPosition;
    } else if (prevEndPosition < currentStartPosition) {
      current.y -= currentStartPosition - prevEndPosition;
    } else if (prevEndPosition != currentStartPosition) {
      print(
          "prevEndPosition = $prevEndPosition currentStartPosition=$currentStartPosition");
    }
  }
Enter fullscreen mode Exit fullscreen mode

Спустя много времени рисований на доске и поиска решений задачи

Основная наша проблема была в том что мы пытались просчитать куда и как должны двигаться наши элементы, а после уже их построить. Но только мы начали связывать каждый элемент друг с другом, все стало на свои места.
Когда мы начали сравнивать пересечение квадратов, мы получили нужный нам результат:

 bool calcOverflowClosestElement(
      {@required List<SuggestionItem> line,
      @required SuggestionItem current,
      bool check = false}) {
    for (SuggestionItem element in line) {
      if (current.rect.intersect(element.rect).height > 0 &&
          current.rect.intersect(element.rect).width > 0) {
        if (current.rect.intersect(element.rect).height > 0) {
          if (!check) {
            current.y += element.rect.intersect(current.rect).height;
          }
        }
      }
    }
    return false;
  }
Enter fullscreen mode Exit fullscreen mode

В догонку с этим когда сделали проверку на элемент сверху, чтобы каждый элемент был привязан еще и к своему родителю, так как матрица может раздвигаться и элементы которые сверху могли стать ниже или выше, мы получаем что-то вроде этого:

 _update({final currentRow, final rowsList}) {
    if (currentRow.iRow > 0) {
      calcOverflowLeft(rowsList.elementAt(currentRow.iRow - 1), currentRow);
    }
    setState(() {
      if (currentRow.iColumn > 0) {
        calcOverflowTop(
            suggestionMatrix[currentRow.iColumn - 1].elementAt(currentRow.iRow),
            currentRow);

        calcOverflowClosestElement(
            line: suggestionMatrix[currentRow.iColumn - 1],
            current: currentRow);
      }
    });
  }

  bool calcOverflowClosestElement(
      {@required List<SuggestionItem> line,
      @required SuggestionItem current,
      bool check = false}) {
    for (SuggestionItem element in line) {
      if (current.rect.intersect(element.rect).height > 0 &&
          current.rect.intersect(element.rect).width > 0) {
        if (current.rect.intersect(element.rect).height > 0) {
          if (!check) {
            current.y += element.rect.intersect(current.rect).height;
          }
        }
      }
    }
    return false;
  }

  void calcOverflowLeft(SuggestionItem prev, SuggestionItem current,
      {bool withGravity}) {
    if (prev.right > current.left) {
      current.x += prev.right - current.left;
    } else if (prev.right < current.left) {
      current.x -= current.left - prev.right;
    }
  }

  void calcOverflowTop(SuggestionItem prev, SuggestionItem current) {
    if (current.x == prev.x)
      current.y += prev.rect.intersect(current.rect).height;
  }
Enter fullscreen mode Exit fullscreen mode

Оригинальный пост: https://medium.com/@followthemoney1/как-мы-делали-custom-category-picker-на-flutter-d078b9697606
Спасибо за внимание!!!

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs