Flutter относительно популярен в последнее время, но статей о системе Flutter не так много, и неизбежно наступить на яму на ранней стадии; моя статья в основном знакомит с тем, как использовать Flutter для достижения более сложного жестового взаимодействия, и, кстати , поделюсь тем, с чем столкнулся в процессе использования Flutter.Некоторые небольшие ямы, уменьшите заход всех в яму;
Краткий обзор
Этот проект поддерживаетios
а такжеandroid
платформа, эффект следующий
Кстати, кстати, поделитесь поколениемgif
Советы. Для экспорта рекомендуется использовать встроенную функцию записи экрана мобильного телефона.mp4
файл на компьютер, а затем используйте компьютерffmpeg
обработка командной строки, управлениеgif
качество и размер файла, я предлагаю контролировать разрешение на уровне 270p и частоту кадров около 10;
Анализ взаимодействия
Друзья, прочитавшие статью, лучше всего умеют держаться за рукиМгновенное приложение, испытайте взаимодействие страницы исследования для себя, это желтый цвет темы желтого логотипанемедленно, известный как «Желтая Джи»;
Первоначальные функции мгновенного приложения включают вращение карты, извлечение карты и автоматическое удаление карты.Соотношение времени пока не реализовано, но основные функции есть;
По привычке разработчика Android, это взаимодействие можно разделить на два уровня элементов управления, внешний слой нам нужен общий раскрывающийся элемент управления, который я называю下拉控件
; Во внутреннем слое нам нужно реализовать элемент управления, который перетаскивает и перемещает в четырех направлениях: вверх, вниз, влево и вправо, который мы называем卡片控件
;下拉控件
а также卡片控件
Нам нужно разобраться не только с жестами, но и с расположением суб-виджетов, подробности я разберу ниже:
Выпадающее управление:
- Дочерние элементы управления располагаются вертикально сверху вниз, а верхнее меню по умолчанию скрыто за пределами экрана.
- Жест вытягивания вниз перемещает все вспомогательные элементы управления вниз, эффект визуальной разницы меню
- Поддержка щелчка, чтобы автоматически расширять и сворачивать эффекты
Управление картой
- Схема укладки карт, хорошо распределенная
- Верхняя карта поддерживает перетаскивание жестами
- Другие карты немного двигаются в ответ на перетаскивание
- Отпустите, чтобы удалить карту
Начните с кода
Разогрев
Применяя приемы разработки приложений, описанное выше взаимодействие представляет собой не что иное, как расположение элементов управления и распознавание жестов. Конечно, разработка Flutter — это тоже эти подпрограммы, но все — виджеты.Основные макеты, обычно используемые во Flutter:Column
,Row
,Stack
Подождите, распознавание жестовListener
,GestureDetector
,RawGestureDetector
Подождите, это основное внимание в этой статье, не ограничиваясь вышеупомянутыми виджетами, потому что Flutter предоставляет слишком много виджетов, и необходимо помнить о ключевых виджетах.
Итак, давайте разберем функциональные точки один за другим из двух основных технических моментов макета и жестов;
макет
Так называемая компоновка здесь включает в себя управление размером и положением виджета. Как правило, родительский виджет отвечает за судьбу дочернего виджета. Флаттер — это вложение виджетов слой за слоем. следующее объяснит конкретный случай снаружи внутрь;
раскрывающийся список
Во-первых, нам нужно реализовать самый внешний макет.Эффект: дочерние виджеты размещаются вертикально, а самый верхний виджет должен быть размещен за пределами экрана по умолчанию;
Как показано выше, красная область — это диапазон экрана,header
макет меню со скрытым заголовком,content
является основной частью макета карты;
Первая яма
Вертикальное расположение моя первая мысльColumn
, эффект, который я хочу,content
Высота такая же, как высота родительского виджета. Моя первая мысль — позволитьExpanded
пакетcontent
, в результате высота содержимого всегда равнаColumn
высота минусheader
Высота, явление заключается в том, что высота содержимого не заполнена, или явление вытеснения, если вы продолжаете использоватьColunm
может придется отказатьсяExpanded
, дать вручнуюcontent
Назначение высоты может быть способом, но я не хочу назначать ее вручнуюcontent
Высота слишком неэлегантна и, наконец, устарелаColumn
;
Другой вопрос, как скрытьheader
, думаю два варианта:
- внешний слой
Transform
Обернуть весь макет, внутренний слойTransform
пакетheader
, а затем назначьте внутренний слойdy = -headerHeight
, так как жест динамически тянется вниз, не меняетсяheader
изTransform
, но измените самый внешнийTransform
изdy
; - динамическое изменение
header
Высота, начальная высота равна 0, которая вычисляется динамически по мере опускания жеста;
Но у двух выше есть ямки.Первый метод повлияет на событие щелчка элемента управления.onTap
Обратно метод вызываться не будет, второй повлияет на высоту, потому что высота постоянно меняетсяheader
Расположение внутренних подвиджетов трудно контролировать при плохом зрении;
Окончательное предложение
наконец принятStack
к макету, поStack
СотрудничатьPositioned
,выполнитьheader
Макет вне экрана и может быть сделан так, чтоcontent
Макет заполняет родительский виджет;
PullDragWidget
Widget build(BuildContext context) {
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: _contentGestures,
child: Stack(
children: <Widget>[
Positioned(//content布局
top: _offsetY,
bottom: -_offsetY,
left: 0,
right: 0,
child: IgnorePointer(
ignoring: _opened,
child: widget.child,
)),
Positioned(////header布局
top: -widget.dragHeight + _offsetY,
bottom: null,
left: 0,
right: 0,
height: widget.dragHeight,
child: _headerWidget()),
],
));
}
Сначала объяснитеPositioned
базовое использование,top
,bottom
,height
Управляйте высотой и положением и используйте их вместе,top
а такжеbottom
Его можно понимать как marginTop и marginBottom,height
Как следует из названия, это высота прямого виджета, еслиtop
настроитьbottom
Это означает, что высота равнаparentHeight-top-bottom
,еслиtop
/bottom
Сотрудничатьheight
использование, высота, как правило, фиксированная, конечноtop
а такжеbottom
принимает отрицательные числа;
Сначала повторно проанализируйте код_offsetY
расстояние раскрывающегося списка, сумма изменения, начальное значение равно 0,content
Нужныйtop = _offsetY
а такжеbottom = -_offsetY
, верхнее и нижнее положение меняются, а высота не изменится, точно так жеheader
состоит в том, чтобы принятьtop
а такжеheight
управление, высота фиксированная, нужно только динамически менятьtop
Ты сможешь;
Писать макеты во Flutter очень просто, и я настоятельно рекомендую его использовать.Stack
Макет, потому что он более гибкий и не имеет слишком много ограничений, используйте его хорошоStack
В основном приходится использоватьPositioned
, выучить это хорошо;
карточный контроль
Эффект, достигаемый картами, заключается в том, что они сложены последовательно и имеют правильную пропорцию.Это легко себе представить.Stack
Чтобы добиться, конечно же, ямы наверху, используйтеStack
Это очень просто;
Использовать Stack для перекрывающихся эффектов очень просто, а для рассеянных эффектов есть больше возможностей.Например, вы можете использоватьPositioned
, также можно завернутьContainer
Изменятьmargin
илиpadding
, но, учитывая поворот угла, я предпочитаю использоватьTransform
,потому чтоTransform
Может играть не только со смещением, но и с углом, масштабированием и т. д. Его внутренняя операция на самом деле представляет собой матричное преобразование;Transform
Очень простой в использовании, ноTransform
В некоторых особых случаях многоуровневой вложенности ответа не будет.onTap
Случай события, я думаю, это должно бытьTransform
Ошибка события перетаскивания пока не обнаружена Является ли это ошибкой, необходимо подтвердить, и это не повлияет на использование в настоящее время;
CardStackWidget
Widget build(BuildContext context) {
if (widget.cardList == null || widget.cardList.length == 0) {
return Container();
}
List<Widget> children = new List();
int length = widget.cardList.length;
int count = (length > widget.cardCount) ? widget.cardCount : length;
for (int i = 0; i < count; i++) {
double dx = i == 0 ? _totalDx : -_ratio * widget.offset;
double dy = i == 0 ? _totalDy : _ratio * widget.offset;
Widget cardWidget = _CardWidget(
cardEntity: widget.cardList[i],
position: i,
dx: dx,
dy: dy,
offset: widget.offset,
);
if (i == 0) {
cardWidget = RawGestureDetector(
gestures: _cardGestures,
behavior: HitTestBehavior.deferToChild,
child: cardWidget,
);
}
children.add(Container(
child: cardWidget,
alignment: Alignment.topCenter,
padding: widget.cardPadding,
));
}
return Stack(
children: children.reversed.toList(),
);
}
_CardWidget
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 0.75,
child: Transform(
transform: Matrix4.translationValues(
dx + (offset * position.toDouble()),
dy + (-offset * position.toDouble()),
0),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.network(
cardEntity.picUrl,
fit: BoxFit.cover,
),
Container(color: const Color(0x5a000000)),
Container(
margin: EdgeInsets.all(20),
alignment: Alignment.center,
child: Text(
cardEntity.text,
textAlign: TextAlign.center,
style: TextStyle(
letterSpacing: 2,
fontSize: 22,
color: Colors.white,
fontWeight: FontWeight.bold),
maxLines: 4,
),
)
],
),
)),
);
}
Краткая сводка кода макета карты,CardStackWidget
карта управленияStack
Родительский контроль отвечает за раскладку каждой карточки,_CardWidget
Это выложить внутреннюю часть одной карты.Вообще говоря, нет никаких сложностей.Логика контроля детализации находится на верхнем уровне._CardWidget
и нижний слой_CardWidget
Расчет смещения;
Содержимого макета очень много, но в целом он относительно прост.Так называемые питы не обязательно питы, но они не подходят для некоторых сценариев применения;
Распознавание жестов
Наиболее часто используемое распознавание жестов во Flutter — этоListener
а такжеGestureDetector
Эти два виджета, гдеListener
Он в основном обрабатывается для исходной точки касания,GestureDetector
Исходные точки касания были преобразованы в различные жесты, методы этих двух классов описаны ниже;
Listener
Listener({
Key key,
this.onPointerDown, //手指按下回调
this.onPointerMove, //手指移动回调
this.onPointerUp,//手指抬起回调
this.onPointerCancel,//触摸事件取消回调
this.behavior = HitTestBehavior.deferToChild, //在命中测试期间如何表现
Widget child
})
Обратный вызов жеста GestureDetector:
Property/Callback | Description |
---|---|
onTapDown | Вызывается каждый раз, когда пользователь взаимодействует с экраном |
onTapUp | Запускается, когда пользователь перестает касаться экрана |
onTap | Срабатывает при кратковременном прикосновении к экрану |
onTapCancel | Запускается, когда пользователь касается экрана, но не завершает действие касания. |
onDoubleTap | Пользователь дважды касается экрана за короткий промежуток времени. |
onLongPress | Срабатывает, когда пользователь касается экрана более 500 мс. |
onVerticalDragDown | Запускается, когда точка касания начинает взаимодействовать с экраном при вертикальном перемещении |
onVerticalDragStart | Запускается, когда точка касания начинает двигаться вертикально |
onVerticalDragUpdate | Этот обратный вызов запускается каждый раз, когда положение точки касания на экране изменяется. |
onVerticalDragEnd | Когда пользователь перестает двигаться, операция перетаскивания считается завершенной и запускается обратный вызов. |
onVerticalDragCancel | Запускается, когда пользователь внезапно прекращает перетаскивание |
onHorizontalDragDown | Срабатывает, когда точка касания начинает взаимодействовать с экраном при движении в горизонтальном направлении |
onHorizontalDragStart | Срабатывает, когда точка касания начинает двигаться в горизонтальном направлении |
onHorizontalDragUpdate | Этот обратный вызов запускается каждый раз, когда положение точки касания на экране изменяется. |
onHorizontalDragEnd | Срабатывает, когда горизонтальное перетаскивание заканчивается |
onHorizontalDragCancel | Запускается, когда onHorizontalDragDown не завершился успешно |
onPanDown | Запускается, когда точка касания начинает взаимодействовать с экраном |
onPanStart | Уволен, когда точка касания начинает двигаться |
onPanUpdate | Этот обратный вызов запускается каждый раз, когда положение точки касания на экране изменяется. |
onPanEnd | Запускается, когда операция панорамирования завершается |
onScaleStart | Запускается, когда точка касания начинает взаимодействовать с экраном, и устанавливает фокус 1,0 |
onScaleUpdate | Запускается при взаимодействии с экраном и отмечает новый фокус |
onScaleEnd | Точка касания больше не взаимодействует с экраном, а также указывает на завершение жеста масштабирования. |
Listener
а такжеGestureDetector
Как выбрать, сначалаGestureDetector
основывается наListener
инкапсуляция, которая разрешает большинство конфликтов жестов, мы используемGestureDetector
достаточно, ноGestureDetector
Не панацея, нужно подгонять при необходимостиRawGestureDetector
;
Еще одна очень важная концепция, событие жеста Flutter — это механизм всплытия от внутреннего виджета к внешнему виджету.Предполагается, что внутренний и внешний виджеты одновременно прослушивают событие вертикального перетаскивания.onVerticalDragUpdate
, часто внутренний элемент управления получает событие, а внешнее событие пассивно отменяется, эта концепция полностью отличается от механизма перехвата родительского макета Android;
Хотя у Flutter нет внешнего механизма перехвата, похоже, есть проблеск надежды, то естьIgnorePointer
а такжеAbsorbPointer
Widget, эти два приятеля могут игнорировать или запрещать дереву под-виджетов не реагировать на событие;
Анализ жестов
После того, как основной принцип будет представлен, давайте проанализируем взаимодействие кейсов.Как упоминалось выше, я разделил общий макет на раскрывающиеся элементы управления и элементы управления карточками, а также проанализировал поведение перетаскивания мгновенного приложения: когда раскрывающийся список управление не разворачивает выпадающее меню, можно использовать карточное управление В ответ на жесты в направлении вверх, влево и вправо выпадающее управление реагирует только на жест вниз при раскрытии выпадающего меню , карта не может реагировать ни на какие жесты, а выпадающее управление может реагировать на все события по вертикали;
На приведенном выше рисунке более наглядно поясняется реакция на жест в двух состояниях.Раскрывающийся элемент управления является родительским виджетом, а элемент управления карточкой — дочерним виджетом.Поскольку дочерний виджет может отдавать приоритет жесту, мы не можем позволить дочернему виджету реагировать на жест вниз на начальном этапе;
из-заGestureDetector
Инкапсулируются только горизонтальные и вертикальные жесты, и эти два жеста нельзя использовать одновременно.GestureDetector
Из исходного кода вы можете инкапсулировать жест, который отслеживает четыре разных направления?
GestureDetector
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
if (onVerticalDragDown != null ||
onVerticalDragStart != null ||
onVerticalDragUpdate != null ||
onVerticalDragEnd != null ||
onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(debugOwner: this),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = onVerticalDragDown
..onStart = onVerticalDragStart
..onUpdate = onVerticalDragUpdate
..onEnd = onVerticalDragEnd
..onCancel = onVerticalDragCancel;
},
);
}
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
GestureDetector
То, что наконец возвращено,RawGestureDetector
,вgestures
Являетсяmap
, вертикальный жестVerticalDragGestureRecognizer
этот класс;
VerticalDragGestureRecognizer
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the vertical axis.
VerticalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
@override
bool _isFlingGesture(VelocityEstimate estimate) {
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop;
return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
}
@override
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;
@override
Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
@override
double _getPrimaryValueFromOffset(Offset value) => value.dy;
@override
String get debugDescription => 'vertical drag';
}
VerticalDragGestureRecognizer
наследоватьDragGestureRecognizer
, большая часть логики вDragGestureRecognizer
, мы сосредоточимся только на переопределенных методах:
-
_hasSufficientPendingDragDeltaToAccept
Метод является ключевой логикой, управляющей принятием жеста перетаскивания. -
_getDeltaForDetails
Возвращает смещения dx и dy процесса перетаскивания. -
_getPrimaryValueFromOffset
Возвращает значение однонаправленного жеста, ноль может передаваться для разных направлений (как по горизонтали, так и по вертикали) -
_isFlingGesture
Является ли поведение Fling жеста
Пользовательский DragGestureRecognizer
Хотите добиться жестов, которые принимают три направления, пользовательскиеDragGestureRecognizer
Это хорошая идея; я надеюсь принимать параметры в четырех направлениях: вверх, вниз, влево и вправо, отслеживать различные поведения жестов в соответствии с разными параметрами и настраивать направление принятия в соответствии с изображением тыквы.GestureRecognizer
:
DirectionGestureRecognizer
class DirectionGestureRecognizer extends _DragGestureRecognizer {
int direction;
//接受中途变动
ChangeGestureDirection changeGestureDirection;
//不同方向
static int left = 1 << 1;
static int right = 1 << 2;
static int up = 1 << 3;
static int down = 1 << 4;
static int all = left | right | up | down;
DirectionGestureRecognizer(this.direction,
{Object debugOwner})
: super(debugOwner: debugOwner);
@override
bool _isFlingGesture(VelocityEstimate estimate) {
if (changeGestureDirection != null) {
direction = changeGestureDirection();
}
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
final double minDistance = minFlingDistance ?? kTouchSlop;
if (_hasAll) {
return estimate.pixelsPerSecond.distanceSquared > minVelocity &&
estimate.offset.distanceSquared > minDistance;
} else {
bool result = false;
if (_hasVertical) {
result |= estimate.pixelsPerSecond.dy.abs() > minVelocity &&
estimate.offset.dy.abs() > minDistance;
}
if (_hasHorizontal) {
result |= estimate.pixelsPerSecond.dx.abs() > minVelocity &&
estimate.offset.dx.abs() > minDistance;
}
return result;
}
}
bool get _hasLeft => _has(DirectionGestureRecognizer.left);
bool get _hasRight => _has(DirectionGestureRecognizer.right);
bool get _hasUp => _has(DirectionGestureRecognizer.up);
bool get _hasDown => _has(DirectionGestureRecognizer.down);
bool get _hasHorizontal => _hasLeft || _hasRight;
bool get _hasVertical => _hasUp || _hasDown;
bool get _hasAll => _hasLeft && _hasRight && _hasUp && _hasDown;
bool _has(int flag) {
return (direction & flag) != 0;
}
@override
bool get _hasSufficientPendingDragDeltaToAccept {
if (changeGestureDirection != null) {
direction = changeGestureDirection();
}
// if (_hasAll) {
// return _pendingDragOffset.distance > kPanSlop;
// }
bool result = false;
if (_hasUp) {
result |= _pendingDragOffset.dy < -kTouchSlop;
}
if (_hasDown) {
result |= _pendingDragOffset.dy > kTouchSlop;
}
if (_hasLeft) {
result |= _pendingDragOffset.dx < -kTouchSlop;
}
if (_hasRight) {
result |= _pendingDragOffset.dx > kTouchSlop;
}
return result;
}
@override
Offset _getDeltaForDetails(Offset delta) {
if (_hasAll || (_hasVertical && _hasHorizontal)) {
return delta;
}
double dx = delta.dx;
double dy = delta.dy;
if (_hasVertical) {
dx = 0;
}
if (_hasHorizontal) {
dy = 0;
}
Offset offset = Offset(dx, dy);
return offset;
}
@override
double _getPrimaryValueFromOffset(Offset value) {
return null;
}
@override
String get debugDescription => 'orientation_' + direction.toString();
}
из-заDragGestureRecognizer
Многие из методов являются приватными.Если вы хотите повторно скопировать копию кода, то перепишите основной метод для обработки другой логики жестов в соответствии с другими входными параметрами;
Меры предосторожности
Постучать по доске, на заказDragGestureRecognizer
Время:_getDeltaForDetails
представление возвращаемого значенияdx
а такжеdy
dx
dy
_hasSufficientPendingDragDeltaToAccept
_getDeltaForDetails
dx
dy
DirectionGestureRecognizer
left
right
up
down
DirectionGestureRecognizer(DirectionGestureRecognizer.down)
DirectionGestureRecognizer(DirectionGestureRecognizer.left | DirectionGestureRecognizer.right | DirectionGestureRecognizer.up)
DirectionGestureRecognizer
PullDragWidget
_contentGestures = {
//向下的手势
DirectionGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DirectionGestureRecognizer>(
() => DirectionGestureRecognizer(DirectionGestureRecognizer.down),
(instance) {
instance.onDown = _onDragDown;
instance.onStart = _onDragStart;
instance.onUpdate = _onDragUpdate;
instance.onCancel = _onDragCancel;
instance.onEnd = _onDragEnd;
}),
//点击的手势
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(), (instance) {
instance.onTap = _onContentTap;
})
};
Widget build(BuildContext context) {
return RawGestureDetector(//返回RawGestureDetector
behavior: HitTestBehavior.translucent,
gestures: _contentGestures,//手势在此
child: Stack(
children: <Widget>[
Positioned(
top: _offsetY,
bottom: -_offsetY,
left: 0,
right: 0,
child: IgnorePointer(
ignoring: _opened,
child: widget.child,
)),
Positioned(
top: -widget.dragHeight + _offsetY,
bottom: null,
left: 0,
right: 0,
height: widget.dragHeight,
child: _headerWidget()),
],
));
}
PullDragWidget
RawGestureDetector
gestures
_opened
header
IgnorePointer
CardStackWidget
_cardGestures = {
DirectionGestureRecognizer://监听上左右三个方向
GestureRecognizerFactoryWithHandlers<DirectionGestureRecognizer>(
() => DirectionGestureRecognizer(DirectionGestureRecognizer.left |
DirectionGestureRecognizer.right |
DirectionGestureRecognizer.up), (instance) {
instance.onDown = _onPanDown;
instance.onUpdate = _onPanUpdate;
instance.onEnd = _onPanEnd;
}),
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(), (instance) {
instance.onTap = _onCardTap;
})
};
-
onPanDown
onPanUpdate
onPanEnd
GestureDetector
Pan
Drag
Drag
水平
竖直
Pan
Pan
distance
kTouchSlop*2
Drag
dx
dy
kTouchSlop
dx
dy
distance
VerticalDrag
Pan
VerticalDrag
Pan
Pan
VerticalDrag
left
right
up
down
GestureRecognizer
Vertical
Horizontal
left
right
up
down
-
Column
Row
Expanded
Stack
Positioned
Transform
-
GestureDetector
RawGestureDetector
IgnorePointer
GestureRecognizer
-
AnimationController
Tween
EventBus
clone