Dima
Posted on November 20, 2020
Сидишь ты такой, никого не трогаешь, работаешь себе спокойно, и потом тебе прилетает дизайн на очередной проект. Начинаешь смотреть, разбираться.вроде все хорошо и прикольно, только вот мы не задумывались что бывают элементы дизайна которые и в правду могут быть тяжелыми в реализации.
Смысл приложения был очень прост, это всего лишь новостник, только вот в нем была одна часть, которая сразу выглядела очень сложно, это категории.
Какой был дизайн:
И что получилось:
А теперь все сначала. Изначально эта задача не выглядела сложно, так как мы все знаем что существует GridView или же такие библиотеки как https://pub.dartlang.org/packages/flutter_staggered_grid_view. После того как мы протестировали и поняли что анимация в этих случаях работать не будет, так как view обновляется и даже с использованием всяких Hero и других анимаций, у нас не получается нормально привязать элемент для и связать с анимацией плавного перехода, мы начали себе ломать голову в плоть до того чтобы использовать какие-то Wrap, Flexible, FlowLayout..виджеты.
В конечном счёте я пришел к тому что пора думать самому как реализовать эту систему.
Сначала мы выбрали виджет родителя на котором все должно было происходить, и это был Stack. Так как у нас много виджетов, то нам нужно понимать изначальное состояние каждого из них и конечно же их количество.
Первое решение, почему стоит думать сначала об виджете
Изначально, первым решением было, построить матрицу и привязать каждый элемент по крайней верхней\левой точке, и вышло что-то вроде этого:
Так как мы работали изначально с массивом данных нужно было построить наши виджеты и заполнить матрицу начальными значениями:
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());
}));
После добавить в наш список и отрисовать их:
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;
}
А при клике на какой-то элемент соответственно обновлять остальные, если наш текущий элемент пересекается с другим, то подвинуть остальные:
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));
}
и так же:
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");
}
}
Спустя много времени рисований на доске и поиска решений задачи
Основная наша проблема была в том что мы пытались просчитать куда и как должны двигаться наши элементы, а после уже их построить. Но только мы начали связывать каждый элемент друг с другом, все стало на свои места.
Когда мы начали сравнивать пересечение квадратов, мы получили нужный нам результат:
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;
}
В догонку с этим когда сделали проверку на элемент сверху, чтобы каждый элемент был привязан еще и к своему родителю, так как матрица может раздвигаться и элементы которые сверху могли стать ниже или выше, мы получаем что-то вроде этого:
_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;
}
Оригинальный пост: https://medium.com/@followthemoney1/как-мы-делали-custom-category-picker-на-flutter-d078b9697606
Спасибо за внимание!!!
Posted on November 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.