[Python, Разработка под MacOS, Разработка под Linux, Разработка под Windows] Трепещущий Kivy. Обзор возможностей фреймворка Kivy и библиотеки KivyMD
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
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
SPL
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
SPL
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
SPL
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
SPL
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
SPL
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
SPL
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
SPL
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
SPL
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.
Извините, данный ресурс не поддреживается. :(
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, JavaScript, C#, Логические игры] 10 лучших игр по программированию, которые улучшат ваши навыки (перевод)
- [.NET, Оболочки, API, C#, Разработка под Windows] WinUI 3 — Новая эра разработки под Windows
- [Open source, Виртуализация, Разработка под Linux, Разработка под Windows] Шпаргалка по pip, 6 заблуждений насчет AIOps, бесплатный онлайн-курс, а еще про Windows-программы на Linux
- [Информационная безопасность, Python, Тестирование веб-сервисов] Пишем расширение для Burp Suite с помощью Python
- [Python, PostgreSQL] Обрезаем большую таблицу PostgreSQL в production
- [Python, Программирование, Машинное обучение] Взлом reCAPTCHA v2
- [Python, Научно-популярное, Биотехнологии, Будущее здесь] Визуализация и анализ белков в Biopython (перевод)
- [Python, Microsoft Azure, Тестирование веб-сервисов, Облачные сервисы] HTTP атака на Azure
- [Python, Программирование, Программирование микроконтроллеров] Маленькие Python для маленьких embedded-программистов: CircuitPython и MicroPython для MeowBit
- [Программирование, C++, C, Разработка под Linux] Введение в неблокирующие алгоритмы (перевод)
Теги для поиска: #_python, #_razrabotka_pod_macos (Разработка под MacOS), #_razrabotka_pod_linux (Разработка под Linux), #_razrabotka_pod_windows (Разработка под Windows), #_kivy, #_kivymd, #_python, #_python, #_razrabotka_pod_macos (
Разработка под MacOS
), #_razrabotka_pod_linux (
Разработка под Linux
), #_razrabotka_pod_windows (
Разработка под Windows
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:47
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Kivy и Flutter — два фреймворка с открытым исходным кодом для кроссплатформенной разработки. Flutter:
Kivy:
Недавно на просторах Ютуба наткнулся на видео демонстрацию 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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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.dartSPLimport '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. Извините, данный ресурс не поддреживается. :( =========== Источник: habr.com =========== Похожие новости:
Разработка под MacOS ), #_razrabotka_pod_linux ( Разработка под Linux ), #_razrabotka_pod_windows ( Разработка под Windows ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:47
Часовой пояс: UTC + 5