跳到主要内容

玩转动画

· 阅读需 3 分钟
Feodor Fitsner
Flet创始人兼开发者

尽管Flet的动画支持发布已有一段时间,但我们刚刚完成了其新功能的文档编写!我们都知道,如果功能没有文档记录,就等于不存在!😉

Flutter提供了多种方法来创建动画,例如“隐式”、“显式”、“补间”、“分段”、“预设”动画以及显示在Rive和Lottie编辑器中准备的动画场景。

我们将从“隐式动画”开始,它允许您通过设置目标值来实现对控件属性的动画效果;每当目标值发生变化时,控件会将属性从旧值动画过渡到新值。

演示时间

查看演示源码。顺便提一下,演示托管在Heroku上,您可以将其用作自己部署的起点。

隐式动画

以下控件属性可以启用隐式动画:

此外,所有Container控件属性现在都可以动画化,并且新增了AnimatedSwitcher控件,用于在旧内容和新内容之间进行动画过渡。

其他新功能

Markdown控件

允许以Markdown格式渲染文本。支持多种扩展:CommonMarkGitHub WebGitHub Flavored

查看更多信息和示例,请参阅Markdown控件文档

URL 启动器

page.launch_url(url)方法允许以编程方式在新浏览器窗口中打开URL,例如:

page.launch_url("https://google.com")

它还可以与Markdown控件很好地配合使用,在markdown文档中打开链接。

键盘快捷键

Page现在包含on_keyboard_event事件处理程序,可全局拦截所有按键。

查看这个简单的使用示例

无障碍改进

我们在文档中增加了无障碍部分,涵盖了屏幕阅读器的语义支持。

ShaderMark控件

一个将着色器生成的遮罩应用于其内容的控件。允许制作出像逐渐淡出图像到下边缘的漂亮效果。

就是这些了!

试试Flet,并告诉我们您的想法!

在新的Flet版本中,我们为您带来了美丽的渐变效果、按钮样式和TextField圆角

· 阅读需 5 分钟
Feodor Fitsner
Flet 创始人兼开发者

我们刚刚发布了 Flet 0.1.46,增加了令人兴奋的新功能:

  • Container 的渐变背景
  • 按钮、TextField 和 Dropdown 控件进行了全面的样式设置
  • ...等等

渐变背景

线性渐变

import math
import flet as ft

def main(page: ft.Page):

page.add(
ft.Container(
alignment=ft.alignment.center,
gradient=ft.LinearGradient(
begin=ft.alignment.top_left,
end=Alignment(0.8, 1),
colors=[
"0xff1f005c",
"0xff5b0060",
"0xff870160",
"0xffac255e",
"0xffca485c",
"0xffe16b5c",
"0xfff39060",
"0xffffb56b",
],
tile_mode=ft.GradientTileMode.MIRROR,
rotation=math.pi / 3,
),
width=150,
height=150,
border_radius=5,
)
)

ft.app(target=main)

查看 LinearGradient 文档以获取更多关于 LinearGradient 属性的信息。

径向渐变

import flet as ft

def main(page: ft.Page):

page.add(
ft.Container(
alignment=ft.alignment.center,
gradient=ft.RadialGradient(
center=Alignment(0.7, -0.6),
radius=0.2,
colors=[
"0xFFFFFF00", # 黄色太阳
"0xFF0099FF", # 蓝色天空
],
stops=[0.4, 1.0],
),
width=150,
height=150,
border_radius=5,
)
)

ft.app(target=main)

查看 RadialGradient 文档以获取更多关于 RadialGradient 属性的信息。

扫描渐变

import math
import flet as ft

def main(page: ft.Page):

page.add(
ft.Container(
alignment=ft.alignment.center,
gradient=SweepGradient(
center=ft.alignment.center,
start_angle=0.0,
end_angle=math.pi * 2,
colors=[
"0xFF4285F4",
"0xFF34A853",
"0xFFFBBC05",
"0xFFEA4335",
"0xFF4285F4",
],
stops=[0.0, 0.25, 0.5, 0.75, 1.0],
),
width=150,
height=150,
border_radius=5,
)
)

ft.app(target=main)

查看 SweepGradient 文档以获取更多关于 SweepGradient 属性的信息。

按钮样式

此次 Flet 版本引入了 style 属性到所有按钮控件中,它是 ButtonStyle 类的一个实例。 ButtonStyle 允许控制按钮的所有视觉方面,例如形状、前景色、背景色和阴影颜色、内容填充、边框宽度和半径!

此外,每个单独的样式属性都可以针对按钮的不同“材料状态”进行配置,例如“悬停”、“聚焦”、“禁用”等。例如,您可以为悬停状态配置不同的形状、背景颜色,并为所有其他状态配置回退值。

查看这个“极限”样式示例:

import flet as ft
from flet.border import BorderSide
from flet.buttons import RoundedRectangleBorder

def main(page: ft.Page):

page.add(
ft.ElevatedButton(
"Styled button 1",
style=ft.ButtonStyle(
color={
ft.MaterialState.HOVERED: ft.colors.WHITE,
ft.MaterialState.FOCUSED: ft.colors.BLUE,
ft.MaterialState.DEFAULT: ft.colors.BLACK,
},
bgcolor={ft.MaterialState.FOCUSED: ft.colors.PINK_200, "": ft.colors.YELLOW},
padding={ft.MaterialState.HOVERED: 20},
overlay_color=ft.colors.TRANSPARENT,
elevation={"pressed": 0, "": 1},
animation_duration=500,
side={
ft.MaterialState.DEFAULT: BorderSide(1, ft.colors.BLUE),
ft.MaterialState.HOVERED: BorderSide(2, ft.colors.BLUE),
},
shape={
ft.MaterialState.HOVERED: RoundedRectangleBorder(radius=20),
ft.MaterialState.DEFAULT: RoundedRectangleBorder(radius=2),
},
),
)
)

ft.app(target=main)

ft.MaterialState.DEFAULT 状态是一个备用样式。

按钮的形状也可以通过 ButtonStyle.shape 属性进行更改:

import flet as ft
from flet.buttons import (
BeveledRectangleBorder,
CircleBorder,
ContinuousRectangleBorder,
RoundedRectangleBorder,
StadiumBorder,
)

def main(page: ft.Page):
page.padding = 30
page.spacing = 30
page.add(
ft.FilledButton(
"Stadium",
style=ft.ButtonStyle(
shape=ft.StadiumBorder(),
),
),
ft.FilledButton(
"Rounded rectangle",
style=ft.ButtonStyle(
shape=ft.RoundedRectangleBorder(radius=10),
),
),
ft.FilledButton(
"Continuous rectangle",
style=ft.ButtonStyle(
shape=ft.ContinuousRectangleBorder(radius=30),
),
),
ft.FilledButton(
"Beveled rectangle",
style=ft.ButtonStyle(
shape=ft.BeveledRectangleBorder(radius=10),
),
),
ft.FilledButton(
"Circle",
style=ft.ButtonStyle(shape=ft.CircleBorder(), padding=30),
),
)

ft.app(target=main)

请查看ElevatedButton.style属性文档,了解ButtonStyle类及其属性的完整描述。

TextField 和 Dropdown 样式

现在可以为 TextFieldDropdown 控件的正常和聚焦状态配置文字大小、边框样式和圆角半径。TextField 还允许配置光标和选择的颜色。

此外,现在可以使用max_length属性限制输入到TextField中的最大长度。

我们还引入了capitalization属性,用于在将字符键入TextField时自动进行大小写转换。您可以从4种大小写转换策略中选择:none(默认值)、characterswordssentences

以下是一个带有max_lengthcapitalization的样式化TextField示例:

import flet as ft

def main(page: ft.Page):
page.padding=50
page.add(
ft.TextField(
text_size=30,
cursor_color=ft.colors.RED,
selection_color=ft.colors.YELLOW,
color=ft.colors.PINK,
bgcolor=ft.colors.BLACK26,
filled=True,
focused_color=ft.colors.GREEN,
focused_bgcolor=ft.colors.CYAN_200,
border_radius=30,
border_color=ft.colors.GREEN_800,
focused_border_color=ft.colors.GREEN_ACCENT_400,
max_length=20,
capitalization="characters",
)
)

ft.app(target=main)

下面是一个样式化 Dropdown 控件的示例:

import flet as ft

def main(page: ft.Page):
page.padding=50
page.add(
ft.Dropdown(
options=[
ft.dropdown.Option("a", "Item A"),
ft.dropdown.Option("b", "Item B"),
ft.dropdown.Option("c", "Item C"),
],
border_radius=30,
filled=True,
border_color=ft.colors.TRANSPARENT,
bgcolor=ft.colors.BLACK12,
focused_bgcolor=ft.colors.BLUE_100,
)
)

ft.app(target=main)

其他更改

IconButton 增加了 selected 状态,并与新的 style 配合使用。

这是一个切换图标按钮的示例:

import flet as ft

def main(page: ft.Page):

def toggle_icon_button(e):
e.control.selected = not e.control.selected
e.control.update()

page.add(
ft.IconButton(
icon=ft.icons.BATTERY_1_BAR,
selected_icon=ft.icons.BATTERY_FULL,
on_click=toggle_icon_button,
selected=False,
style=ft.ButtonStyle(color={"selected": ft.colors.GREEN, "": ft.colors.RED}),
)
)

ft.app(target=main)

试试 Flet告诉我们 你的想法!

控件引用

· 阅读需 3 分钟
Feodor Fitsner
Flet 创始人和开发者

Flet 控件是对象,要访问它们的属性,我们需要保留对这些对象的引用(变量)。

请看下面的示例:

import flet as ft

def main(page):

first_name = ft.TextField(label="First name", autofocus=True)
last_name = ft.TextField(label="Last name")
greetings = ft.Column()

def btn_click(e):
greetings.controls.append(ft.Text(f"Hello, {first_name.value} {last_name.value}!"))
first_name.value = ""
last_name.value = ""
page.update()
first_name.focus()

page.add(
first_name,
last_name,
ft.ElevatedButton("Say hello!", on_click=btn_click),
greetings,
)

ft.app(target=main)

main() 方法的一开始,我们创建了三个控件,这些控件将在按钮的 on_click 处理程序中使用:两个用于输入名字和姓氏的 TextField,以及一个用于显示问候信息的 Column。我们创建控件并设置其所有属性,在 main() 方法的最后,通过 page.add() 调用,我们使用它们的引用(变量)。

当添加越来越多的控件和事件处理程序时,将所有控件定义放在一个地方变得具有挑战性,因此它们会散布在 main() 的主体中。浏览 page.add() 的参数时,很难想象(不用不断跳转到 IDE 中的变量定义)最终的表单会是什么样子:

    page.add(
first_name,
last_name,
ft.ElevatedButton("Say hello!", on_click=btn_click),
greetings,
)

first_name 是一个 TextField 吗?它是否设置了自动聚焦(autofocus)?greetingsRow 还是 Column

Ref

Flet 提供了 Ref 实用程序类,它允许定义对控件的引用,在事件处理程序中使用该引用,并在构建树时将引用设置为实际控件。这个想法来自于 React

要定义一个新的类型控件引用:

first_name = ft.Ref[ft.TextField]()

要访问引用的控件(控件解引用),请使用 Ref.current 属性:

# 清空名字
first_name.current.value = ""

要将控件分配给引用,请将 Control.ref 属性设置为引用:

page.add(
ft.TextField(ref=first_name, label="First name", autofocus=True)
)
备注

所有 Flet 控件都有 ref 属性。

我们可以重写程序以使用引用:

import flet as ft


def main(page):

first_name = ft.Ref[ft.TextField]()
last_name = ft.Ref[ft.TextField]()
greetings = ft.Ref[ft.Column]()

def btn_click(e):
greetings.current.controls.append(
ft.Text(f"Hello, {first_name.current.value} {last_name.current.value}!")
)
first_name.current.value = ""
last_name.current.value = ""
page.update()
first_name.current.focus()

page.add(
ft.TextField(ref=first_name, label="First name", autofocus=True),
ft.TextField(ref=last_name, label="Last name"),
ft.ElevatedButton("Say hello!", on_click=btn_click),
ft.Column(ref=greetings),
)

ft.app(target=main)

现在我们可以在 page.add() 中清楚地看到页面的结构以及它包含的所有控件。

是的,逻辑变得有点冗长,因为需要添加 .current. 来访问引用的控件,但这只是个人喜好的问题 :)

尝试 Flet告诉我们 你的想法!

Flet 移动战略

· 阅读需 5 分钟
Feodor Fitsner
Flet 创始人和开发者

Flet 项目最近受到了很多关注,我们要感谢所有尝试 Flet 并在社区中传播它的开发者!你们的支持激励我们以更快的步伐推动 Flet 项目向前发展!

新的 Flet 开发者经常问是否有办法将 Flet 程序打包成 .apk 文件以部署到 Android 设备或 .ipa 文件以部署到 iOS。

在这篇文章中,我想分享我们关于 Flet 移动化的愿景,并提供一个路线图。

服务驱动 UI

Flet 是一个服务驱动 UI (SDUI) 框架。SDUI 是一种新兴技术,在 Technology Radar 的文章 中有最佳描述:

服务驱动 UI 将渲染分离到移动应用中的通用容器中,而每个视图的结构和数据则由服务器提供。这意味着曾经需要往返应用商店的更改现在可以通过简单更改服务器发送的响应来完成。

DoorDashAirbnbLyft 和其他公司已经成功地在他们的移动应用中实现了服务驱动 UI,以缩短上市时间。

Flet 方法

Flet 将实现服务驱动 UI 方法,在服务器上运行用 Python 或其他语言编写的程序,客户端只需一个轻量级客户端 - 独立的 Flutter 应用程序(应用商店中的 .apk.ipa 包)或作为其他应用的一部分的 Flutter 小部件 - 传送到移动设备:

一旦 SDUI 体验准备就绪,我们将开始开发 独立移动包

路线图

为了在移动平台上提供最佳的 Flet 应用体验,我们计划在今年年底前发布以下项目:

用于 Flutter 的 Flet 小部件

我们的第一步是将 Flet 客户端分离为一个 Flutter 小部件,并在 https://pub.dev 上发布该包。然后,移动开发人员可以将 Flet 小部件集成到现有或新的 Flutter 应用中,以为核心应用功能添加动态服务驱动的 UI 体验。也可以创建一个新的 Flutter 应用,仅使用一个 Flet 小部件来托管完整的 Flet 应用。

开发者将按照 Flutter 指南将他们的应用打包、签名和分发到 AndroidiOSLinuxmacOSWindows 平台。

Flet 团队将提供示例 CI 流水线以自动化 Flutter 应用的打包、签名和发布。

用于 iOS 和 Android 的 Flet Studio

下一步是在 App Store 和 Google Play 上发布一个独立“Flet Studio”应用(名称未最终确定),用于“测试使用 Flet 框架开发的移动体验”。开发人员或测试人员将能够在 Flet Studio 中“注册”他们托管的 Flet 应用的 URL,并立即查看其在移动设备上的性能表现。

白标 Flet 移动应用

我们将提供指南和 CI 流水线,自动将白标 Flet 应用发布到用户的 App Store 或 Google Play 账户。该应用将“固定”到特定的应用 URL,并可以额外捆绑应用资产(媒体、字体),以最小化网络使用。

Flet应用的独立移动包

我们将研究并开发一个将 Flet 框架、用户程序、语言运行时和所有依赖项打包到一起的独立移动包(.apk.ipa 包)的,以便 Flet 程序不需要 Web 服务器。

嵌入 Flet 到原生应用

我们将提供指南、示例应用和 CI 流水线,以使用 Flutter Add-to-App 功能将 Flet 小部件集成到现有的原生 Android 和 iOS 应用(不是用 Flutter 开发的)中。Put Flutter to work 文章提供了一个关于如何将 Flutter 集成到现有移动应用中的实际例子。

这是当前的计划。

与此同时,试试 Flet,并 告诉我们 你的想法!

导航与路由

· 阅读需 7 分钟
Feodor Fitsner
Flet 创始人和开发者

Flet 0.1.42 已发布,带来了导航和路由功能!

导航和路由是单页应用程序(SPA)的一个重要功能,它允许将应用程序用户界面组织成虚拟页面(视图),并在它们之间“导航”,同时应用程序的URL反映了当前的应用状态。

对于移动应用,导航和路由还可以作为深层链接到特定的应用部分。

为了在 Flet 中添加导航和路由,我们付出了更多努力 ,因为实现是基于Navigator 2.0 Flutter API的,并需要用“页面和视图”取代 Flet 的“页面”抽象。Flutter 的新导航和路由 API 有以下显著改进:

  1. 对历史堆栈的编程控制。
  2. 一种简便的方法来拦截 AppBar 中的“后退”按钮调用。
  3. 与浏览器历史的强同步。

浏览示例源代码

页面路由

页面路由是应用程序URL中 # 符号之后的部分:

如果用户未在应用程序URL中设置默认应用程序路由,则默认为 /。所有路由都以 / 开头,例如 /store, /authors/1/books/2

可以通过读取 page.route 属性获取应用程序路由,例如:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(f"Initial route: {page.route}"))

ft.app(target=main, view=ft.AppView.WEB_BROWSER)

获取应用程序URL,打开一个新的浏览器标签页,粘贴URL,修改 # 后的部分为 /test,然后按回车。你应该会看到“Initial route: /test”。

每次URL中的路由发生变化(通过编辑URL或使用浏览器历史中的后退/前进按钮),Flet 都会调用 page.on_route_change 事件处理程序:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(f"Initial route: {page.route}"))

def route_change(route):
page.add(ft.Text(f"New route: {route}"))

page.on_route_change = route_change
page.update()

ft.app(target=main, view=ft.AppView.WEB_BROWSER)

现在尝试多次更新URL哈希,然后使用后退/前进按钮!每次路由变化时,你应该会看到页面上添加了一条新消息:

可以通过更新 page.route 属性以编程方式更改路由:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(f"Initial route: {page.route}"))

def route_change(route):
page.add(ft.Text(f"New route: {route}"))

def go_store(e):
page.route = "/store"
page.update()

page.on_route_change = route_change
page.add(ft.ElevatedButton("Go to Store", on_click=go_store))

ft.app(target=main, view=ft.AppView.WEB_BROWSER)

点击“Go to Store”按钮,你会看到应用程序URL已更改,并且在浏览器历史中添加了一个新项。你可以使用浏览器“后退”按钮导航到上一个路由。

页面视图

Flet 的 Page 现在不仅仅是一个单独的页面,而是一个包含叠加在一起的 View 的容器,像三明治一起:

视图的集合代表了导航器历史。Page具有 page.views 属性来访问视图集合。

列表中的最后一个视图是当前显示在页面上的视图。视图列表必须至少有一个元素(根视图)。

要模拟页面之间的过渡,请更改 page.route 并在 page.view 列表的末尾添加一个新 View

从集合中弹出最后一个视图并在 page.on_view_pop 事件处理程序中将路由更改为“上一个”路由以返回。

在路由变化时构建视图

为了构建可靠的导航,程序中必须有一个地方根据当前路由来构建视图列表。换句话说,导航历史堆栈(由视图列表表示)必须是路由的函数。

这个地方是 page.on_route_change 事件处理程序。

让我们将所有内容放在一起,完成一个允许在两个页面之间导航的完整示例:

import flet as ft

def main(page: ft.Page):
page.title = "Routes Example"

def route_change(route):
page.views.clear()
page.views.append(
ft.View(
"/",
[
ft.AppBar(title=ft.Text("Flet app"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.ElevatedButton("Visit Store", on_click=lambda _: page.go("/store")),
],
)
)
if page.route == "/store":
page.views.append(
ft.View(
"/store",
[
ft.AppBar(title=ft.Text("Store"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.ElevatedButton("Go Home", on_click=lambda _: page.go("/")),
],
)
)
page.update()

def view_pop(view):
page.views.pop()
top_view = page.views[-1]
page.go(top_view.route)

page.on_route_change = route_change
page.on_view_pop = view_pop
page.go(page.route)


ft.app(target=main, view=ft.AppView.WEB_BROWSER)

试试使用“Visit Store”和“Go Home”按钮在页面之间导航,使用浏览器的后退/前进按钮,手动更改URL中的路由——无论怎么做都能正常工作! :)

备注

要在页面之间“导航”,我们使用了 page.go(route) - 一个辅助方法,它更新 page.route,调用 page.on_route_change 事件处理程序来更新视图,最后调用 page.update()

注意 page.on_view_pop 事件处理程序的使用。当用户点击 AppBar 控件中的自动“后退”按钮时,它会触发。在处理程序中,我们从视图集合中删除最后一个元素,并导航到其下的视图根。

路由模板

Flet 提供 TemplateRoute——一个基于 repath 库的实用类,它允许匹配类似于 ExpressJS 的路由并解析其参数,例如 /account/:account_id/orders/:order_id

TemplateRoute 与路由更改事件非常搭配:

troute = TemplateRoute(page.route)

if troute.match("/books/:id"):
print("Book view ID:", troute.id)
elif troute.match("/account/:account_id/orders/:order_id"):
print("Account:", troute.account_id, "Order:", troute.order_id)
else:
print("Unknown route")

你可以在这里阅读更多关于 repath 库支持的模板语法。

今天就到这里!

尝试 Flet告诉我们你的想法吧!