Flutter - Вы видели такое классное приложение😍?

внешний интерфейс Flutter
Flutter - Вы видели такое классное приложение😍?

Эта статья участвовала в "Проект «Звезда раскопок»”, чтобы выиграть творческий подарочный пакет и бросить вызов творческим поощрительным деньгам.

Предисловие: Сегодня 1024 год, желаю всем братьям счастливого праздника, чтобы никогда не выпадали волосы, никогда не было жуков😜. Серьезно: несколько дней назад я нашел проект Flutter с очень классными анимациями, приложение для формирования привычки, после прочтения которого я просто не мог оторваться, у него богатые функции, поэтому я сразу нашел автора с открытым исходным кодом. , попросил у него разрешения написать. Затем начался анализ проекта (пожалуйста, поставьте лайк!!! Поверьте, вы что-то приобретете, прочитав это 👍)

Я частично изменил код его проекта, измененный исходный код находится в конце статьи~

Адрес открытого проекта:GitHub.com/design do/legal…

Сначала о рендерах:

tt0.top-432794.gif tt0.top-795301.gif

Функций тоже много, исходники можно скачать самому (если считаете, что это хорошо, поставьте звезду опенсорсеру, другим не просто!)

В центре внимания этого анализа:

  • Анимация экрана входа в систему, обработка полей ввода и верхнее всплывающее окно
  • Анимируйте нижнюю панель навигации
  • Анимация домашней страницы и обработка кругового индикатора выполнения
  • Адаптировать к темному режиму (анализировать авторское глобальное управление состоянием)

1. Анимация интерфейса входа в систему, обработки поля ввода и всплывающего окна вверху.

  • анимация

    Здесь общая анимация масштабирования в 3 анимациях, поля ввода, кнопка проверки кода анимации панорамирования, экран входа в анимацию масштабирования.

    Когда мы используем анимацию, нам нужно определить контроллер для контроля и управления анимацией.

    AnimationController _animationController;
    

    Конечно, при использовании анимации наше состояние нужно смешивать с классом SingleTickerProviderStateMixin.

    На рендерах нетрудно заметить, что анимация прямо разнесена, поэтому весь наш интерфейс управляется всего одним контроллером, так что он постепенно отображается сверху вниз.

    Что касается масштабирования анимации, во флаттере нам нужно использовать ScaleTransition, самый важный из которых:

    Animation<double> scale //控制widget缩放
    

    Давайте посмотрим, как использовать его в деталях:

    ScaleTransition(
        //控制缩放从0到1
      scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
          //控制动画的Controller
        parent: _animationController,
          //0,0.3是动画运行的时间
          //curve用来描述曲线运动的动画
        curve: Interval(0, 0.3, curve: Curves.fastOutSlowIn),
      )),
      child:...
    )
    

    То же самое верно и для других анимаций здесь, разница заключается в анимации и времени выполнения анимации.

    Ключевое отличие:

    Поле ввода проверочного кода:

    curve: Interval(0.3, 0.6, curve: Curves.fastOutSlowIn),
    

    Кнопка «Получить код подтверждения»:

    Основное отличие здесь в том, что position используется для обработки начальной абсолютной позиции.

    SlideTransition(
        //大家可以将begin: Offset(2, 0)的数据更改,这样就会清晰的体验到它的功能
      position: Tween<Offset>(begin: Offset(2, 0), end: Offset.zero)
          .animate(CurvedAnimation(
          parent: _animationController,
          curve:
          Interval(0.6, 0.8, curve: Curves.fastOutSlowIn))),child:...)
    

    Кнопка входа:

    ScaleTransition(
      scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.8, 1, curve: Curves.fastOutSlowIn),
      )),child:...)
    

    Реализация анимации такова, не правда ли, очень просто~

  • Ограниченная обработка поля ввода номера мобильного телефона

登录输入框处理.png

Я думаю, что этот стиль очень классный, в основном потому, что он не очень распространен в обычное время, поэтому давайте проанализируем его.

Здесь мы инкапсулируем поле ввода CustomEditField, которое может лучше обрабатывать анимацию.

определение анимации

///文本内容
String _value = '';
TextEditingController editingController;
AnimationController numAnimationController;
Animation<double> numAnimation;

И этот компонент должен смешать (Mixin) TickerProviderStateMixin и AutomaticKeepAliveClientMixin, потому что AnimationController должен вызвать метод createTicker в TickerProvider (если вам интересно, вы можете просмотреть исходный код флаттера)

with TickerProviderStateMixin, AutomaticKeepAliveClientMixin

При инициализации:

@override
void initState() {
_value = widget.initValue;
  //初始化controller
editingController = TextEditingController(text: widget.initValue);
  //初始化限制框的控制器与动画
numAnimationController =
    AnimationController(duration: Duration(milliseconds: 500), vsync: this);
numAnimation = CurvedAnimation(
    parent: numAnimationController, curve: Curves.easeOutBack);
if (widget.initValue.length > 0) {
  numAnimationController.forward(from: 0.3);
}
super.initState();
}

При уничтожении:

@override
void dispose() {
editingController.dispose();
numAnimationController.dispose();
super.dispose();
}

Пользовательский интерфейс: используйте стек, чтобы обернуть поле ввода и поле ограничения

Stack(
  children:[
  	TextField(),
  	//限制框的动画,所以在外面套一层ScaleTransition
  	ScaleTransition(
          child:Padding()
      )
  ]
)

При использовании этого инкапсулированного компонента мы в основном имеем дело с numDecoration

Цвет здесь - обработка глобального управления, непосредственное копирование кода необходимо изменить

numDecoration: BoxDecoration(
  shape: BoxShape.rectangle,
  color: AppTheme.appTheme.cardBackgroundColor(),
  borderRadius: BorderRadius.all(Radius.circular(15)),
  boxShadow: AppTheme.appTheme.containerBoxShadow()),
numTextStyle: AppTheme.appTheme
  .themeText(fontWeight: FontWeight.bold, fontSize: 15),
  • Обработка верхнего всплывающего окна

1634777618(1).png

Используя плагин flash, настраиваемое, мощное и простое в использовании окно предупреждений.

Для повторного использования кода здесь выполняется обработка инкапсуляции

class FlashHelper {
  static Future<T> toast<T>(BuildContext context, String message) async {
    return showFlash<T>(
        context: context,
        //显示两秒
        duration: Duration(milliseconds: 2000),
        builder: (context, controller) {
            //弹出框
          return Flash.bar(
              margin: EdgeInsets.only(left: 24, right: 24),
              position: FlashPosition.top,
              brightness: AppTheme.appTheme.isDark()
                  ? Brightness.light
                  : Brightness.dark,
              backgroundColor: Colors.transparent,
              controller: controller,
              child: Container(
                alignment: Alignment.center,
                padding: EdgeInsets.all(16),
                height: 80,
                decoration: BoxDecoration(
                    shape: BoxShape.rectangle,
                    borderRadius: BorderRadius.all(Radius.circular(16)),
                    gradient: AppTheme.appTheme.containerGradient(),
                    boxShadow: AppTheme.appTheme.coloredBoxShadow()),
                child: Text(
                    //显示的文字
                  message,
                  style: AppTheme.appTheme.headline1(
                      textColor: Colors.white,
                      fontWeight: FontWeight.normal,
                      fontSize: 16),
                ),
              ));
        });
  }
}

2. Анимация нижней панели навигации

tt0.top-150276.gif

Я просто в шоке, иконки все нарисованы, автор очень открытый, лайк!

  • Рисунок значка

    Жилой дом:

static final home = FluidFillIconData([
  //房子
  ui.Path()..addRRect(RRect.fromLTRBXY(-10, -2, 10, 10, 2, 2)),
  ui.Path()
    ..moveTo(-14, -2)
    ..lineTo(14, -2)
    ..lineTo(0, -16)
    ..close(),
]);

Четыре квадрата:

static final window = FluidFillIconData([
//正方形
ui.Path()..addRRect(RRect.fromLTRBXY(-12, -12, -2, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, -12, 12, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(-12, 2, -2, 12, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, 2, 12, 12, 2, 2)),
]);

Тренд:

static final progress = FluidFillIconData([
//趋势图
ui.Path()
  ..moveTo(-10, -10)
  ..lineTo(-10, 8)
  ..arcTo(Rect.fromCircle(center: Offset(-8, 8), radius: 2), -1 * math.pi,
      -0.5 * math.pi, true)
  ..moveTo(-8, 10)
  ..lineTo(10, 10),
ui.Path()
  ..moveTo(-6.5, 2.5)
  ..lineTo(0, -5)
  ..lineTo(4, 0)
  ..lineTo(10, -9),
]);

мой:

static final user = FluidFillIconData([
//我的
ui.Path()..arcTo(Rect.fromLTRB(-5, -16, 5, -6), 0, 1.9 * math.pi, true),
ui.Path()..arcTo(Rect.fromLTRB(-10, 0, 10, 20), 0, -1.0 * math.pi, true),
]);

Идея большого парня - быть сильным👍

  • Волновая анимация при переключении

    Здесь в основном две части: одна — это анимация волны при нажатии переключателя, а другая — эффект удара после окончания анимации.

    Этот эффект нам нужно нарисовать через CustomPainter

    Нам нужно определить некоторые параметры (ссылаясь на наиболее важные из них)

    final double _normalizedY;final double _x;
    

    затем нарисуй

 @override
 void paint(canvas, size) {
   // 使用基于“_normalizedY”值的各种线性插值绘制两条三次bezier曲线
   final norm = LinearPointCurve(0.5, 2.0).transform(_normalizedY) / 2;
   final radius = Tween<double>(
       begin: _radiusTop,
       end: _radiusBottom
     ).transform(norm);
   // 当动画结束后的凹凸效果
   final anchorControlOffset = Tween<double>(
       begin: radius * _horizontalControlTop,
       end: radius * _horizontalControlBottom
     ).transform(LinearPointCurve(0.5, 0.75).transform(norm));
   final dipControlOffset = Tween<double>(
       begin: radius * _pointControlTop,
       end: radius * _pointControlBottom
     ).transform(LinearPointCurve(0.5, 0.8).transform(norm));
     
     
   final y = Tween<double>(
       begin: _topY,
       end: _bottomY
       ).transform(LinearPointCurve(0.2, 0.7).transform(norm));
   final dist = Tween<double>(
       begin: _topDistance,
       end: _bottomDistance
       ).transform(LinearPointCurve(0.5, 0.0).transform(norm));
   final x0 = _x - dist / 2;
   final x1 = _x + dist / 2;

     //绘制工程
   final path = Path()
     ..moveTo(0, 0)
     ..lineTo(x0 - radius, 0)
     ..cubicTo(x0 - radius + anchorControlOffset, 0, x0 - dipControlOffset, y, x0, y)
     ..lineTo(x1, y) //背景的宽高
     ..cubicTo(x1 + dipControlOffset, y, x1 + radius - anchorControlOffset, 0, x1 + radius, 0)
       //背景的宽高
     ..lineTo(size.width, 0)
     ..lineTo(size.width, size.height)
     ..lineTo(0, size.height);

   final paint = Paint()
       ..color = _color;

   canvas.drawPath(path, paint);
 }

 @override
 bool shouldRepaint(_BackgroundCurvePainter oldPainter) {
   return _x != oldPainter._x
       || _normalizedY != oldPainter._normalizedY
       || _color != oldPainter._color;
 }

Это завершает фон с волновой анимацией~

  • Анимация отскока кнопки

    По сути, способ реализации такой же, как и у волновой анимации, и рисуется она тоже через CustomPainter.

    (Показать только основной код)

//绘制其他无状态的按钮
final paintBackground = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2.4
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round
        ..color = AppTheme.iconColor;
//绘制点击该按钮时的颜色
final paintForeground = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2.4
    ..strokeCap = StrokeCap.round
    ..strokeJoin = StrokeJoin.round
    ..color = AppTheme.appTheme.selectColor();

Фон иконки и прыжки. Нам нужно определить AnimationController и Animation, чтобы отрисовать анимацию прыжков.

Обработка анимации при инициализации

@override
void initState() {
  _animationController = AnimationController(
      duration: const Duration(milliseconds: 1666),
      reverseDuration: const Duration(milliseconds: 833),
      vsync: this);
  _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController)
    ..addListener(() {
      setState(() {
      });
    });
  _startAnimation();

  super.initState();
}
final offsetCurve = _selected ? ElasticOutCurve(0.38) : Curves.easeInQuint;
final scaleCurve = _selected ? CenteredElasticOutCurve(0.6) : CenteredElasticInCurve(0.6);

final progress = LinearPointCurve(0.28, 0.0).transform(_animation.value);

final offset = Tween<double>(
  begin: _defaultOffset,
  end: _activeOffset
  ).transform(offsetCurve.transform(progress));
final scaleCurveScale = 0.50;
final scaleY = 0.5 + scaleCurve.transform(progress) * scaleCurveScale + (0.5 - scaleCurveScale / 2);

Используется для управления запуском и уничтожением анимаций:

@override
void didUpdateWidget(oldWidget) {
setState(() {
  _selected = widget._selected;
});
_startAnimation();
super.didUpdateWidget(oldWidget);
}

void _startAnimation() {
if (_selected) {
  _animationController.forward();
} else {
  _animationController.reverse();
}
}

макет пользовательского интерфейса:

return GestureDetector(
onTap: _onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
  constraints: BoxConstraints.tight(ne),
  alignment: Alignment.center,
  child: Container(
    margin: EdgeInsets.all(ne.width / 2 - _radius),
    constraints: BoxConstraints.tight(Size.square(_radius * 2)),
    decoration: ShapeDecoration(
      color: AppTheme.appTheme.cardBackgroundColor(),
      shape: CircleBorder(),
    ),
    transform: Matrix4.translationValues(0, -offset, 0),
    //Icon的绘制
    child: FluidFillIcon(
        _iconData,
        LinearPointCurve(0.25, 1.0).transform(_animation.value),
        scaleY,
    ),
  ),
),
);

Это завершает нижнюю панель навигации!

3. Анимация главной страницы и круговая обработка индикатора выполнения

  • Обработка анимации общего списка главной страницы

    Эта часть данных является наиболее сложной.

    Как и в других анимациях, нам нужен контроллер для управления, а на этой странице нам также нужен список для хранения данных.

    final AnimationController mainScreenAnimationController;
    final Animation<dynamic> mainScreenAnimation;
    final List<Habit> habits;
    

    Хранение данных в этой статье пока не анализируется, вы можете запустить исходный код самостоятельно~

    Инициализировать анимацию:

@override
void initState() {
  animationController = AnimationController(
      duration: const Duration(milliseconds: 2000), vsync: this);
  super.initState();
}

Поскольку для анимации используется много компонентов, мы используем AnimatedBuilder для корневого узла, а основные используемые анимации FadeTransition и Transform такие же, как и выше, поэтому я не буду здесь вдаваться в подробности.

  • круговой индикатор выполнения

    Мы инкапсулируем пользователя CircleProgressBar для рисования кругового индикатора выполнения.

    Эта часть пользовательского интерфейса очень проста, в основном потому, что рисование анимации более сложное.

屏幕截图 2021-10-23 140905.jpg

ui:

return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
  animation: this.curve,
  child: Container(),
  builder: (context, child) {
    final backgroundColor =
        this.backgroundColorTween?.evaluate(this.curve) ??
            this.widget.backgroundColor;
    final foregroundColor =
        this.foregroundColorTween?.evaluate(this.curve) ??
            this.widget.foregroundColor;
  
    return CustomPaint(
      child: child,
        //重点是这个封装组件,这里是圆形里面的进度条
      foregroundPainter: CircleProgressBarPainter(
        backgroundColor: backgroundColor,
        foregroundColor: foregroundColor,
        percentage: this.valueTween.evaluate(this.curve),
        strokeWidth: widget.strokeWidth
      ),
    );
  },
),
);

Детальный рисунок:

@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
final Size constrainedSize =
    size - Offset(this.strokeWidth, this.strokeWidth);
final shortestSide =
    Math.min(constrainedSize.width, constrainedSize.height);
final foregroundPaint = Paint()
  ..color = this.foregroundColor
  ..strokeWidth = this.strokeWidth
  ..strokeCap = StrokeCap.round
  ..style = PaintingStyle.stroke;
final radius = (shortestSide / 2);

// Start at the top. 0 radians represents the right edge
final double startAngle = -(2 * Math.pi * 0.25);
final double sweepAngle = (2 * Math.pi * (this.percentage ?? 0));

// Don't draw the background if we don't have a background color
if (this.backgroundColor != null) {
  final backgroundPaint = Paint()
    ..color = this.backgroundColor
    ..strokeWidth = this.strokeWidth
    ..style = PaintingStyle.stroke;
  canvas.drawCircle(center, radius, backgroundPaint);
}

canvas.drawArc(
  Rect.fromCircle(center: center, radius: radius),
  startAngle,
  sweepAngle,
  false,
  foregroundPaint,
);
}

Вот еще одна очень полезная функция:

Определение времени и приветственные слова

屏幕截图 2021-10-23 142038.jpg

Эта демонстрация содержит большую часть времени обработки

屏幕截图 2021-10-23 142440.jpgНапример:

///根据当前时间获取,[monthIndex]个月的开始结束日期
static Pair<DateTime> getMonthStartAndEnd(DateTime now, int monthIndex) {
  DateTime start = DateTime(now.year, now.month - monthIndex, 1);
  DateTime end = DateTime(now.year, now.month - monthIndex + 1, 0);
  return Pair<DateTime>(start, end);
}

Настоятельно рекомендуется изучить всем, чаще используется в разработке!

Большая часть анимационного пользовательского интерфейса этого приложения была проанализирована, а остальные используются повторно. Если вы считаете, что это хорошо, вы можете загрузить его и испытать на себе, а также выработать полезную привычку~

4. Адаптировать к темному режиму (проанализировать авторское глобальное управление состоянием)

Здесь автор использует Bloc для управления состоянием.

///  theme mode
enum AppThemeMode {
  Light,
  Dark,
}
///字体模式
enum AppFontMode {
  ///默认字体
  Roboto,
  ///三方字体
  MaShanZheng,
}
///颜色模式,特定view背景颜色
enum AppThemeColorMode { 
    Indigo, Orange, Pink, Teal, Blue, Cyan, Purple }

На этой основе определяются цвета, стили, такие как:

String fontFamily(AppFontMode fontMode) {
  switch (fontMode) {
    case AppFontMode.MaShanZheng:
      return 'MaShanZheng';
  }
  return 'Roboto';
}

Затем используйте троичное суждение при использовании стиля, чтобы управление состоянием было очень простым.

На этом анализ пользовательского интерфейса этого проекта завершен. Вы также можете использовать этот проект для изучения локального хранилища. Увидев это, вам может понравиться 😘

уведомлять