3-Android录音app

在上一章,我们简要介绍过Kivy要实现跨平台应用,可能在不同的平台需要选择不同的代码,为一些用户增强体验效果,实现具体平台的任务。

有时,这些都很简单;比如,如果Kivy发现目标系统支持它,多点触控就会启动——不需要写任何代码,但是要考虑一些点击事件原来的功能可能会与多点触控产生冲突。

另外一些平台相关的任务,像代码不能在其他系统上运行,是有很多原因造成的。还记得画图app的鼠标光标吗?代码要用Pygame封装的底层SDL光标功能,如果你熟悉SDL和Pygame那就很简单。因此,为了让app可以跨平台,我们要尽量避免在系统兼容性不好的代码;因为那样可能导致程序崩溃。

然而,Kivy应用具有良好的平台兼容性——Mac,Windows,Linux,iOS,Android和Raspberry Pi——都没什么大问题。

教学大纲:

  • 通过Pyjnius实现Python与Java的交互
  • 在Android系统设备上测试Kivy应用
  • 用Python调用Android的声音API,允许我们记录和播放声频文件
  • 制作一个紧凑型用户界面,类似Windows Phone
  • 用图标字体改进app矢量图标的显示

平台相关代码

这本书绝大多数app都是平台无关代码,因为Kivy具有高度移植性。但这一次我们做一个仅支持Android平台的应用。这么做肯定会减少我们的用户,但是它能让我们接触到一些具体平台功能的处理方法。

这种需求可以实现,是立足于Kivy不断努力支持多个平台,使得用户在不同平台上具有类似的体验。因此,我们可以真正简单的做到一次编写,处处运行。

但是,要实现跨平台,你就要用每个系统支持的功能。不同系统功能的最大公约数集合包括屏幕可以显示图像,如果有声卡就获取声音,接受用户的输入等等。

每个Kivy应用,本质上都基于Python,还支持Python的标准模块。可以利用网络编程,支持大量的协议操作,还提供很多通用性的算法和功能。

还有就是在大多数平台上,纯kivy程序的IO能力会受到限制,通用计算机系统的一小部分都是这样,像智能手机和平板电脑。

让我们看看现代移动设备的API接口,这里以Android为例。我们把每个API分成两部分:一部分是Python/Kivy支持的,另一部分不是。

Python/Kivy支持的特性如下:

  • 图形硬件加速
  • 支持多点触控输入
  • 播放声音
  • 支持网络

Python/Kivy不支持的特性如下:

  • 调制解调器,语言电话和短信
  • 内置摄像头拍照和录像
  • 内置麦克录音
  • 数据云存储
  • 蓝牙和其他近场通信
  • 位置服务和GPS
  • 指纹识别
  • 传感器类,加速器、陀螺仪
  • 屏幕亮度调节
  • 振动功能
  • 电池充电百分比

    这些不支持的列表里面,不同的Python模块已经支持,像Audiostream可以录音,Plyer可以实现很多功能。 因此,这些特性并非完全不能支持;实际上,这些功能在不同的平台上都是十分碎片化的,即使在Android系统上也没有统一的版本;因此,你写完具体平台的代码后,还是会发现没法儿移植。

从前面的比较中可以看出,Android有一堆功能,只要一部分被Python/Kivy支持。这无疑为你用Kivy开发Android应用留下了大量的自由想象空间。你会学到Python调用Android API的知识,可以让Kivy做任何事情。

另一个优势就是,你可以编写全新的类去支持具体特定硬件的移动设备,包括虚拟现实app,支持的陀螺仪游戏,全景拍摄相机等等。

Pyjnius介绍

要充分利用Android功能,就要用Java写的一堆API。我们要做的录音app,类似于Android和iOS的应用,很简单的功能。不像纯Kivy程序要从头开始,Android API为我们提供一堆录音的功能。

下面我们就通过做录音app来演示Python-Java交互的Pyjnius模块卓越的性能,同样是Kivy开发者的项目。我们要开发的内容很简单——录音,回放功能——你会发现这种交互很简单,不需要一堆错综复杂的细节去实现这点小功能。

Pyjnius最有趣的属性就是它并非在Android上面添加一个层来调用API,而是运行你直接通过Python运行Java。这样你就可以完全使用原生的Android API,可以参考适合Java开发的Android文档,不过不是Python文档。但是,这比没有API文档要好。

我们这里说Pyjnius是用来做Android开发的,其实也可以开发Java桌面应用。这是很有趣的,因为还有一个Java API的Python模块叫Jython,很慢而且不完整。Pyjnius可以让你直接使用CPython,再加上Numpy就可以让程序飞起来。 总之,让你想通过Python用Java,考虑Pyjnius吧。

Android模拟器

这章做的app是要运行在Android上的,不能运行在我们的电脑上,因此我们需要用到Android设备,如果你没有设备,也可以安装Android模拟器。一个方便高效的Android模拟器可以让你事半功倍。

推荐一个模拟器,就是Genymotion,你可以下载一个免费版来用。不同的系统安装方法不同,我们就不提供教程了,自行谷歌之,还是比较简单的。

用虚拟机安装Android模拟器的时候,下面一些建议供参考:

  • 建议保持Android最新版本,向后兼容性比较差;旧版本的系统级别的调试问题没有完全解决。
  • Android社区资源丰富,如果有问题就检索,你遇到的坑别人也踩过。
  • Kivy Launcher app是很不错的测试工具,你可以在官方网站找到apk,建议装到手机上,方便程序调试。
  • 不同的模拟器质量和兼容性层次不齐。如果你发现一次没搞定,建议你换个虚拟机或模拟器试试。

下面这个截图就是Genymotion启动的模拟器,完全支持Kivy Launcher。Genymotion

Metro UI

现在让我们用Window Phone的主屏风格来建立一个用户界面。这些不同大小的矩形彩色网格,被称为Metro UI风格,不过后来更名为Modern UI。我们的app就是要仿这个。metroui

当然,我们并不是要做出这样,只是用一下风格来构建我们的界面。下面是对风格的总结:

  • 每个元素都是一个矩形网格
  • IU元素呈现扁平化特征(第一章讨论过,表面纯色,没有阴影,也没有圆角)
  • 格子可以根据需要变大,方便点击

看起来非常简单吧。其实用Kivy实现起来也很简单。

按钮

现在开始吧,首先做个按钮Button类,就像我们在之前的应用里做的,这里我们重用第二章画图app的按钮:

我们之前设计调色板时,把背景色成设置重白色。这里我们的background_color属性是一个底色,我们分配一个浅白色作为background_color。这次我们不要边框有颜色。

之后就是按下按钮的颜色,background_down属性我们设置成25%透明白色。再在上面加上黑色,就可以获得一个深色的背景:backgroundcolor

网格结构

布局有点小复杂。Kivy没有现成的Modern UI模板可用,我们要自己仿制一个GridLayout部件。它和BoxLayout部件类似,不过是二维的,所以没有orientation: 'horizontal''vertical'属性。

如果没有其他需求,一个GridLayout部件就可以搞定,但是我们还想要不同尺寸的按钮。目前,GridLayout部件不支持通过网格的合并成更大的网格(HTML里面的rowspancolspan属性更方便)。因此,我们换个思路,先用一个GridLayout部件作为根部件,用大网格,然后再在里面增加一个GridLayout部件,用小网格。

因为Kivy可以完美支持嵌套布局,我们可以用下面的kv代码实现recorder.kv文件:

翻翻前面的章节,把main.py也做出来就可以运行了,自己试试吧。注意类的名称是RecorderApp,kv文件名大写,再加App,不清楚参考第一章命名相关内容。

注意嵌套的GridLayout部件是怎么和同级的大按钮放在一起的。如果你记得前面那个WP系统主平面,还得继续改进代码:四个小按钮和一个大按钮一样大。嵌套的GridLayout部件是这些小按钮的容器。

可视化属性

在外面的网格里,padding属性是为了让部件离屏幕的四边有一定边距。把其他GridLayout部件的可视化属性都放到一个类中:

要注意的是paddingspacing属性都是列表,不是数值。spacing[0]是水平间距,之后是垂直间距。但是,我们用一个数值初始化,就像前面的代码显示的;数值可以用在两个方向。

嵌套网格由具有相同间距的两列组成,row_default_height属性是厚度:我们可以说,“让行高等于格子宽度”。但是下面我们手动计算想要的高度,0.5是因为有两列:

行高 = 0.5 × (屏幕宽度 – 所有边距和间距)

如果我们不这么做,网格里面的按钮就会垂直填充全部空间,这不是我们要的效果,尤其不是很多按钮的时候,每个就会显得巨大难看。另外,我们希望所有的按钮都是方块。

下面就是之前代码设计的“Modern UI”界面:uidesign

可缩放的矢量图标

要做好看的应用UI,就得有图标,不只是按钮+文字。当然,我们可以在按钮上加图片,但是更好的办法是有图标字体——后面你会发现这种方法更具柔性。

图标字体

图标字体本质上和普通字体一样,除了它们的符号与文字无关。比如,你输入“P”就可以获得Python的logo,而不是字母P;每个字都是与字母相对于的图标。

使用图标字体的不足——用这些字体的代码很难读——因为文字-图标对应关系不太明显。这点可以通过常量来代替要输入符号。

还有一些字体不使用英文字母,改用Unicode码与图标对应,比如Emoji颜文字。使用这类图标字体的前提是目标平台支持Unicode,这方面不是每个平台都ok,尤其是移动平台。我们这里的app只用ASCII码。

合理使用图标字体

在网页上,字体图标解决了很多图片(栅格图)相关的问题:

  • 首先是图片放大后会失真——虽然有算法可以解决这类问题,但是并不完美。相反图标字体是矢量图,可以无限放大。
  • 栅格图文件包含的示意图(像图标和UI元素)比矢量格式字节空间大。这就显然不能应用到JPEG格式的图片上,会使字节空间更大。
  • 另外,图标字体通常就是一套字体放在一个文件上,就是说一个HTTP请求就可以遍历。普通的图片分别是单个文件,明显增加HTTP请求负担;有一些方法可以改善这点,像CSS sprites可以把多个图片合成一张图改善性能,不过使用不太广泛,而且有些问题。
  • 图标字体里面,颜色可以随意变化,在CSS文件增加color: red即可。尺寸、角度和其他那些普通图片不容易搞定的属性都很容易设置,就好像在位图上操作。

以上这些并不适用于Kivy开发,不过图标字体的使用已经是现代网页开发的范本了,尤其是自大量优质字体开始出现以来——现在有几百种图标字体可用。

最棒的两个免费字体就是Font SquirrelGoogle Fonts。不用介意这些网站的字体是用来做网页开发的,大多数字体都可以离线使用。>最需要注意的是字体文件格式:Kivy只能支持True Type(.ttf)文件格式。好在大多数字体都是这个格式,即使不是也可以格式转换。

Kivy中使用图标字体

我们的app使用由John Caserta设计的Modern Pictograms免费字体。截图如下所示:ModernPictogramsiconfont

要把字体加入Kivy程序,我们可以用第一章时钟app的方法。这里,我们不这么做,因为图标字体字体风格完全不同。还是,通过字体名称连接字体,而不是用字体文件名(modernpics.ttf)连接。这样,你就可以重命名字体文件或者移动文件路径,而不用每次都通篇改一遍。在main.py写如下代码:

这样recorder.kv文件就可以使用图标字体了。首先,我们把Button改进一下,方便后面改变字体。代码如下:

halign: 'center'表示我们希望每行文字都在按钮的正中间。markup: True是必须的,因为我们后面自定义按钮需要这样。

现在,我们来升级所有按钮。这是一个例子:

通常不需要为Kivy文件字符串加括号,这么做是为了显示成多行,这和一行长串是一样的。

注意在[font][size]表情里面的'e',这就是图标代码。每个按钮用一个图标,改变一个图标就是替换recorder.kv文件里面的一个字母。具体对应关系可以在Modern Pictograms字体网站查询。

为了方便浏览图标字体,你需要使用字体浏览器。通常,每个系统都有类似程序。

  • Windows系统使用Character Map
  • Mac系统使用Font Book
  • Linux系统由桌面环境决定,GNOME使用gnome-font-viewer

当然也可以网页搜索。流行字体都有详细的说明。

下面就是我们的界面啦:appui

不错吧,可以Modern UI很像吧。

你可能想知道右上角的小绿图标是干嘛的。它们是为了给不同的设备设置不同的录音质量。另外三个按钮——录音,播放,删除——尚不足以呈现Modern UI的风格,因为它需要更丰富好玩的外观。

在Android上测试

现在,我们的app虽然没有包含任何Android平台相关的代码,但是我们也可以在Android平台上测试。能这么做的前提是Android平台上装了Kivy Launcher。

把app打包成Kivy Launcher支持的程序有点琐碎。我们打算增加两个文件,android.txticon.png,都放在main.pyrecorder.kv同名文件夹里,然后拷贝到SD卡的/Kivy文件夹即可,如下图所示:SDcard当然启动Kivy Launcher,它就会扫描文件目录的完整路径,如果没SD卡还是能找到。

android.txt非常简单:

titleauthor设置后会在程序列表显示出来。orientation可以是portrait(正常显示,高度>宽度)和landscape(水平显示,宽度>高度),由应用布局设计决定。

icon.png文件是可选的,如果没有就是空白图标。建议找个漂亮的,程序的第一印象就是图标。注意icon.png文件名不能改变,否则Kivy Launcher找不到启动位置。

把之前做过的程序都放进去,你就会看到程序列表,如下所示:applicationslist

如果不是这样,建议检查一下文件路径。

现在就可打开程序了。这是在Android上测试Kivy程序最方便的办法——就是简单的复制文件。

用Android的API

已经完成了app的用户界面,下面我们来用Android的API实现录音的功能,要用到的两个Java类是MediaRecorderMediaPlayer

Python和Java都是面向对象语言,看着好像差不多,但是两者对OOP理论的应用大相径庭。与Python相比,很多Java的API都在一坨坨的使用设计模式。所以,你在解决很小的任务时,也需要写一堆废话,就不用觉得奇怪。

在1913年的时候,Vladimir Lenin就写下了对Java架构的评论:

只有一种办法可以粉碎这些“类”的阻力,那就是,在我们的社会中找到能够推陈出新的力量。

这段话没有提及之后的Python和Pyjnius,但是观点明确——即使在一百年前,过度使用类也是不受社会欢迎的。

幸运的是,我们的任何相对简单。要用Android API实现录音,我们只需要下面5个Java类:

  • android.os.Environment:这个类提供了很多有用的环境变量。我们需要用它来确定文件保存的路径。临时存放的位置就是'/sdcard/'或者简单的内容,但是即使不同的设备路径不一样。因此,我们别这么设置路径。
  • android.media.MediaRecorder:这是我们的主力,实现了音频和视频的抓取和保存功能。
  • android.media.MediaRecorder$AudioSource, android.media.MediaRecorder$AudioEncoderandroid.media.MediaRecorder$OutputFormat:这些类列出了我们需要传递到MediaRecorder不同方法里面的参数。

Java类命名规则: 通常类名里面的'$'符号表示内部类。这种方法有点无厘头,因为你可以声明一个相似的类不用后面跟任何东西——'$'在Java变量和类名称中是可用的,与JavaScript里面的没有太多不同。但这种奇葩的命名方法让人无语。

加载Java类

下面的代码就是通过Pyjnius加载Java类:

如果你直接运行代码,可能会出现下面的错误:

  • ImportError: No module named jnius:如果Pyjnius没装
  • jnius.JavaException: Class not found ‘android/os/Environment’:如果需要Android类没加载成功(在桌面系统上Android没装好可能会这样)。

如果遇到其中任何一个错误都说明没配置好。因为代码不再是跨平台的了,让我们到Android设备或者模拟器上测试一下。这个app完全依赖Android相关的Java特性。

下面我们将把Java类与Python代码结合到一起。

记得这些类的定义文档都是Java的,不是Python。你可以看看Google官方的Android开发入门,然后把Java反应成Python代码,看着很恐怖,多试试,其实很简单。

查找保持路径

让我们演示一下Pyjnius这种混合语言使用API的过程。我们还可以通过Java检查SD卡是否已经挂载。

翻译成Python就是:

两者是完全一样的。然后,我们可以通过log查看运行的结果。

调试信息会显示下面这行日志:

[INFO] App: storage path == “/storage/sdcard0”

在设备中看日志

当你在开发阶段运行Kivy应用时,日志会立刻在命令行窗口显示。当你在Kivy Launcher运行代码时,虽然不是很容易,显示日志也是非常有用的特性。

要看到日志,你可以在程序运行的时候到程序目录下(/Kivy/Recorder),里面会生成一个新的.kivy目录,这个目录里面有日志文件.kivy/logs

如果是用Android SDK运行模拟器,可以打开开发者模式的USB调试功能,然后用adb logcat查看所有日志,里面包括Kivy日志。如果你用Android Studio 或Eclipse等IDE,你可以用logcat对日志进行过滤处理。

当调试程序出问题或应用不能启动的时候日志文件是很冗长的。Kivy还打印了各种关于运行环境的警告日志,像缺少库文件或功能,Python模块加载失败,或者其他提示。

录音

现在让我们用Android的API来逐个实现app的功能。现在的代码就是把Android的API翻译成Python。如果你对原始的Java代码感兴趣,你可以去官方网站看文档

下面的代码就是MediaRecorder对象初始化:

这就是把啰七八嗦的Java代码直译成Python的代码。

你可以自定义里面的属性。比如AMR_NB自适应多速率,Adaptive Multi-Rate编解码器,用来做语音优化的,广泛用于GSM和其他移动网络的设备中)改成AudioEncoder.AAC进阶音讯编码,Advanced Audio Coding标准,类似于MP3的一种编码标准)。这么改不是很合适,因为手机的麦克风不只是录制音乐,还要录制你的声音。

下面就是“开始/结束录音(Begin/End)”按钮部分代码。这部分代码和第一章时钟app的思路一样:

很简单吧,就是首先储存状态is_recording,然后实现各个功能:

  1. 开始或停止MediaRecorder对象;
  2. 改变is_recording状态;
  3. 升级按钮文字以反映当前状态。

程序的最后一部分就是升级recorder.kv,我们要调整一下“开始/结束录音(Begin/End)”按钮代码来调用begin_end_recording()函数:

这就搞定了。现在运行程序就可以向SD卡里录音了。不过在这之前请先看看下节内容。按钮的节目如下所示:

begainendbutton

重要警告——权限

默认的Kivy应用没有录音权限,android.permission.RECORD_AUDIO。如果初始化MediaRecorder实例就会失败。

当然有很多方法解决这个问题。首先,最容易的就是重新编译Kivy Launcher源代码,让应用获取录音权限,最新版在这里

在安装.apk文件之前,请卸载原来的版本。另外如果你感受一下Android反编译,也可以用**apktool直接反编译apk文件。步骤如下:

  1. 下载Kivy Launcher的app文件KivyLauncher.apk,用apktool文件命令:
  2. AndroidManifest.xml文件里增加权限:
  3. 重新打包成.apk文件:
  4. jarsigner工具为.apk文件签名。可以看看签名Android包的官方文档

这样,安装Kivy Launcher后就可以录音了。

你可以用同样的方法在通过Pyjnius为Python代码增加不同的权限。比如,要获得GPS API接入的权限,你的app就需要android.permission.ACCESS_FINE_LOCATION,其它Android设备权限可以看官方文档

播放声音

播放声音很简单,也不需要权限,对应的API也更简练。我们就要一个类MediaPlayer

当用户按下播放(Play)按钮时,下面的代码就要运行。我们还为下一节的删除文件增加了reset_player()函数。代码如下:

这些API的方法定义都可以在官方文档找到,不过意思很简单:就是复位播放器,加载声音文件,开始播放。文件的格式是自动设置的,可以让代码简单点。

实际上这类代码应该一直放在try ... catch里面,因为不知道什么地方就会异常。比如文件格式不对,SD卡没插好,或是读取失败,涉及到IO的问题多了。最好的办法就是小心驶得万年船(better safe than sorry)

删除文件

最后一个功能是用java.io.File,和Android关系不大了,是Java标准库。Android官方文档也会包含Java核心类的解释,尽管Java比Android早几十年。代码很简单,就是最后一行:

首先,我们用reset_player()函数停止播放功能,然后把文件删掉。

奇葩的是,Java的File.delete()方法不会抛出异常,所以不需要用try ... catch语句去控制异常了。能省则省!

细心的读者会问为啥不用Python的标准库os.remove()呢。其实用Java库跟Python一样简单;不过Java更快一点。另外,可以看出Pyjnius可以很好的使用Java类。 还要注意这个函数在桌面系统上也可以运行,因为它与Android无关;你只要用Java和Pyjnius就可以了。

现在UI和三个主要的功能都实现了,我们的app完成了。

总结

凡事都有利弊,非移植性代码和可移植性代码都如是。但是,做出正确的选择实际上非常困难,因为选择原生API通常会发生在项目早期,到了后期可能觉得完全不切实际,导致项目终歇菜。

本章开篇已经讨论过这种方法的主要优势:对于平台相关的代码,你实际上可以做任何事。没有人为的限制;你的Python代码可以不受限制的接入原生代码的底层API。

但是,依赖单一平台的风险就是:

  • Android市场自然比Android+iOS+…市场小
  • 把代码移植到新系统,要使用这些平台相关的特性时很困难
  • 如果项目只在一个平台,如果有平台要求发生改变就很危险。被Google封杀比同时被AppStore和Google,以及其他市场封杀的风险大得多

你可以好好想想,做一个适合你的app的决定。

再说UI

总之,大胆模仿其他UI模式理念(包括布局,字体,配色等)。毕加索的名言,“杰出艺术家模仿,伟大艺术家盗窃”,这正是当今网页和应用开发的精髓。

另外,微软用了“Modern UI”做了一堆应用,包括移动应用和桌面应用,并不是说这种设计模式就是好。众所周知,微软的操作系统才是这种设计模式被接受的基础。

现在放下Java吧。下一章我们用Python大名鼎鼎的Twisted网络框架做一个简单的CS模式的聊天app。