{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Kivy指南-1-时钟app\n", "\n", "> 一个仿iOS和Android内置时钟应用的app\n", "\n", "- toc: true \n", "- badges: true\n", "- comments: true\n", "- categories: [jupyter,Kivy,Android,iOS]\n", "- image: kbpic/1.1clockapp.png" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "一个仿iOS和Android内置时钟应用的app。分两部分:\n", "1. 个没有交互的数字时钟,简述Kivy的事件驱动(event-driven)方法,引入计时器的功能,持续更新。\n", "2. 交互的秒表功能,设计流畅的自适应布局。\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "学习大纲:\n", "* Kivy语言基础,DSL(domain-specific language)处理部件(widgets)\n", "* Kivy布局方式\n", "* 自定义字体和文字样式\n", "* 事件管理\n", "\n", "app最终效果如下,只要60行代码,Python代码和kv代码各一半。\n", "\n", "![clockapp](kbpic/1.1clockapp.png \"clockapp\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 起点" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "将kivy的`helloworld`稍作修改。增加一个布局容器(layout container),`BoxLayout`,后面可增加更多部件。" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# %load ../0_Hello/main.py\n", "from kivy.app import App\n", "\n", "\n", "class ClockApp(App):\n", " pass\n", "\n", "\n", "if __name__ == \"__main__\":\n", " ClockApp().run()" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "```yaml\n", "# clock.kv\n", "BoxLayout:\n", " orientation: 'vertical'\n", "\n", " Label:\n", " text: '00:00:00'\n", "``` " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`BoxLayout`容器可以包含多个子部件,水平或垂直堆放。由于`kv`只有一个子部件,`BoxLayout`就会让它充满所有空间。\n", "> 当运行`main.py`文件时,Kivy自动调用`clock.kv`。类名是`ClockApp`,`.kv`文件名就是`clock`,类名小写并去掉`App`。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 新UI" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "扁平化设计模式(flat design paradigm)如日中天,覆盖Web,移动,桌面应用领域,兴起于iOS7和Win8。互联网公司也追随,于Google I/O 2014出Material design,其他HTML5框架,如Bootstrap亦如是。\n", "\n", "扁平化设计强调内容胜于外观,忽略逼真图片的阴影和细致的质地,支持纯色和简单几何图形。强调比学院派的仿真设计(skeuomorphic design)更简单的程序化创造,前者倾向于丰富视觉效果和艺术感。\n", "\n", ">仿真主义是用户界面设计的主流方法。认为应用程序属于真实世界的一部分,比如一个带按钮的计算器app应该被做成廉价的、物质的计算器的感觉,有助于提升用户体验(得看是谁用)。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "如今,放弃视觉细节而转向简单、流线型界面仿佛是共识。另一方面,仅靠一堆彩色框框就想做成惊世骇俗的作品很有难度。扁平化设计成了文字排版好的代名词原因就是文字成了UI设计中重要的部分,所有我们要让文字好看。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 设计灵感" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "模仿Android 4.1 Jelly Bean的时钟设计。字体是Google的[Roboto](http://www.fontsquirrel.com/fonts/roboto)字体,取代了Android 4.0 Ice Cream Sandwich的Droid字体。\n", "\n", "![clockui](kbpic/1.2android4.1clockUI.png \"clockui\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 加载自定义字体" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Kivy默认是Droid Sans字体,通过`font_name`属性可设置自定义字体。这里只有一种字体,可以直接将`.ttf`文件名放上。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```yaml\n", "# clock.kv\n", "Label:\n", " font_name: 'Loster.ttf'\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "但是我们要好几种字体,一个属性就不够了。因为不同字体都是单个文件,而属性只能跟一个文件名。涉及多种字体可以用`LabelBase.register`方法可以接受多种字体,如下所示:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "LabelBase.register(\n", " name=\"Roboto\",\n", " fn_regular=\"Roboto-Regular.ttf\",\n", " fn_bold=\"Roboto-Bold.ttf\",\n", " fn_italic=\"Roboto-Italic.ttf\",\n", " fn_bolditalic=\"Roboto-BoldItalic.ttf\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "改进之后,一个部件的`font_name`属性可设置多种自定义字体了。但这种方法有两个限制:\n", "1. kivy只接受TrueType的`.ttf`字体。如果是OpenType的`.otf`或者网页字体如`.woff`,得先[转换](http://fontforge.org/)。\n", "2. 字体normal,italic,bold,bold italic四种样式有最大值。旧字体没问题,如Droid Sans。但是新字体都有4到20多种样式,其高度和其他特征也不同。Roboto至少有12种样式。\n", "\n", "第二点迫使我们选择app字体时要把12种样式全放进去,这么做会增大app的体积,Roboto字体有1.7M。\n", "\n", "本例中我们只要两种样式:浅色(`Roboto-Thin.ttf`)和加粗(`Roboto-Medium.ttf`)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from kivy.core.text import LabelBase\n", "\n", "LabelBase.register(\n", " name=\"Roboto\", fn_regular=\"Roboto-Thin.ttf\", fn_bold=\"Roboto-Medium.ttf\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "下面我们来使用字体,放到`Label`后面即可。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```yaml\n", "# clock.kv\n", "Label:\n", " text: '00:00:00'\n", " font_name: 'Roboto'\n", " font_size: 60\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 字体格式" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "markup语言毋庸置疑HTML。Kivy实现了另外一种BBCode的markup语言,用[]作标签。\n", "\n", "| BBCode tag |Effect on text |\n", "| :-------------: |:-------------:|\n", "| [b]...[/b] | **Effect on text**|\n", "| [i]...[/i] | *Italic*|\n", "| [font=Lobster]...[/font] | Change font|\n", "| [color=#FF0000]...[/color] | Set color with CSS-like syntax|\n", "| [sub]...[/sub] | Subscript (text below the line)|\n", "| [sup]...[/sup] | Superscript (text above the line)|\n", "| [ref=name]...[/ref] | Clickable zone, `` in HTML|\n", "| [anchor=name] | Named location, `` in HTML|\n", "\n", ">由于Kivy发展很快,以上内容绝非最终版本,详情查阅[kivy文档](http://kivy.org)。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "再看看图2,我们要实现小时数字加粗的效果就easy了。\n", "\n", "```yaml\n", "# clock.kv\n", "Label:\n", " text: '[b]00[/b]:00:00'\n", " markup: True\n", "```\n", "\n", "Kivy的BBCode需要将markup属性设置为True。\n", ">如果要整行加粗,可以直接设bold属性为True。其他斜体、颜色、字体、大小同理。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 改变背景色" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "下面我们来调整窗口背景色,是`Window`对象的一个属性。可以在`__name__ == '__main__'`后面增加代码:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from kivy.core.window import Window\n", "from kivy.utils import get_color_from_hex\n", "\n", "Window.clearcolor = get_color_from_hex(\"#101216\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "函数`get_color_from_hex`允许使用[CSS的RGB颜色值(`#RRGGBB`)](https://\n", "developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Color),也可以用其他函数。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 显示时间" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "大多数UI框架都是事件驱动,Kivy也不例外。这种方式相比通常的程序更简单——事件驱动的代码需要不断返回到主循环(`main loop`);但是,这么做不能处理用户行为(点击鼠标,改变窗口),而且界面会冻结(`freeze`),Windows经常这样`程序停止响应`。\n", "\n", "总之,不能在程序里面加无限循环实现。" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Don't do this\n", "while True:\n", " update_time() # some function that displays time\n", " sleep(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "理论上可行,但UI实际会失去相应,直到系统或用户关闭进程才结束。记住Kivy内部一直运行主循环,我们可以通过事件与计算器来利用它。\n", "\n", "事件驱动还意味着我们需要对不同事件作出响应,可能是用户输入,网络行为,或超时等等。\n", "\n", "很多程序监听共同事件之一就是`App.on_start`,定义在类里面,在app初始化的时候调用。另一个常见的是`on_press`,当用户点击,tap,或其他按钮操作时启用。\n", "\n", "通过时间和计时器,我们就可以用Kivy自带的Clock类实现想要的功能。两个方法:\n", "* `Clock.schedule_once`:在一段时间后运行一次\n", "* `Clock.schedule_interval`:周期性的运行\n", "\n", "> 和JavaScript中的`window.setTimeout`和`window.setInterval`类似。其实Kivy和JS很像,即使API完全不同。\n", "\n", "`Clock`所有的计时事件都是Kivy主循环的一部分。这种方法与线程不同,这样调用一个阻塞函数可能会阻止其他事件被及时唤醒。" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "### 更新屏幕上的时间" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "要接入显示时间的`Label`部件,需要给它一个`id`,通过`id`属性来获取部件,这和Web开发类似。\n", "\n", "```yaml\n", "# clock.kv\n", "Label:\n", " id: time\n", "```\n", "之后就可以通过`root.ids.time`来接入`Label`部件了。这里`root`就是`BoxLayout`。\n", "\n", "给`ClockApp`类增加一个`update_time`方法来更新时间:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def update_time(self, nap):\n", " self.root.ids.time.text = strftime(\"[b]%H[/b]:%M:%S\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "再增加一个调度功能,让程序更新后每秒更新一次:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def on_start(self):\n", " Clock.schedule_interval(self.update_time, 1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "运行程序看看是不是开始更新了。代码如下:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# %load main.py\n", "from kivy.app import App\n", "from kivy.clock import Clock\n", "from kivy.core.window import Window\n", "from kivy.utils import get_color_from_hex\n", "\n", "from time import strftime\n", "\n", "\n", "class ClockApp(App):\n", " def update_time(self, nap):\n", " self.root.ids.time.text = strftime(\"[b]%H[/b]:%M:%S\")\n", "\n", " def on_start(self):\n", " Clock.schedule_interval(self.update_time, 1)\n", "\n", "\n", "if __name__ == \"__main__\":\n", "\n", " Window.clearcolor = get_color_from_hex(\"#301216\")\n", " ClockApp().run()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "看看python的`time`标准库`strftime`函数是如何与Kivy的BBCode组合成C语言字符串的。\n", "\n", "```yaml\n", "# %load clock.kv\n", "BoxLayout:\n", " orientation: 'vertical'\n", "\n", " Label:\n", " text: '[b]00[/b]:00:00'\n", " markup: True\n", " id: time\n", "``` " ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "### 用属性绑定部件" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "除了ID绑定部件,还可以新建一个属性,在kv文件中进行绑定。这么做更符合DRY原则,只是多几行代码。如下所示:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# In main.py\n", "from kivy.properties import ObjectProperty\n", "from kivy.uix.boxlayout import BoxLayout\n", "\n", "\n", "class ClockLayout(BoxLayout):\n", " time_prop = ObjectProperty(None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "我们在这段代码用`BoxLayout`写了个新root部件类,它有一个自定义属性`time_prop`,将连接`Label`部件。\n", "\n", "在`clock.kv`文件里,我们把属性绑定`id`,自定义属性和默认属性语法一致:\n", "\n", "```yaml\n", "# %load clock.kv\n", "ClockLayout:\n", " time_prop: time\n", " \n", " Label:\n", " id: time\n", "```\n", "\n", "这样,Python代码不需要知道`id`就可以连接`Label`部件,用新属性`root.time_prop.text = \"demo\"`。\n", "\n", "这样做使代码的可移植性更好,消除了反射(refactor)时Python代码同步的问题。靠属性还是`root.ids`去连接Python代码这事儿,只是代码风格问题,不重要。后面还会介绍其他Kivy属性的用法,让数据绑定更容易。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 布局基础" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Kivy提供了一堆`Layout`类来布局。`Layout`又是`Widget`类的子类,是个容器类。每个布局都是影响其子类位置和尺寸。\n", "\n", "在这个app中,我们的UI很直接,不需要什么神奇,如下所示:\n", "\n", "![layout](kbpic/1.3layout.png \"layout\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "做这种界面就要`BoxLayout`,一种一维网格。在`clock.kv`里面已经有`BoxLayout`了,只有一个子部件。Kivy的布局默认充满屏幕,所以自动适应屏幕。\n", "\n", "如果增加一个`Layout`,就会分一半屏幕,`vertical`和`horizontal`决定分割的方向。\n", "\n", "我们这里就用`vertical`分三块,然后中间那块用`horizontal`分两块,Esay吧。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 完成布局" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "由于中间这块是按钮,不应该比时间还大,可以增加一个`height`属性,然后设置`size_hint`属性为`None`。`size_hint`属性是一个元组`(宽, 高)`,影响部件的宽和高。如果你想用绝对高度和宽度,就要设置`size_hint`属性为`None`,否则高度和宽度设置无效,部件会自动计算尺寸。代码如下:\n", "\n", "```yaml\n", "# %load clock.kv\n", "BoxLayout:\n", " orientation: 'vertical'\n", " Label:\n", " id: time\n", " text: '[b]00[/b]:00:00'\n", " font_name: 'Roboto'\n", " font_size: 60\n", " markup: True\n", " BoxLayout:\n", " height: 90\n", " orientation: 'horizontal'\n", " padding: 20\n", " spacing: 20\n", " size_hint: (1, None)\n", " Button:\n", " text: 'Start'\n", " font_name: 'Roboto'\n", " font_size: 25\n", " bold: True\n", " Button:\n", " text: 'Reset'\n", " font_name: 'Roboto'\n", " font_size: 25\n", " bold: True\n", " Label:\n", " id: stopwatch\n", " text: '00:00.[size=40]00[/size]'\n", " font_name: 'Roboto'\n", " font_size: 60\n", " markup: True\n", "```\n", "运行代码,会发现按钮没有完全填充`BoxLayout`,因为用了`padding`和`spacing`属性,与CSS类似。`main.py`代码如下:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# %load main.py\n", "from kivy.app import App\n", "from kivy.clock import Clock\n", "from kivy.core.window import Window\n", "from kivy.utils import get_color_from_hex\n", "from kivy.core.text import LabelBase\n", "\n", "from time import strftime\n", "\n", "LabelBase.register(\n", " name=\"Roboto\", fn_regular=\"Roboto-Thin.ttf\", fn_bold=\"Roboto-Medium.ttf\"\n", ")\n", "\n", "\n", "class ClockApp(App):\n", " def update_time(self, nap):\n", " self.root.ids.time.text = strftime(\"[b]%H[/b]:%M:%S\")\n", "\n", " def on_start(self):\n", " Clock.schedule_interval(self.update_time, 1)\n", "\n", "\n", "if __name__ == \"__main__\":\n", "\n", " Window.clearcolor = get_color_from_hex(\"#123456\")\n", " ClockApp().run()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 减少重复" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "之前的kv代码一堆重复,其实可以借助CSS的方法是代码更精炼,更DRY。在`BoxLayout`外面增加一个新定义:\n", "\n", "```yaml\n", "# %load clock.kv\n", "