Интерактивный бой Flutter - мгновенное раскрывающееся меню страницы исследования приложения и эффект перетаскивания

Flutter
Интерактивный бой Flutter - мгновенное раскрывающееся меню страницы исследования приложения и эффект перетаскивания

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, думаю два варианта:

  1. внешний слойTransformОбернуть весь макет, внутренний слойTransformпакетheader, а затем назначьте внутренний слойdy = -headerHeight, так как жест динамически тянется вниз, не меняетсяheaderизTransform, но измените самый внешнийTransformизdy;
  2. динамическое изменение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а такжеAbsorbPointerWidget, эти два приятеля могут игнорировать или запрещать дереву под-виджетов не реагировать на событие;

Анализ жестов

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

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

из-за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То, что наконец возвращено,RawGestureDetectorgesturesЯвляется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а такжеdydxdy

_hasSufficientPendingDragDeltaToAccept_getDeltaForDetailsdxdy

DirectionGestureRecognizerleftrightupdown

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()),
        ],
      ));
}

PullDragWidgetRawGestureDetectorgestures_openedheaderIgnorePointer

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

GestureDetectorPanDragDrag水平竖直PanPandistancekTouchSlop*2DragdxdykTouchSlopdxdydistanceVerticalDragPanVerticalDragPanPanVerticalDrag

leftrightupdownGestureRecognizerVerticalHorizontalleftrightupdown

  • ColumnRowExpandedStackPositionedTransform
  • GestureDetectorRawGestureDetectorIgnorePointer
  • GestureRecognizer
  • AnimationControllerTween
  • EventBus

clone