DEV Community

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

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

followthemoney1 profile image Dima ・2 min read

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

Какой был дизайн: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
Спасибо за внимание!!!

Discussion

pic
Editor guide