Создание первого приложения Flutter

Flutter и перспектива

Немного поиграем с 3D и виджетом Transform

В этой статье мы создадим небольшое приложение, демонстрирующее работу виджета Transform по отрисовке 3D перспективы. Мы увидим, как легко это сделать с Flutter, при том, что это сделает не каждый на нативных виджетах.

Ниже скрин приложения в работе (маленький кружок показывает позицию пальца пользователя на экране):

Flutter и перспектива

Приступим

Создайте проект командой flutter create или же средствами вашей IDE. Добавьте два виджета в наше приложение:  Transform и GestureDetector.

Первый:

// v1: move default app to separate function with fixed name
// Add transform widget, rotate and perspective
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key); // changed

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Offset _offset = Offset(0.4, 0.7); // new

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform(  // Transform widget
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // perspective
        ..rotateX(_offset.dy)
        ..rotateY(_offset.dx),
      alignment: FractionalOffset.center,
      child: _defaultApp(context),
    );
  }

  _defaultApp(BuildContext context) {  // new
    return Scaffold(
      appBar: AppBar(
        title: Text('The Matrix 3D'), // changed
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}

Вот что должно получиться при запуске:

Flutter и перспектива

Здесь мы немного почистили типовой код, убрав ненужные комментарии и уже ненужное в Dart 2 ключевое слово new. Мы переместили часть представления (метод build из _MyHomePageState) в отдельный метод _defaultApp (строки 49–74). И для простоты  присвоили название прямо в AppBar (строка 52), вместо передачи его параметром в MyHomePage.

Виджет Transform

Добавленный виджет Transform описан в строках 39–46. Посмотрим на него внимательнее. Transform принимает 3D матрицу перехода, т.е. Matrix4. Почему 3D матрица? Разве Flutter не для двумерной графики?

Практически все современные смартфоны, за исключением самых слабых, имеют невероятно быстрый GPU, оптимизированный для 3D графики. Следовательно, практически всё, что вы видите на смартфоне, это обработка в 3D, даже изначально двумерные объекты. Немного странно, да?

Изменяя параметры матрицы, мы манипулируем отображением представления (даже в 3D!). Типичные трансформации включают перемещение, поворот, изменение масштаба и перспективы. Создаём экземпляр матрицы методом identity (строка 40), затем трансформируем его. Трансформации не коммутативны, поэтому нам необходимо определять их в правильном порядке. Законченная матрица будет отправлена GPU для трансформации затронутых ею объектов.

Трансформация это довольно непростая тема, но если вы хотите узнать больше про неё, погуглите материалы по 3D графике, а также  по матрицам перехода и однородным координатам.

Перспектива

Первая трансформация (строка 41) затрагивает изменение перспективы. Перспектива делает удалённые объекты меньше. Установим настройки матрицы так: ряд 3, колонка 2, увеличение 0.001.

Что же за число 0.001? Путём увеличения и уменьшения этого значения, поиграйте с перспективой — это будет похоже на  увеличение/уменьшение картинки в объективе камеры. Чем больше это число, тем более выражена перспектива — как будто вы стоите ближе к объекту.

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

Повороты

Затем в строках 42 и 43 мы задаём два поворота на основе значения переменной _offset (в стр. 29; а ниже мы используем эту же переменную для отслеживания положения пальца пользователя). И что удивительно, поворот по оси X зависит от смещения Y, а поворот по оси Y — от смещения X. Почему так?

Flutter и перспектива

Рассмотрим это изображение, на котором зелеными стрелками указаны оси X и Y. Эти оси начинаются в верхнем левом углу дисплея (именно поэтому ось Y указывает вниз), но наша программа устанавливает начало координат (в строке 44) из центра дисплея.

Поворот задаётся по отношению к оси, метод rotateX определяет поворот вокрут оси X, которая отклоняется по направлению оси Y (вверх-вниз). Точно так же, rotateY отклоняется в направлении X (лево-право) (вокруг оси Y). Поэтому rotateX контролируется с _offset.dy и rotateY с _offset.dx.

Также здесь есть ось Z, которая идёт перпендикулярно экрану, таким образом значение Z положительно при удалении от зрителя. И метод rotateZ поворачивает объект в плоскости дисплея.

Взаимодействие

Ну и наконец мы должны добавить виджет GestureDetector:

// v2: add Gesture detector
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key); // changed

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Offset _offset = Offset.zero; // changed

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform(  // Transform widget
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // perspective
        ..rotateX(0.01 * _offset.dy) // changed
        ..rotateY(-0.01 * _offset.dx), // changed
      alignment: FractionalOffset.center,
      child: GestureDetector( // new
        onPanUpdate: (details) => setState(() => _offset += details.delta),
        onDoubleTap: () => setState(() => _offset = Offset.zero),
        child: _defaultApp(context),
      )
    );
  }

  _defaultApp(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('The Matrix 3D'), // changed
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}

В строке 28 _offset инициализируется в значении нуля. Строки 44–48 определяют GestureDetector, отслеживающий два вида жестов: перемещение (напр., движения пальца по экрану) и двойной тап. В строке 45 увеличиваем количество движений (в пикселях) в переменной _offset. В строке 46 обнуляем _offset при двойном тапе пользователя по экрану. И для обоих жестов метод setState() перерисовывает экран.

В строках 41 и 42 смещение (в пикселях) умножается на 0.01 для удобства расчета вращения (оно указывается в радианах, где полное вращение это 2π , т.е. приблизительно 6.28  — таким образом для полного вращения потребуется перемещение на 628 пикселей).

Готово!

Конечно, ни один из этих объектов не является по-настоящему трёхмерным. Все они двумерные (плоские) (даже искусственные тени от AppBar и FAB). Однако, это не мешает нам крутить их в трёхмерном пространстве как угодно.

Ещё один пример

Flutter и перспектива

Итак, сделаем красивый псевдотрёхмерный эффект, наподобие такого:

Flutter и перспектива

Как это работает

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

Как это воплотить в коде? Разобьём эту задачу на две:

  • Разрезаем элемент на две половины
  • Вращаем одну половинку вокруг оси X

Почитав документацию Flutter, я нашёл два виджета, подходящих для этой задачи: ClipRect и Transform.

Реализация

  • Разрезаем элемент на две половины:

Виджет ClipRect имеет параметр clipper для установки параметров изображения, но также есть другой способ использования ClipRect, в комбинации с Align:

class FlipWidget extends StatelessWidget {
  Widget child;

  FlipWidget({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ClipRect(
            child: Align(
          alignment: Alignment.topCenter,
          heightFactor: 0.5,
          child: child,
        )),
        Padding(
          padding: EdgeInsets.only(top: 2.0),
        ),
        ClipRect(
            child: Align(
          alignment: Alignment.bottomCenter,
          heightFactor: 0.5,
          child: child,
        )),
      ],
    );
  }
}

Пробуем:

Flutter и перспектива

Готово. К этому добавим, что child позволяет нам свободно управлять содержимым анимации (текстом, картинкой, да чем угодно).

  • Вращаем одну половинку вокруг оси X:

Виджет Transform имеет параметр transform типа Matrix4 для выбора вида применяемой трансформации, также Matrix4 имеет конструктор rotationX(), похоже, что мы можем его использовать для верхней половинки панели:

@override
Widget build(BuildContext context) {
   return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Transform(
          transform: Matrix4.rotationX(pi / 4),
          alignment: Alignment.bottomCenter,
          child: ClipRect(
              child: Align(
            alignment: Alignment.topCenter,
            heightFactor: 0.5,
            child: child,
          )),
        ),
        ...
      ],
    );
}

Пробуем:

Flutter и перспектива

Правда, это похоже на эффект масштабирования?

Изменим передаваемые значения в Matrix4 должно всё исправить:

...
Transform(
  transform: Matrix4.identity()..setEntry(3, 2, 0.006)..rotateX(pi / 4),
  alignment: Alignment.bottomCenter,
  child: ClipRect(
      child: Align(
    alignment: Alignment.topCenter,
    heightFactor: 0.5,
    child: child,
  )),
),
...

Пробуем:

Flutter и перспектива

Осталось добавить анимацию для нашего виджета. Здесь немного сложнее. Фактически, каждая панель содержит контент с обеих сторон (спереди и сзади), но в один момент времени видна только одна сторона. Думаю, что нам нужно создать анимацию, в которой панели поворачиваются вверх — она будет состоять из двух этапов (последовательно), первый — перевернуть нижнюю половину вверх и показать нижнюю половину следующей панели, в то же время скрыть нижнюю половину текущей панели, а второй этап — перевернуть верхнюю половину в том же направлении и открыть верхнюю половину следующего, тут же спрятав верхнюю половину текущей:

Flutter и перспектива

Полный код можно скачать здесь.

Вот что получим в итоге:

Flutter и перспектива

Написано по материалам статей «Perspective on Flutter» и «Make 3D flip animation in Flutter»

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Flutter и перспектива

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).

Leave a Comment

Чтобы не пропустить новые статьи, оставь свой Email

Поздравляем вы подписаны на новости ТехноДжем!

TВо время отправки данных произошла ошибка. Попробуйте ещё раз

Оставляя свою почту