Статьи по Теме
- Flutter реализует анимацию диффузного размытия дна (1) Страница перехода
- Flutter реализует анимацию диффузного размытия дна (2) взаимодействие со страницей
задний план
В течение долгого времени небольшим партнерам команды проекта очень нравился дизайн и взаимодействие An'an, От макета домашней страницы до стиля пользовательской страницы и анимации распространения знака плюс, все они хотят использовать его в проекте. . Учитывая их сильную любовь, был достигнут некоторый паритет компоновки. В последнее время, наконец, пришло время реализовать анимацию распространения после нажатия на знак «плюс» внизу и появления анимации нескольких элементов операции.
Введение
Прежде чем читать эту статью, вам необходимо иметь определенное представление о Flutter, включая жизненный цикл, размытие по Гауссу, анимацию,MediaQuery
Ожидание связанных знаний, конечно, все контент можно найти ~
效果图:
Процесс взаимодействия в основном делится на следующие три шага:
- Щелкните знак «плюс», чтобы распространить эффект размытия по Гауссу по кругу от положения знака «плюс»;
- Элементы операции появляются последовательно с определенными анимационными эффектами;
- Нажмите «X», пустое место или системную клавишу возврата, и фон сожмется до положения знака «плюс» в круге.
В проект загружена полная демка и компоненты, ставьте звездочку проходя мимо~
- ripple_backdrop_animate_route
- OpenJMU/lib/pages/home/AddButtonPage.dart
- OpenJMU/lib/pages/MainPage.dart
Предварительные условия
Для достижения эффекта необходимо уточнить несколько предпосылок:
- Маршрут нужно сделать прозрачным, иначе размытие по Гауссу не может действовать на предыдущий маршрут;
- В соответствии с жизненным циклом выполнение анимации должно быть выполнено в первый раз.
build
выполняется сразу послеinitState
илиdidChangeDependencies
Выполнить, иначе он будет существоватьcontext
Пустой или ошибка синхронизации триггера; - Анимация закрытия должна быть включена
pop()
до исполнения, иначеwidget
был размонтирован(this.mounted == false)
.
Процесс реализации
Ниже приводится конкретный процесс реализации, который будет описан с указанными выше условиями.
Прозрачная маршрутизация перехода
В Интернете существует множество случаев прозрачной маршрутизации, в том числеПуть законаТакже включена прозрачная маршрутизация, поэтому я не буду вдаваться в подробности, просто вставлю код напрямую.
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
@required this.builder,
RouteSettings settings,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
@override
bool get opaque => false;
@override
Color get barrierColor => null;
@override
String get barrierLabel => null;
@override
bool get maintainState => true;
@override
/// 这里时长设置为0,是因为我们的布局一开始
/// 并不包含任何内容,所以直接砍掉跳转时间。
Duration get transitionDuration => Duration.zero;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: result,
);
}
}
После завершения сборки сразуpush
Просто нормально.
Navigator.of(context).push(TransparentRoute(
builder: (context) => AddingButtonPage(),
));
Диффузионная анимация
существуетwidget
Чтобы реализовать анимацию бега в , сначала нужно добавитьTickerProviderStateMixin
И объявить одинcontroller
и анимация(Animation
)сам.
class _DemoPageState extends State<DemoPage>
with TickerProviderStateMixin {
/.../
Animation<double> _backDropFilterAnimation;
AnimationController _backDropFilterController;
В последующих функциях сначалаcontroller
Инициализируйте и установите продолжительность анимации.
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
В это время мы начали думать о размере диффузии: круг с дном в центре и постепенно увеличивающимся радиусом, когда радиус достигает, сколько он может полностью перекрыть видимый диапазон?
Отвечать:
√ (width² + (height * 2 + padding.top)²) / 2
Половина квадратного корня (квадрат двойной высоты, расширенный квадратом)
Разве не очень знакомая формула? Да, это "Теорема Пифагора"~
Вставить с помощьюdart:math
Простая реализация теоремы Пифагора:
import 'dart:math' as math;
double pythagoreanTheorem(double short, double long) {
return math.sqrt(math.pow(short, 2) + math.pow(long, 2));
}
Вот картинка, иллюстрирующая задачу о радиусе.
Чтобы управление размытием полностью покрывало область обзора, радиус рассеянного круга должен быть больше длины гипотенузы, образованной удвоенной длиной и шириной вида и его вершин, а не только высоты вида.padding.top
Высота строки состояния, также добавленная к высоте.
Следовательно, мы определили конечный радиус окружности, а начальный радиус равен 0. В это время вы можете написать первыйTween
используется для определения диапазона изменения радиуса окружности.MediaQuery
Используется для получения длинной и короткой сторон представления. Кстати, определите кривую, чтобы реализовать эффект перехода кривой. ФлаттерCurves
Встроено много кривых, здесь я выбираюCurves.easeInOut
.
/// 视野区域的大小(Size)
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;
/// 动画曲线
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
/// 放大动画的设定档
Animation<double> _backDropFilterAnimation = Tween(
begin: 0.0, end: r * 2
).animate(_backDropFilterCurve);
Причина, по которой значение завершения здесь в два раза больше радиуса, заключается в том, что круг рисуется с размером внешнего квадрата круга, поэтому размер здесь должен быть установлен равным удвоенному радиусу для достижения эффекта истинного радиуса.
Файл конфигурации анимации завершен.Чтобы заставить анимацию двигаться, необходимо привязать значение, выполняемое анимацией, к переменной, и выполнить анимацию. Итак, мы добавляем прослушиватель к этой анимации и выполняем ее.setState
для обновления размера и выполнения анимации.
/// 保存半径的变量
double _backdropFilterSize = 0.0;
/// 监听动画执行
_backDropFilterAnimation.addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
/// 正向执行动画
_backDropFilterController.forward();
На данный момент установлена анимация увеличения, а затем мы создаем макет и привязываем его к анимации.
Размытие по Гауссу
Мы уже знали, когда устанавливали анимацию, что конечный размер круга намного больше, чем видимый размер представления.Чтобы добиться такого относительного макета или абсолютного макета во Flutter, нам нужно использоватьStack
. В этот момент следует отметить, чтоStack
Свойство переполнения (overflow
) должен быть установлен на отображение, в противном случае круг может расширяться только до максимальной ширины вида.
Stack(
overflow: Overflow.visible,
children: <Widget>[],
);
Начнем с рассмотрения размера области размытия по Гауссу. Зная, что радиус круга равен длине диагонали, насколько велика должна быть площадь?
Снова сделайте снимок, чтобы увидеть, где должен находиться наш круг рассеивания относительно вида:
Positioned
Используется абсолютный макет, где его системой отсчета является область просмотра. Тогда мы можем легко судить о верхнем и горизонтальном переливе для расчета размера.
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;
/// 顶部溢出大小
final double topOverflow = r - s.height;
/// 横向溢出大小
final double horizontalOverflow = r - s.width;
return Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: - horizontalOverflow,
right: - horizontalOverflow,
top: - topOverflow,
bottom: - r,
/.../
Этот установленный диапазон представляет собой размер внешнего квадрата, когда круг расширяется до максимального радиуса.
Реализовать размытие по Гауссу во Flutter очень просто, просто используйтеBackdropFilter
Да, обычно его нужно завернуть снаружиClipRect
Он используется для решения проблемы размытой области, и наше требование — круг, поэтому его следует использовать здесь.ClipRRect
.
import 'dart:ui' as ui;
Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: - horizontalOverflow,
right: - horizontalOverflow,
top: - topOverflow,
bottom: - r,
child: SizedBox(
/// 高宽与变量绑定
width: _backdropFilterSize,
height: _backdropFilterSize,
/// 使用圆角ClipRRect达到圆形效果
child: ClipRRect(
/// 圆角的大小,使用最大值则所有时候都为圆形
borderRadius: BorderRadius.circular(r * 2),
child: BackdropFilter(
/// XY用于设定模糊程度
filter: ui.ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
/// 使用空格占位,否则模糊背景不显示
child: Text(" "),
),
),
),
),
],
);
Поместите Gaussian Blur на макет, и мы завершили позиционирование круга.
Установите область, в которой может быть размещен контент
Размытие фона достигнуто, и следующим шагом является размещение содержимого в области разумного размера в макете.
Верхняя половина нашего круга находится в видимой области, поэтому мы находимся на заднем плане, используяAlign
, используя размер переполнения и известный размер видимой области, чтобы определить, где разместить содержимое.
Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(...),
Align(
/// 区域相对顶部居中对齐,在可视区域附近
alignment: Alignment.topCenter,
child: Container(
/// 推出顶部溢出部分,使得区域顶部对齐视图顶部
margin: EdgeInsets.only(top: topOverflow),
/// 将可视区域大小设定为控件大小
width: s.width,
height: s.height,
/// 设置constraint,防止子控件发生意料之外的溢出
constraints: BoxConstraints(
maxWidth: s.width,
maxHeight: s.height,
),
child: child ?? SizedBox(),
),
);
],
);
На этом этапе мы можем легко поместить контент в размытую область и настроить макет, когда он нам не нужен.
Общее исполнение
Анимационная часть завершена, мы инкапсулируем анимационную часть и добавляем ее в первое завершениеbuild
после казни.
import 'package:flutter/scheduler.dart';
class _AddingButtonPageState extends State<AddingButtonPage> with TickerProviderStateMixin {
@override
void initState() {
/// 使用scheduler,将动画加入到build后进行
SchedulerBinding.instance.addPostFrameCallback((_) => backDropFilterAnimate(context));
super.initState();
}
void backDropFilterAnimate(BuildContext context) async {
final Size s = MediaQuery.of(context).size;
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
_backDropFilterAnimation = Tween(
begin: 0.0,
end: pythagoreanTheorem(s.width, s.height) * 2,
).animate(_backDropFilterCurve)
..addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
_backDropFilterController.forward();
}
/.../
В этот момент анимация анимации нижнего диффузного размытия, прыгающего на страницу, так легко завершена ~
Эпилог
Основываясь на многомесячном опыте дайвинга, большинство людей считают, что Flutter сложно анимировать, потому что они не могут понятьAnimation
Различные свойства и операции , и даже документация отрывочны и трудны для понимания, но на самом деле, после того, как она действительно написана, в анимационной части остается лишь небольшое количество кода, и смысл понять несложно.
Добро пожаловать, присоединяйтесьFlutter Candies, чтобы вместе производить милые конфеты Flutter (группа QQ: 181398081)