Трепещущий Kivy. Обзор возможностей фреймворка Kivy и библиотеки KivyMD

—

от автора

Kivy и Flutter — два фреймворка с открытым исходным кодом для кроссплатформенной разработки.

Flutter:

  • создан компанией Google и выпущенный в 2017 году;

  • в качестве языка программирования использует Dart;

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

Kivy:

  • создан сообществом Kivy в 2010 году;

  • в качестве языка программирования использует Python и собственный декларативный язык для разметки UI элементов — KV Language;

  • не использует нативные компоненты, рисуя весь интерфейс с помощью OpenGL ES 2.0 и SDL2;

Недавно на просторах Ютуба наткнулся на видео демонстрацию Flutter приложения — Facebook Desktop Redesign built with Flutter Desktop. Отличное демонстрационное приложение в стиле material design! И поскольку я один из разработчиков библиотеки KivyMD (набор material компонентов для фреймворка Kivy) мне стало интересно, насколько просто будет сделать такой же красивый интерфейс. К счастью автор оставил ссылку на репозиторий проекта.

Как вы думаете, какое приложение на вышеприведенных скриншотах написано с использованием Flutter и какое с помощью Kivy? Ответить сходу трудно, поскольку ярко выраженных отличий нет. Единственное, что сразу бросается в глаза (нижний скриншот) — в Kivy все еще нет нормального сглаживания. И это грустно, но не критично. Сравнивать мы будем отдельные элементы приложения и их исходный код на Dart (Flutter) и Python/KV language (Kivy).

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

StoryCard

Kivy

Разметка карточки на языке KV-Language:

Базовый Python класс:

from kivy.properties import StringProperty  from kivymd.uix.relativelayout import MDRelativeLayout   class StoryCard(MDRelativeLayout):     avatar = StringProperty()     story = StringProperty()     name = StringProperty()      def on_parent(self, *args):         if not self.avatar:             self.remove_widget(self.ids.avatar) 

Flutter:

import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart';  class Story extends StatefulWidget {   final String name;   final String avatar;   final String story;    const Story({     Key key,     this.name,     this.avatar,     this.story,   }) : super(key: key);    @override   _StoryState createState() => _StoryState(); }  class _StoryState extends State<Story> {   @override   Widget build(BuildContext context) {     return Container(       width: 150,       margin: const EdgeInsets.only(top: 30),       decoration: BoxDecoration(         borderRadius: BorderRadius.circular(30),         boxShadow: [           BoxShadow(             color: Colors.black.withOpacity(0.3),             blurRadius: 20,             offset: Offset(0, 10),           ),         ],       ),       child: Stack(         overflow: Overflow.visible,         fit: StackFit.expand,         children: [           ClipRRect(             borderRadius: BorderRadius.circular(30),             child: Image.network(               widget.story,               fit: BoxFit.cover,             ),           ),           if (widget.avatar != null)             Positioned.fill(               top: -30,               child: Align(                 alignment: Alignment.topCenter,                 child: Container(                   decoration: BoxDecoration(                     borderRadius: BorderRadius.circular(30),                     boxShadow: [                       BoxShadow(                         color: Colors.black.withOpacity(0.4),                         blurRadius: 5,                         offset: Offset(0, 3),                       ),                     ],                   ),                   child: ClipRRect(                     borderRadius: BorderRadius.circular(30),                     child: Image.network(                       widget.avatar,                       fit: BoxFit.cover,                       width: 60,                       height: 60,                     ),                   ),                 ),               ),             ),           if (widget.avatar != null)             Positioned.fill(               child: Align(                 alignment: Alignment.bottomCenter,                 child: Row(                   children: [                     Expanded(                       child: Container(                         padding: const EdgeInsets.all(15),                         decoration: BoxDecoration(                           borderRadius: BorderRadius.circular(30),                           gradient: LinearGradient(                             begin: Alignment.topCenter,                             end: Alignment.bottomCenter,                             colors: [                               Colors.transparent,                               Colors.black,                             ],                           ),                         ),                         child: widget.name != null ? Text(                           widget.name,                           textAlign: TextAlign.center,                           maxLines: 1,                           overflow: TextOverflow.ellipsis,                           style: TextStyle(                             color: Colors.white,                             fontWeight: FontWeight.bold,                           ),                         ) : SizedBox(),                       ),                     ),                   ],                 ),               ),             ),         ],       ),     );   } } 

Как видим, код на Python и KV-Language получается вдвое короче. Исходный код проекта на Python/Kivy, который рассматривается в этой статье, имеет общий размер 31 килобайт. 3 килобайта из этого объема приходится на Python код, остальное — KV-Language. Исходный код на Flutter — 54 килобайт. Впрочем, здесь удивляться, кажется, нечему — Python один их самый лаконичных языков программирования в мире.

Мы не будем спорить о том, что лучше: описывать UI при помощи DSL языков или прямо в коде. В Kivy, кстати, также можно строить виджеты Python кодом, но это не очень хорошее решение.

TopBar

Flutter:

Kivy:

Реализация этого бара, включая анимацию, на Python/Kivy заняла всего 88 строчек кода. На Dart/Flutter — 325 строк и 9 килобайт на диске. Посмотрим, что представляет из себя этот виджет:

Лого, три таба, аватар, три таба и один таб — кнопка настроек. Реализация таба с анимированным индикатором:

Анимация индикатора и смена типа курсора мыши реализована в Python файле в одноименном с правилом разметки классе:

from kivy.animation import Animation from kivy.properties import StringProperty, BooleanProperty from kivy.core.window import Window  from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.behaviors import FocusBehavior   class Tab(FocusBehavior, MDBoxLayout):     icon = StringProperty()     active = BooleanProperty(False)      def on_enter(self):         Window.set_system_cursor("hand")      def on_leave(self):         Window.set_system_cursor("arrow")      def on_active(self, instance, value):         Animation(             opacity=value,             width=self.width if value else 0,             d=0.25,             t="in_sine" if value else "out_sine",         ).start(self.ids.separator) 

Мы просто анимируем ширину и opacity индикатора в зависимости от состояния кнопки (active). Состояние кнопки устанавливается в главном классе экрана приложения:

class FacebookDesktop(ThemableBehavior, MDScreen):     def set_active_tab(self, instance_tab):         for widget in self.ids.tab_box.children:             if issubclass(widget.__class__, MDBoxLayout):                 if widget == instance_tab:                     widget.active = True                 else:                     widget.active = False 

Подробнее об анимации а Kivy:

Материальный дизайн. Создание анимаций в Kivy
Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

Реализация на Dart/Flutter.

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

app_logo.dart

import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart';  class AppLogo extends StatelessWidget {   @override   Widget build(BuildContext context) {     return Container(       decoration: BoxDecoration(         borderRadius: BorderRadius.circular(10),         boxShadow: [           BoxShadow(             color: Colors.blue.withOpacity(.6),             blurRadius: 5,             spreadRadius: 1,           ),         ],       ),       child: ClipRRect(         borderRadius: BorderRadius.circular(10),         child: Image.asset(           'assets/images/facebook_logo.jpg',           width: 30,           height: 30,         ),       ),     );   } } 

avatar.dart

import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart';  class TopBarAvatar extends StatefulWidget {   @override   _TopBarAvatarState createState() => _TopBarAvatarState(); }  class _TopBarAvatarState extends State<TopBarAvatar>     with SingleTickerProviderStateMixin {   Animation<Color> _animation;   AnimationController _animationController;    @override   void initState() {     _animationController = AnimationController(       vsync: this,       duration: Duration(milliseconds: 150),     );      _animation = ColorTween(       begin: Colors.grey.withOpacity(.4),       end: Colors.blue.withOpacity(.6),     ).animate(_animationController);      _animation.addListener(() {       setState(() {});     });      super.initState();   }    @override   Widget build(BuildContext context) {     return MouseRegion(       onHover: (event) {         setState(() {           _animationController.forward();         });       },       onExit: (event) {         setState(() {           _animationController.reverse();         });       },       cursor: SystemMouseCursors.click,       child: Padding(         padding: const EdgeInsets.symmetric(horizontal: 15),         child: Container(           decoration: BoxDecoration(             borderRadius: BorderRadius.circular(15),             boxShadow: [               BoxShadow(                 color: _animation.value,                 blurRadius: 10,                 spreadRadius: 0,               ),             ],           ),           child: ClipRRect(             borderRadius: BorderRadius.circular(15),             child: Image.asset(               'assets/images/avatar.jpg',               width: 50,               height: 50,             ),           ),         ),       ),     );   } } 

button.dart

import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart';  class TopBarButton extends StatefulWidget {   final IconData icon;   final bool isActive;   final Function onTap;    const TopBarButton({     Key key,     this.icon,     this.isActive = false,     this.onTap,   }) : super(key: key);    @override   _TopBarButtonState createState() => _TopBarButtonState(); }  class _TopBarButtonState extends State<TopBarButton>     with SingleTickerProviderStateMixin {   Animation<Color> _animation;   AnimationController _animationController;    @override   void initState() {     _animationController = AnimationController(       vsync: this,       duration: Duration(milliseconds: 150),     );      _animation = ColorTween(       begin: Colors.grey.withOpacity(.6),       end: Colors.blue.withOpacity(.6),     ).animate(_animationController);      _animation.addListener(() {       setState(() {});     });      super.initState();   }    @override   void didUpdateWidget(TopBarButton oldWidget) {     if (widget.isActive) {       _animationController.forward();     } else {       _animationController.reverse();     }      super.didUpdateWidget(oldWidget);   }    @override   Widget build(BuildContext context) {     return GestureDetector(       onTap: widget.onTap,       child: MouseRegion(         cursor: SystemMouseCursors.click,         child: Container(           height: 80,           child: Stack(             alignment: Alignment.center,             children: [               Padding(                 padding: const EdgeInsets.symmetric(horizontal: 30),                 child: Icon(                   widget.icon,                   color: _animation.value,                 ),               ),               Positioned(                 bottom: -1,                 child: Align(                   alignment: Alignment.bottomCenter,                   child: AnimatedContainer(                     duration: Duration(milliseconds: 50),                     curve: Curves.easeInOut,                     decoration: BoxDecoration(                       color: _animation.value,                       borderRadius: BorderRadius.circular(5),                       boxShadow: [                         BoxShadow(                           color: _animation.value,                           blurRadius: 5,                           offset: Offset(0, 2),                         ),                       ],                     ),                     width: widget.isActive ? 50 : 0,                     height: 4,                   ),                 ),               ),             ],           ),         ),       ),     );   }    @override   void dispose() {     _animationController.dispose();      super.dispose();   } } 

widget.dart

import 'package:facebook_desktop/screens/home/components/top_bar/app_logo.dart'; import 'package:facebook_desktop/screens/home/components/top_bar/avatar.dart'; import 'package:facebook_desktop/screens/home/components/top_bar/button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';  class TopBar extends StatefulWidget {   @override   _TopBarState createState() => _TopBarState(); }  class _TopBarState extends State<TopBar> {   int _selectedPage = 0;    @override   Widget build(BuildContext context) {     return Container(       color: Colors.white,       padding: const EdgeInsets.symmetric(         horizontal: 30,       ),       child: Row(         children: [           Expanded(             flex: 1,             child: Align(               alignment: Alignment.centerLeft,               child: AppLogo(),             ),           ),           Expanded(             flex: 6,             child: Row(               mainAxisAlignment: MainAxisAlignment.center,               children: [                 TopBarButton(                   icon: FeatherIcons.home,                   isActive: _selectedPage == 0,                   onTap: () {                     setState(() {                       _selectedPage = 0;                     });                   },                 ),                 TopBarButton(                   icon: FeatherIcons.youtube,                   isActive: _selectedPage == 1,                   onTap: () {                     setState(() {                       _selectedPage = 1;                     });                   },                 ),                 TopBarButton(                   icon: FeatherIcons.grid,                   isActive: _selectedPage == 2,                   onTap: () {                     setState(() {                       _selectedPage = 2;                     });                   },                 ),                 TopBarAvatar(),                 TopBarButton(                   icon: FeatherIcons.users,                   isActive: _selectedPage == 3,                   onTap: () {                     setState(() {                       _selectedPage = 3;                     });                   },                 ),                 TopBarButton(                   icon: FeatherIcons.zap,                   isActive: _selectedPage == 4,                   onTap: () {                     setState(() {                       _selectedPage = 4;                     });                   },                 ),                 TopBarButton(                   icon: FeatherIcons.smile,                   isActive: _selectedPage == 5,                   onTap: () {                     setState(() {                       _selectedPage = 5;                     });                   },                 ),               ],             ),           ),           Expanded(             flex: 1,             child: Align(               alignment: Alignment.centerRight,               child: IconButton(                 color: Colors.grey.withOpacity(.6),                 icon: Icon(FeatherIcons.settings),                 onPressed: () {},               ),             ),           ),         ],       ),     );   } } 

ChatCard (Kivy, Flutter)

Анимация сдвига карточки происходит относительно родительского виджета (parent) при получении событий фокуса и анфокуса (on_enter, on_leave):

on_enter: Animation(x=root.parent.x + dp(12), d=0.4, t="out_cubic").start(root) on_leave: Animation(x=root.parent.x + dp(24), d=0.4, t="out_cubic").start(root) 

И базовый класс Python:

from kivy.core.window import Window from kivy.properties import StringProperty  from FacebookDesktop.components.cards.fake_card import FakeCard   class ChatCard(FakeCard):     avatar = StringProperty()     text = StringProperty()     name = StringProperty()      def on_enter(self):         Window.set_system_cursor("hand")      def on_leave(self):         Window.set_system_cursor("arrow") 

Реализация Python/Kivy — 60 строк кода, реализация Dart/Flutter — 182 строки кода.

chat_card.dart

import 'package:ezanimation/ezanimation.dart'; import 'package:facebook_desktop/components/user_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';  class ChatCard extends StatefulWidget {   final String image;   final String name;   final String message;   final EdgeInsets padding;    const ChatCard({     Key key,     this.image,     this.name,     this.message,     this.padding,   }) : super(key: key);    @override   _ChatCardState createState() => _ChatCardState(); }  class _ChatCardState extends State<ChatCard> {   EzAnimation _animation;    @override   void initState() {     _animation = EzAnimation(       0.0,       -5.0,       Duration(milliseconds: 200),       curve: Curves.easeInOut,       context: context,     );      _animation.addListener(() {       setState(() {});     });      super.initState();   }    @override   Widget build(BuildContext context) {     return Transform.translate(       offset: Offset(_animation.value, 0),       child: MouseRegion(         cursor: SystemMouseCursors.click,         onEnter: (event) {           _animation.start();         },         onExit: (event) {           _animation.reverse();         },         child: Padding(           padding: widget.padding ?? const EdgeInsets.all(15),           child: Container(             width: 250,             padding: const EdgeInsets.all(15),             decoration: BoxDecoration(               color: Colors.white,               borderRadius: BorderRadius.circular(10),               boxShadow: [                 BoxShadow(                   color: Colors.black.withOpacity(.1),                   blurRadius: 15,                   offset: Offset(0, 8),                 ),               ],             ),             child: Column(               crossAxisAlignment: CrossAxisAlignment.start,               children: [                 UserTile(                   name: widget.name,                   image: widget.image,                   trailing: Icon(                     FeatherIcons.messageSquare,                     color: Colors.blue,                     size: 14,                   ),                 ),                 SizedBox(                   height: 10,                 ),                 Text(                   widget.message,                   style: TextStyle(color: Colors.grey, fontSize: 12),                   maxLines: 3,                   overflow: TextOverflow.ellipsis,                 ),               ],             ),           ),         ),       ),     );   }    @override   void dispose() {     _animation.dispose();      super.dispose();   } } 

user_tile.dart

 import 'package:facebook_desktop/screens/home/components/section.dart'; import 'package:flutter/material.dart';  class UserTile extends StatelessWidget {   final String name;   final String image;   final Widget trailing;    const UserTile({     Key key,     this.name,     this.image,     this.trailing,   }) : super(key: key);    @override   Widget build(BuildContext context) {     return Row(       crossAxisAlignment: CrossAxisAlignment.start,       children: [         Container(           margin: const EdgeInsets.only(right: 10),           decoration: BoxDecoration(             color: Colors.white,             borderRadius: BorderRadius.circular(10),             boxShadow: [               BoxShadow(                 color: Colors.black.withOpacity(.1),                 blurRadius: 5,                 offset: Offset(0, 2),               ),             ],           ),           child: ClipRRect(             borderRadius: BorderRadius.circular(5),             child: Image(               image: NetworkImage(                 image,               ),               fit: BoxFit.cover,               height: 50,               width: 50,             ),           ),         ),         Column(           crossAxisAlignment: CrossAxisAlignment.start,           children: [             SectionTitle(               title: name,             ),             SizedBox(               height: 5,             ),             Text(               '12 min ago',               style: TextStyle(color: Colors.grey),             ),           ],         ),         if (trailing != null)         Expanded(           child: Align(             alignment: Alignment.topRight,             child: trailing           ),         ),       ],     );   } } 

Но не все так просто, как кажется. В процессе я обнаружил, что в библиотеке KivyMD отсутствуют кнопки с типом «badge». В проекте на Flutter, кстати, тоже использовались кастомные кнопки. Поэтому для создания правой панели инструментов пришлось сделать такие кнопки самостоятельно.

Базовый Python класс:

from kivy.properties import StringProperty  from kivymd.uix.relativelayout import MDRelativeLayout   class BadgeButton(MDRelativeLayout):     icon = StringProperty()     text = StringProperty() 

И уже создать левую панель инструментов:

Даже учитывая, что мне пришлось создавать кастомные кнопки типа «badge», код левой панели инструментов на Python/Kivy получился короче — 58 строк кода, реализация на Dart/Flutter — 97 строк.

button.dart

import 'package:flutter/material.dart';  class LeftBarButton extends StatelessWidget {   final IconData icon;   final String badge;    const LeftBarButton({     Key key,     this.icon,     this.badge,   }) : super(key: key);    @override   Widget build(BuildContext context) {     return GestureDetector(       child: Stack(         children: [           Container(             padding: const EdgeInsets.all(10),             child: Icon(               icon,               color: Colors.grey.withOpacity(.6),             ),           ),           if (badge != null)             Positioned(               top: 5,               right: 2,               child: Container(                 padding: const EdgeInsets.all(3),                 decoration: BoxDecoration(                   borderRadius: BorderRadius.circular(100),                   color: Colors.blue,                 ),                 child: Text(                   badge,                   style: TextStyle(                     color: Colors.white,                     fontSize: 10,                   ),                 ),               ),             )         ],       ),     );   } } 

widget.dart

import 'package:facebook_desktop/screens/home/left_bar/button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart';  class LeftBar extends StatelessWidget {   @override   Widget build(BuildContext context) {     return Container(       margin: const EdgeInsets.all(30),       padding: const EdgeInsets.all(5),       decoration: BoxDecoration(         color: Colors.white,         borderRadius: BorderRadius.circular(50),         boxShadow: [           BoxShadow(             color: Colors.grey.withOpacity(.1),             blurRadius: 2,             offset: Offset(0, 4),           )         ],       ),       child: Column(         mainAxisSize: MainAxisSize.min,         children: [           LeftBarButton(             icon: FeatherIcons.mail,             badge: '10',           ),           SizedBox(             height: 5,           ),           LeftBarButton(             icon: FeatherIcons.search,           ),           SizedBox(             height: 5,           ),           LeftBarButton(             icon: FeatherIcons.bell,             badge: '20',           ),         ],       ),     );   } } 

Безусловно я не умаляю достоинств фреймворка Flutter. Инструмент замечательный! Я всего лишь хотел показать Python разработчикам, что они могут делать те же самые вещи, что и во Flutter, но на их любимом языке программирования с помощью фреймворка Kivy и библиотеки KivyMD. Что касается мобильных платформ, то здесь стоит признать, что Flutter превосходит Kivy в скорости работы. Но это уже уже другая статья… Ссылка на репозиторий проекта Facebook Desktop Redesign built with Flutter Desktop в реализации Python/Kivy/KivyMD.

ссылка на оригинал статьи https://habr.com/ru/post/546684/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *