Python 安卓应用构建教程:使用 Kivy 和 AndroidStudio(四)
原文:Building Android Apps in Python Using Kivy with Android Studio
七、使用 Android Studio 中的 Kivy 项目
当 Python 开发者知道可以使用 Python 创建 Android 应用时,首先要问的问题之一是 Python 是否可以像在 Android Studio 中使用原生 Android 语言(即 Java)开发的那样开发丰富的 Android 应用。不幸的是,与 Java 相比,Python 在构建 Android(移动)应用时能力有限。但好消息是有办法丰富用 Python 创建的 Android 应用。
Kivy 团队开发了一些 Python 库,允许开发人员以比在 Android Studio 中更简单的方式访问 Android 功能。只需几行 Python 代码,我们就可以访问一个 Android 特性。其中一个库是 Plyer,这一章将会讨论。只需一行代码,就可以将通知推送到 Android 通知栏。
因为 Plyer 还在开发中,它的一些特性还没有实现。另一种解决方案是使用一个名为 Pyjnius 的库在 Python 中反映 Java 类。这涉及到在 Python 文件中编写 Java 代码。这有助于我们访问一些 Android 功能,但仍有一些未解决的问题。在 Python 中反映 Java 类会增加开发应用的复杂性。此外,在反射一些 Java 类时会出现一些异常。因此,使用 Plyer 和 Pyjnius 不足以访问所有 Android 功能。
记住,正如在第一章中提到的,当我们使用 Buildozer 构建 Kivy 应用时,会创建一个 Android Studio 项目。这个项目可以很容易地导入到 Android Studio 中,然后我们可以在那里继续开发应用。这使我们能够构建我们想要的任何特性。
在 Kivy 中,打包一些 Python 库有问题,比如 OpenCV。如果我们不能从使用 OpenCV 的 Kivy 应用构建 Android 应用,我们可以避免在 Python 中使用 OpenCV,然后在 Android Studio 中使用它。这种解决方法将在本章末尾进行说明。
普洛伊
在前几章创建的应用中,我们使用了 Kivy 支持的两个主要 Android 功能——访问摄像头和播放音频。一切都是用 Python 完成的。但 Android 中还有其他功能可能会让生活变得更容易,我们无法在 Kivy 中访问这些功能。我们可以在硬币游戏中使用陀螺仪来移动主角。对于 CamShare,当客户端和服务器之间的连接丢失时,我们可能会将通知推送到通知栏。但是我们如何用 Kivy(也就是用 Python)来实现呢?不幸的是,只有 Kivy 无法访问这些功能。
Kivy 团队创建了一个名为 Pyjnius 的库来访问 Python 中的 Java 类。因此,如果不能从 Python 访问某个特性,可以从 Java 访问它。注意,Java 代码是在 Python 文件中编写的,因此代码不是 Python 化的。
为了解决这个问题,该团队创建了一个名为Plyer的库来访问 Python 代码中的 Android 功能。它还不发达,一些功能在当前版本(1.3.3.dev0)中不可用,如摄像头、音频和 Wi-Fi。这个界面非常容易学习,并且它用最少的 Python 代码就可以访问这些特性。在 https://plyer.readthedocs.io/en/latest 可获得供应商文件。
在本节中,我们将讨论一个简单的例子,在这个例子中,使用 Plyer 将通知推送到通知栏并改变方向。在前几章创建的应用中,我们在构建 Android 应用之前,在台式计算机上对它们进行了测试。在本章中,我们不能在构建 Android 应用之前在桌面上测试应用,因为所使用的库(例如,Plyer)正在 Android 设备上运行。如果运行 Android 应用时出现异常,最好使用 logcat 来监控。
改变方向和推送通知
使用plyer.notification,我们可以将通知推送到 Android 设备的通知栏。这个模块有一个名为notify的函数,它接受许多参数来初始化通知。
为了改变器件方向,plyer.orientation存在。它有以下三个功能:
-
set_landscape(reverse=False):如果反转参数为True,则将方向设置为横向。 -
set_portrait(reverse=False):如果反转参数为True,则将方向设置为纵向。 -
set_sensor(mode='any|landscape|portrait'):根据模式参数中指定的值设置方向。
清单 7-1 显示了带有三个按钮部件的应用的 KV 文件。第一个执行show_notification()函数,将通知推送到通知栏。
当按下第二个按钮时,方向变为横向。注意plyer.orientation是在 KV 文件中导入的。在 KV 文件中调用set_landscape()函数。第三个按钮的工作方式与第二个类似,但是调用set_portrait()功能将方向改为纵向。
#:import orientation plyer.orientation
BoxLayout:
orientation: "vertical"
Button:
text: "Show Notification"
on_press: app.show_notification()
Button:
text: "Portrait"
on_press: orientation.set_portrait(reverse=True)
Button:
text: "Landscape"
on_press: orientation.set_landscape(reverse=True)
Listing 7-1KV File with Buttons for Pushing a Notification Message and Changing the Orientation of the Screen
Python 文件如清单 7-2 所示。show_notification()函数只有一行代码调用notify()函数。标题和消息参数出现在通知栏上。
import kivy.app
import plyer
class PushNotificationApp(kivy.app.App):
def show_notification(self):
plyer.notification.notify(title='Test', message='Notification using Plyer')
app = PycamApp()
app.run()
Listing 7-2Pushing a Notification Message to the Android Notification Bar
在构建 Android 应用之前,我们通常在台式计算机上运行应用进行测试。因为 Plyer 是安卓包,所以不能在台式电脑上测试,必须直接在安卓设备上运行。为了使调试过程更容易,我们使用logcat来跟踪错误堆栈。请记住,在构建、部署和运行 APK 文件时,可以使用以下命令激活logcat:
ahmedgad@ubuntu:~/Desktop$ buildozer android debug deploy run logcat
在 Android 设备上部署并运行应用,然后按下第一个按钮,我们将看到如图 7-1 所示的通知。应用图标用作通知的图标。这是推送通知所需的最少代码。
图 7-1
使用 Plyer 在 Python 中创建的 Android 通知消息
控制 Android 手电筒
使用plyer.flash可以在 Plyer 中访问 Android 手电筒。清单 7-3 中的 KV 文件创建了三个按钮部件,用于打开、关闭和释放闪光灯。下面列出了用于执行此类工作的函数:
-
plyer.flash.on():打开闪光灯 -
plyer.flash.off():关闭闪光灯 -
plyer.flash.release():释放闪光灯
#:import flash plyer.flash
BoxLayout:
orientation: "vertical"
Button:
text: "Turn On"
on_press: flash.on()
Button:
text: "Turn Off"
on_press: flash.off()
Button:
text: "Release"
on_press: flash.release()
Listing 7-3Controlling the Android Flashlight Using Plyer in Python
清单 7-4 中的 Python 文件除了创建一个扩展了kivy.app.App类的新类之外什么也没做。
import kivy.app
import plyer
class PycamApp(kivy.app.App):
pass
app = PycamApp()
app.run()
Listing 7-4Generic Python File for Developing a Kivy Application
运行应用后,我们将看到如图 7-2 所示的窗口。记住将CAMERA和FLASHLIGHT作为项目列在buildozer.spec文件的android.permissions属性中。
您可以扩展这个应用来创建在指定时间内打开和关闭手电筒的闪光模式。
图 7-2
一个控制手电筒的 Android 应用
前面的例子展示了 Plyer 库在 Python 代码中访问 Android 特性是多么简单。不幸的是,Plyer 被限制在一些功能上,不能做一个 Android 开发者想做的所有事情。有一些不受支持的功能,如显示祝酒辞,还有一些功能在某些平台上没有实现,如访问摄像头和播放音频。我们可以使用 Pyjnius 库通过 Java 代码访问这些缺失的特性。
皮涅乌斯
Pyjnius 是 Kivy 团队开发的一个库,用于访问 Python 代码中的 Java 类,以便在 Python Kivy 项目中使用当前不支持的 Android 功能。该库有一个名为autoclass()的核心函数,它接受 Java 类名并返回一个代表该 Java 类的变量。这个过程叫做反射。
清单 7-5 中显示了一个非常简单的例子。它使用java.lang.System类中的println()方法打印一条消息。第一条语句导入 Pyjnius 库。然后,通过将 Java 类的名称作为输入传递给autoclass()函数,Java 类在 Python 中得到反映。本例中返回的名为System的变量代表该类,但是用 Python 编写。我们可以像在 Java 中一样访问这个类中的方法。
import jnius
System = jnius.autoclass("java.lang.System")
System.out.println("Hello Java within Python")
Listing 7-5Printing a Message in Python Using the System Class in Java
另一个简单的例子是读取文本文件的内容。在读取文件之前,我们必须在 Python 中反映所有必需的 Java 类。建议我们先准备好 Java 文件,然后再转换成 Python。读取文本文件所需的 Java 代码如清单 7-6 所示。
import java.io.BufferedReader
import java.io.FileReader
FileReader textFile = new FileReader("pycam.kv");
BufferedReader br = new BufferedReader(textFile);
StringBuilder sb = new StringBuilder();
String line = br.readLine();
while (line != null) {
System.out.println(line);
line = br.readLine();
}
Listing 7-6Reading a Text File in Java
清单 7-6 中的 Java 代码使用了三个类。第一个是用于读取文件的java.io.FileReader类。为了读取读取文件中的行,使用了java.io.BufferedReader类。最后,我们可以使用java.lang.System类在控制台上打印每一行。注意,java.lang包已经被导入到任何 Java 类中,因此我们不需要在 Java 代码中为它添加一个import语句。准备好 Java 代码后,我们需要使用 Pyjnius 将其嵌入到 Python 中。
清单 7-6 中的前三个类在 Python 中有所体现,如清单 7-7 所示,使用了jnius.autoclass()函数。然后文件名作为输入传递给FileReader类的构造函数,结果返回给textFile变量。在本例中,读取了之前在清单 7-3 中创建的 KV 文件,用于在 Android 中控制手电筒。file 对象在textFile变量中返回,该变量作为输入传递给BufferedReader类的构造函数。结果在bufferedReader变量中返回。
import jnius
FileReader = jnius.autoclass("java.io.FileReader")
BufferedReader = jnius.autoclass("java.io.BufferedReader")
System = jnius.autoclass("java.lang.System")
textFile = FileReader("pycam.kv")
bufferedReader = BufferedReader(text_file)
line = bufferedReader.readLine()
while line != None:
System.out.println(line)
line = bufferedReader.readLine()
Listing 7-7Reading a Text File Using Java Classes Reflected in Python Using Pyjnius
为了从文件中读取一行,我们调用了readLine() Java 方法。为了读取文件中的所有行,只要返回的行不是None,就会创建一个while循环来打印这些行。注意None是指 Java 中的null。打印到控制台的线条如图 7-3 所示。
图 7-3
使用 Java 类在 Python 中读取文本文件的结果
在理解了 Pyjnius 库背后的基本概念之后,我们可以使用它来访问 Android 中的特性。
使用 Pyjnius 在 Android 中播放音频
我们可以从讨论 Android 中用来播放音频的 Java 代码开始,思考如何用 Pyjnius 用 Python 来写。代码如清单 7-8 所示,其中在android.media包中使用了一个名为MediaPlayer的类。实例化该类并将实例返回到mediaPlayer变量后,我们可以调用所需的方法来加载和播放音频文件。
setDataSource()方法接受要播放的音频文件的文件名。prepare()方法准备媒体播放器。最后,start()方法开始播放音频文件。为了捕捉在播放文件出现问题时抛出的异常,对这些方法的调用被一个try-catch块所限制。如果有异常,使用System类将消息打印到控制台。
import android.media.MediaPlayer;
MediaPlayer mediaPlayer = new MediaPlayer();
try {
fileName = "bg_music_piano.wav";
mediaPlayer.setDataSource(fileName);
mediaPlayer.prepare();
mediaPlayer.start();
} catch {
System.out.println("Error playing the audio file.");
}
Listing 7-8Java Code to Play an Audio File in Android
完成 Java 代码后,我们接下来需要讨论在 Python 中反映这些代码的过程。
这个类可以用之前讨论过的autoclass()函数在 Python 中反映出来。如果从那个反射类创建的对象被命名为mediaPlayer,方法将被调用,就像在 Java 代码中那样。
关于 Python 中异常的处理,有两点值得一提。第一个是 Python 中的块是用缩进定义的。第二个是 Java 中的异常是在catch块中处理的。在 Python 中,块名是except。在 Python 和 Java 之间进行这样的映射是很重要的。使用 Pyjnius 播放音频文件的 Python 代码如清单 7-9 所示。
import jnius
MediaPlayer = jnius.autoclass("android.media.MediaPlayer")
mp = MediaPlayer()
try:
fileName = "bg_music_piano.wav"
mp.setDataSource(fileName)
mp.prepare()
mp.start()
except:
print("Error Playing the Audio File")
Listing 7-9Playing an Audio File in Python by Reflecting Java Classes Using Pyjnius
基于清单 7-9 中的代码,我们可以创建一个播放、暂停和停止音频的应用。应用的 KV 文件如清单 7-10 所示。有一个BoxLayout有五个小部件。前两个小部件是Labels,后三个是Buttons。
第一个Label根据播放音频文件的进度更新。这是通过使用Rectangle顶点指令在canvas.before内绘制一个矩形来完成的,根据Color指令,矩形被涂成红色。这个Label小部件的默认大小是 0.0,这意味着当应用启动时它是隐藏的。大小根据播放进度而变化。
第二个Label小部件在播放文件时显示信息,以毫秒表示文件的持续时间,以毫秒表示文件正在播放的位置,以及从 0.0%到 100.00%的进度百分比。
BoxLayout:
orientation: "vertical"
Label:
id: audio_pos
size_hint_x: 0.0
size: (0.0, 0.0)
canvas.before:
Color:
rgb: (1, 0, 0)
Rectangle:
pos: self.pos
size: self.size
Label:
id: audio_pos_info
text: "Audio Position Info"
Button:
text: "Play"
on_release: app.start_audio()
Button:
text: "Pause"
on_release: app.pause_audio()
Button:
text: "Stop"
on_release: app.stop_audio()
Listing 7-10KV File for Playing an Audio File in Android
这三个按钮负责启动、暂停和停止音频。当on_release事件被触发时,回调函数start_audio()、pause_audio()和stop_audio()被调用。
图 7-4 显示了应用在 Android 设备上运行后的窗口。
图 7-4
一个播放音频文件的 Android 应用
Python 文件如清单 7-11 所示。它实现了与三个Button小部件相关的回调函数。start_audio()函数完成前面讨论的工作,从反射MediaPlayer类直到使用start()方法播放音频文件。注意,MediaPlayer的实例被设置为使用self关键字引用的当前对象中的一个属性,以便在定义它的函数之外访问它。可以通过self.mediaPlayer访问。
创建一个名为prepare_audio的类变量来确定音频文件之前是否被加载。它被初始化为False表示文件没有被加载。如果它的值是False,则start_audio()函数加载并启动该文件。启动后,其值被设置为True。
如果音频文件只是暂停,我们不必再次重新加载文件。因此,该文件将通过调用if语句的else部分中的start()方法来启动。这将从暂停前的位置继续播放音频文件。如果暂停后准备好文件,文件将从头开始播放。
请注意,如果播放音频文件出现问题,except块会更改第二个Label的文本以指示错误。
import kivy.app
import jnius
import os
import kivy.clock
class AudioApp(kivy.app.App):
prepare_audio = False
def start_audio(self):
if AudioApp.prepare_audio == False:
MediaPlayer = jnius.autoclass("android.media.MediaPlayer")
self.mediaPlayer = MediaPlayer()
try:
fileName = os.getcwd()+"/bg_music_piano.wav"
self.mediaPlayer.setDataSource(fileName)
self.mediaPlayer.prepare()
kivy.clock.Clock.schedule_interval(self.update_position, 0.1)
self.mediaPlayer.start()
AudioApp.prepare_audio = True
except:
self.current_pos.text = "Error Playing the Audio File"
print("Error Playing the Audio File")
else:
self.mediaPlayer.start()
def pause_audio(self):
if AudioApp.prepare_audio == True:
self.mediaPlayer.pause()
def stop_audio(self):
if AudioApp.prepare_audio == True:
self.mediaPlayer.stop()
AudioApp.prepare_audio = False
def update_position(self, *args):
audioDuration = self.mediaPlayer.getDuration()
currentPosition = self.mediaPlayer.getCurrentPosition()
pos_percent = float(currentPosition)/float(audioDuration)
self.root.ids['audio_pos'].size_hint_x = pos_percent
self.root.ids['audio_pos_info'].text = "Duration: "+str(audioDuration) + "\nPosition: " + str(currentPosition)+"\nPercent (%): "+str(round(pos_percent*100, 2))
app = AudioApp()
app.run()
Listing 7-11Kivy Application for Playing an Audio File in Android Based on Reflected Java Classes Using Pyjnius
使用kivy.clock.Clock类中的schedule_interval()函数,每 0.1 秒执行一次名为update_position()的回调函数。这将根据音频文件中播放的当前毫秒数更新 KV 文件中的两个Label小部件。
首先,使用audioDuration变量中的getDuration()方法返回音频文件持续时间。使用currentPosition变量中的getCurrentPosition()方法返回当前位置,以毫秒为单位。进度百分比的计算方法是用audioDuration除以currentPosition。因为这些变量中的值是整数,并且预期结果在 0.0 和 1.0 之间,也就是 float,所以我们必须使用float()函数将它们的数据类型从 integer 改为 float。文件持续时间、当前位置和当前百分比显示在 KV 文件内的第二个Label小部件上。
注意,显示播放音频文件的进度类似于更新 CoinTex 游戏的角色的生命百分比。在讨论了处理第一个按钮的on_release事件的函数之后,我们可以讨论其余两个按钮的函数。
第二个Button小部件使用pause()方法暂停播放的音频文件。根据它的回调函数pause_audio(),一个if语句确保文件在执行这个方法之前被播放,因为它必须为一个活动的音频文件被调用。
最后一个按钮通过调用stop()方法来停止音频。类似于pause_audio()回调函数,stop_audio()函数有一个if语句,确保音频文件在停止播放之前正在播放。调用此方法后,只有再次准备文件后,才能再次播放文件。因此,我们必须执行在start_audio()函数的if语句中准备音频文件的代码。
图 7-5 显示了播放音频文件并在毫秒 5943 暂停后的结果,这相当于整个文件持续时间的 19.44%。第一个标签宽度是屏幕宽度的 19.44%。
图 7-5
通过在 Python 中反映 Java 类来播放 Android 中的音频文件
这个例子很容易在 Python 中使用 Java 代码。不幸的是,即使是简单的操作,比如显示一条 toast 消息,这个过程也不简单。在 Java 中,toast 只使用一行代码显示,如下所示:
Toast.makeText(this, "Hello Java", Toast.LENGTH_LONG).show();
这个过程需要的不仅仅是在 Python 中反映Toast类(android.widget.Toast)。例如,我们必须知道要显示的文本实际上是CharSequence类的一个实例。因此,为了将文本转换成适合文本参数的类型,必须反射这个类。而且,toast 只在 UI 线程内创建,不在线程外创建。因此,它可以显示在一个Runnable实例中。与 Java 示例相比,开发人员需要做大量的工作。
请记住,使用 Pyjnius 的目的是让 Kivy 开发人员能够更简单地用 Python 构建使用 Java 特性的 Android 应用。如果过程会变得复杂,我不推荐用它来写 Java 代码。它可以用于简单的任务。
但是如果我们不能在 Python 中编写 Java 代码,我们在哪里编写呢?答案就在 Java 文件中。
理解使用推土机构建的 android 项目
记住 Python-for-Android 是 Python(即 Kivy)和 Android(即 Java)之间的桥梁。使用 Buildozer,从 Python Kivy 项目自动构建一个 Android Java 项目。构建完 Android 项目后,Java 项目存在于以下路径中,假设 Python 项目的根目录为NewApp,buildozer.spec文件内指定的包名为kivyandroid。
NewApp/.buildozer/android/platform/build/dists/kivyandroid
如果您熟悉 Android Studio 项目的结构,您可以在项目中导航,找到常规项目中所有必要的文件和目录。
在项目的根目录下,存在build.gradle和AndroidManifest.xml文件。我们将不会详细讨论这些文件,而只是获得帮助我们理解如何从 Android Studio 管理 Android 项目的想法。
AndroidManifest.xml文件如清单 7-12 所示。它从<manifest>元素开始。在这个元素的头中,指定了包名和应用版本。它首先使用 package 属性指定应用包名,该属性设置为com.test.kivyandroid。这是buildozer.spec文件中的package.domain和package.name属性串联的结果。
android:versionCode属性是一个表示应用内部版本号的整数值,用于确定该版本是否比另一个版本新。在将应用上传到 Google Play 时,这个整数有助于它知道有新版本的应用通知用户更新。但是这个整数不会显示给用户。属性的值是一个字符串,显示给用户。
android:installLocation允许我们指定应用的安装位置。如果内部存储器已满,则设置为auto以在外部存储器上安装应用。
为了支持广泛的设备,<supports-screens>元素没有对目标屏幕设置限制,而是将其所有属性设置为True。
<uses-sdk>元素使用android:minSdkVersion和android:targetSdkVersion属性指定最小和目标 SDK。尽量以可能的最高 SDK 版本为目标,但将目标 SDK 设置为至少 26 个,因为 Google Play 不再接受目标 SDK 低于 26 的应用。
<uses-permission>元素设置应用所需的权限。请记住第一章,在 Android 项目根目录下的 templates 文件夹中有一个名为AndroidManifest.tmpl.xml的文件,它循环遍历buildozer.spec文件中的android.permissions属性,以便为每个请求的权限创建一个<uses-permission>元素。WRITE_EXTERNAL_STORAGE元素总是被应用请求。
<?
xml version="1.0" encoding="utf-8"
?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.test.kivyandroid"
android:versionCode="3"
android:versionName="0.3"
android:installLocation="auto">
<supports-screens
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true"
android:xlargeScreens="true" />
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion= "26" />
<!-- OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" />
<!-- Allow writing to external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="@string/app_name"
android:icon="@drawable/icon"
android:allowBackup="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true" >
<meta-data android:name="wakelock" android:value="0"/>
<activity android:name="org.kivy.android.PythonActivity"
android:label="@string/app_name"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Listing 7-12The AndroidManifest.xml File Inside the Kivy Android Studio Project
当构建一个 Android 项目时,会有一个包含多个活动(即 Java 类)的应用。AndroidManifest.xml文件的结构反映了这一点。它有一个名为<application>的元素,用于声明整个应用。使用<activity>元素,我们可以声明每个单独的活动。<application>元素将一个或多个<activity>元素作为子元素。
从<application>元素开始,它的头中有许多属性来定义应用属性。例如,应用名称是使用android:label属性指定的。这被设置为向用户显示的字符串。您可以将其设置为原始字符串,但最好将其设置为字符串资源,以便在应用的其他部分引用它,我们将在后面看到这一点。
在 Kivy built 项目中,指定应用名称的字符串被添加为 ID 为app_name的字符串资源。这里显示的路径中的strings.xml文件保存了应用使用的字符串资源。这个文件在templates目录下有一个名为strings.tmpl.xml的模板。
NewApp/.buildozer/android/platform/build/dists/kivyandroid/src/main/res/values
该文件如清单 7-13 所示。有一个名为app_name的元素,它保存了应用的名称(CoinTex)。为了访问AndroidManifest.xml文件中名为app_name的字符串资源中的值,我们首先需要使用@string引用strings.xml资源文件,然后使用@string/app_name指定字符串资源的 ID。
strings.xml文件也有许多字符串资源,比如presplash_color,它是应用启动时显示的屏幕背景。
<?
xml version="1.0" encoding="utf-8"
?>
<resources>
<string name="app_name">CoinTex</string>
<string name="private_version">1544693478.42</string>
<string name="presplash_color">#000000</string>
<string name="urlScheme">kivy</string>
</resources>
Listing 7-13The strings.xml File Inside the Kivy Android Studio Project
<application>元素有另一个名为android:icon的属性来设置应用图标。它被设置为 ID 为icon的可提取资源。可绘制资源文件位于以下路径。注意,ID 图标指的是一个名为 icon 的图像(位图文件),其扩展名可以是.png、.jpg或.gif。
NewApp/.buildozer/android/platform/build/dists/kivyandroid/src/main/res/drawable
如果将buildozer.spec文件的fullscreen属性设置为 1,android:theme属性将应用隐藏通知栏的样式。
<application>元素还有另一个名为android:hardwareAccelerated的属性,该属性被设置为True,以平滑地渲染屏幕上显示的图形。
<activity>元素声明了应用中的活动。只有一个名为org.kivy.android.PythonActivity的活动,它是主活动。此活动的 Java 类位于以下路径中。通过定位应用的 Java 类,我们可以添加任何需要执行的 Java 代码。我们稍后会看到这一点。
NewApp/.buildozer/android/platform/build/dists/kivyandroid/src/main/java/org/kivy/android
在<activity>元素头中,android:label属性被设置为 ID 为app_name的字符串资源中的值。这是用于在重用的<application>元素中设置应用名称的相同资源。这就是为什么使用字符串资源很重要。
当某些事件发生时,例如在活动运行时隐藏键盘,默认情况下活动会重新启动。android:configChanges属性决定了在不重启的情况下由活动处理的一组配置更改。在我们的项目中,这个属性有三个值,除了orientation和screenSize之外还有keyboardHidden,用于处理屏幕方向从横向到纵向的变化,反之亦然。
android: screenOrientation属性设置设备的方向。它反映了存储在buildozer.spec文件的orientation属性中的值。
<activity>元素有一个名为<intent-filter>的子元素,它声明了活动的意图。这就是活动能做到的。其他应用可以调用您的活动,因为它使用了在该元素中声明的功能。为了允许接受意图,<intent-filter>元素必须至少有一个<action >元素。在我们的活动中,它的动作被显示为名字android.intent.action.MAIN。这意味着父活动是应用的入口点(即,活动在打开应用后打开)。为了在设备的应用启动器中列出应用,添加了名称等于android.intent.category.LAUNCHER的<category>元素。
这是对AndroidManifest.xml文件的快速概述。请注意,该文件中的值可能会被build.gradle文件覆盖。该文件中有趣的部分是声明最小 SDK 和目标 SDK 的部分,如清单 7-14 所示。很好理解,SDK 最小版本是 18,目标 SDK 是 26,版本号是整数 3,版本名是字符串 0.3。注意,这些值与AndroidiManifest.xml文件中定义的值相同。
记住,有一个名为build.tmpl.gradle的模板文件,它接受来自buildozer.spec文件的这些值,并生成build.gradle文件。它也位于AndroidManifest.tmpl.xml和strings.tmpl.xml文件中的templates目录下。
android {
compileSdkVersion 18
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 18
targetSdkVersion 26
versionCode 3
versionName '0.3'
}
Listing 7-14Specifying the Minimum SDK and Target SDK Versions Inside the build.gradle File
当在 Android Studio 中操作 Android 项目时,Android Studio 将搜索在build.gradle文件中指定的buildToolsVersion和compileSdkVersion字段的值。如果找不到它们,项目将无法构建。您可以将这些版本更改为适合您系统的版本。
项目主要活动
根据上一节,主活动类被命名为PythonActivity,它位于org.kivy.android包中。由于扩展了Activity类(android.app.Activity,该类不是一个活动,而是由于扩展了SDLActivity类,后者又根据这里显示的类头扩展了Activity类。
public class PythonActivity extends SDLActivity{}
SDLActivity类存在于org.libsdl.app包中。该文件位于如下所示的路径中。注意,我们使用 SDL 作为 Kivy 的后端,这就是为什么这个类的名字中有 SDL。如果你用的是 PyGame,可能会有一些变化。
NewApp/.buildozer/android/platform/build/dists/kivyandroid/src/main/java/org/libsdl/app
因为任何 Android 应用的入口点都是onCreate()方法,所以我们可以讨论一下。除了方法之外,PythonActivity类顶部的几行代码如清单 7-15 所示。前两行导入名为SDLActivity和ResourceManager的类。
我们曾经在 Android 项目中找到一个名为R.java的类来管理资源,例如维护它们的 id。对于 Kivy 项目,这个类被一个名为ResourceManager的类所替代,这个类在org.renpy.android包中。这就是为什么在onCreate()方法的开头从这个类创建一个实例。
名为mActivity的公共静态类变量被创建并初始化为null。在onCreate()方法中,这个变量被设置为引用当前活动,因为它被赋予了当前对象this,引用活动。注意,我们可以使用这个变量在 Python 脚本中使用 Pyjnius 来访问主活动。这是通过反射PythonActivity类,然后访问它的属性mActivity来实现的。
import org.libsdl.app.SDLActivity;
import org.renpy.android.ResourceManager;
import android.os.Bundle;
...
public class PythonActivity extends SDLActivity {
private static final String TAG = "PythonActivity";
public static PythonActivity mActivity = null;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "My oncreate running");
resourceManager = new ResourceManager(this);
Log.v(TAG, "About to do super onCreate");
super.onCreate(savedInstanceState);
Log.v(TAG, "Did super onCreate");
this.mActivity = this;
this.showLoadingScreen();
new UnpackFilesTask().execute(getAppRoot());
Toast.makeText(this, "Working on the Kivy Project in Android Studio", Toast.LENGTH_LONG).show();
}
...
}
Listing 7-15The onCreate() Method Inside the PythonActivity Class
调用活动内部名为showLoadingScreen()的方法。这个方法只是在一个ImageView中加载预刷图像。记住,这个图像的名称是在buildozer.spec文件的presplash.filename属性中指定的。该方法如清单 7-16 所示。
该方法工作如下。如果ImageView不是在mImageView变量中创建的,它会通过加载预渲染图像资源来创建它。使用resourceManager实例中的getIdentifier()方法将资源标识符返回到presplashId变量中。此方法接受资源名称及其种类,并返回一个表示 ID 的整数。这就是为什么presplashId变量类型是整数。注意,在常规的 Android 项目中,findViewById()方法返回 id。
预喷涂图像位于此路径NewApp/src/main/res/drawable/presplash.jpg。因为资源是一个图像,所以它的种类是可绘制的,因此它被添加到drawable目录中。这个目录位于res目录下,以表示它包含资源。资源的名称是presplash,它是不带扩展名的资源文件名。
在返回它的 ID 之后,原始图像文件被打开并返回给is变量,它是InputStream类的一个实例。然后使用BitmapFactory类的decodeStream()方法将原始数据解码为图像。数据被返回给一个名为bitmap的变量。
之后,ImageView类的一个实例被返回到mImageView变量,显示在其上的图像使用setUmageBitmap()方法进行设置。它接受我们之前创建的bitmap变量。
记住,加载屏幕的背景颜色被保存为 ID 为presplash_color的strings.xml文件中的字符串资源。为了返回字符串资源中的值,使用了ResourceManager类中的getString()方法。使用ImageView类的setBackgroundColor()方法,mImageView实例的背景颜色被改变。为了填充父尺寸,为ImageView指定了一些参数。
protected void showLoadingScreen() {
if (mImageView == null) {
int presplashId = this.resourceManager.getIdentifier("presplash", "drawable");
InputStream is = this.getResources().openRawResource(presplashId);
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(is);
} finally {
try {
is.close();
} catch (IOException e) {};
}
mImageView = new ImageView(this);
mImageView.setImageBitmap(bitmap);
String backgroundColor = resourceManager.getString("presplash_color");
if (backgroundColor != null) {
try {
mImageView.setBackgroundColor(Color.parseColor(backgroundColor));
} catch (IllegalArgumentException e) {}
}
mImageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT));
mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
}
if (mLayout == null) {
setContentView(mImageView);
} else if (PythonActivity.mImageView.getParent() == null){
mLayout.addView(mImageView);
}
}
Listing 7-16The showLoadingScreen() Method
有一个名为mLayout的变量引用包含所有视图的活动布局。根据下一行,这个变量在SDLActivity类中被定义为受保护的静态变量:
protected static ViewGroup mLayout;
为了显示会前图像视图,必须将图像视图添加到活动布局中。为了给布局添加一个视图,使用了addView()方法。使用命令mLayout.addView(mImageView),预绘制图像视图被添加到由mLayout变量引用的活动布局中。
注意以下几点非常重要:
-
该应用没有布局 XML 资源文件。因此,布局是在 Java 代码中动态创建的。
-
在
SDLActivity类的静态mLayout变量中定义的活动只有一个布局。因此,添加到布局中的视图是可见的,直到我们删除它们。例如,在应用开始之前(即,在加载结束之后),必须从布局中移除预溅图像视图。
在屏幕完成加载后,根据PythonActivity类中名为removeLoadingScreen()的方法移除预渲染图像视图。这将在本章后面讨论。
请注意,如果之前没有创建布局,则使用setContentView()功能将图像本身作为主视图显示在屏幕上。
在加载屏幕上显示预溅图像而不是直接启动应用的目的是在应用启动之前必须加载文件。在项目根目录中,有一个名为python-install的文件夹,其中包含构建应用所需的所有 Python 文件。
在显示加载屏幕后,onCreate()方法通过从扩展了AsyncTask的UnpackFilesTask类创建一个实例来启动一个后台线程。请注意,在加载屏幕处于活动状态时,运行该线程后会显示一条提示消息。
列表 7-17 中显示了这个类的类头和一些代码块。注意,这个类嵌套在PythonActivity类中,因此不需要import语句。
UnpackFilesTask类实现了AsyncTask类中的doInBackground()回调方法,该方法在实例创建后立即启动。在它内部,调用了一个在UnpackFilesTask类内部名为unpackData()的方法。它加载项目文件。
import android.os.AsyncTask;
...
private class UnpackFilesTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... params) {
...
unpackData("private", app_root_file);
return null;
}
@Override
protected void onPostExecute(String result) {
...
mActivity.finishLoad();
mActivity.showLoadingScreen();
}
...
}
Listing 7-17The UnpackFilesTask Class
文件加载后,SDL 调用一个名为keepActive()的方法。如清单 7-18 所示。这个方法在SDLActvity类中定义,在PythonActivity类中实现。
@Override
public void keepActive() {
if (this.mLoadingCount > 0) {
this.mLoadingCount -= 1;
if (this.mLoadingCount == 0) {
this.removeLoadingScreen();
}
}
}
Listing 7-18Implementation of the keepActive() Method
该方法调用另一个名为removeLoadingScreen()的方法,该方法从活动布局中移除预绘制图像视图。它的实现如清单 7-19 所示。它创建一个在应用 UI 上运行的线程。在这个线程中,使用引用活动布局的getParent()方法返回mImageView变量的父级。使用removeView()方法,从布局中移除mImageView。为了删除该变量中的值,将其值返回到null。
public void removeLoadingScreen() {
runOnUiThread(new Runnable() {
public void run() {
if (PythonActivity.mImageView != null &&
PythonActivity.mImageView.getParent() != null) {
((ViewGroup)PythonActivity.mImageView.getParent()).removeView( PythonActivity.mImageView);
PythonActivity.mImageView = null;
}
}
});
}
Listing 7-19Implementation of the removeLoadingScreen() Method
执行removeLoadingScreen()方法后,活动布局将为空。这意味着我们已经准备好用新的 UI 元素填充布局,就像 Python 应用中定义的那样。为此,UnpackFilesTask类还实现了onPostExecute()回调方法,该方法在类线程完成执行时被执行。这种方法在 UI 线程上执行,因此能够管理 UI。在该方法内部,执行另一个名为finishLoad()的方法。这个方法在SDLActivity类中定义。这个类头和 finishLoad()方法的一些行都显示在清单 7-20 中。
import android.widget.AbsoluteLayout;
import android.view.*;
import android.app.*;
...
public class SDLActivity extends Activity {
...
protected static ViewGroup mLayout;
...
protected void finishLoad() {
...
mLayout = new AbsoluteLayout(this);
mLayout.addView(mSurface);
setContentView(mLayout);
}
...
}
Listing 7-20Setting the Application Layout Inside the finishLoad() Method Within the SDLActivity Class
请注意,完成加载屏幕的目标不仅仅是启动应用。另一个目的是让 UI 布局由SDLActivity管理,因为它是负责准备应用 UI 的人。finishLoad()方法在mLayout实例中准备一个布局。稍后我们将使用这种方法向主活动的布局添加视图(PythonActivity)。
显示 Python 中的 Toast 消息
在 PythonActivity 类内部,有一个名为toastError()的方法,如清单 7-21 所示。它根据输入字符串参数显示一条 toast 消息。它使用runOnUIThread()在 UI 上运行一个线程。使用保存在mActivity变量中的PythonActivity实例调用这个方法。
public void toastError(final String msg) {
final Activity thisActivity = this;
runOnUiThread(new Runnable () {
public void run() {
Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show();
}
});
// Wait to show the error.
synchronized (this) {
try {
this.wait(1000);
} catch (InterruptedException e) {
}
}
}
Listing 7-21Implementation of the toastError() Method to Display Toast Messages
我们可以从这个方法中获益,在 Python 代码中显示 toast 消息。唯一要做的就是反射PythonActivity类并访问它的mActivity属性,这将调用toastError()方法。应用的 KV 文件如清单 7-22 所示。根节点BoxLayout有一个按钮,当释放时它调用一个叫做show_toast()的方法。因为 toast 颜色较深,所以添加了一个白色标签,以便更容易看到 toast 消息。
BoxLayout:
orientation: "vertical"
Button:
text: "Show Toast"
on_release: app.show_toast()
Label:
canvas.before:
Color:
rgb: (1, 1, 1)
Rectangle:
pos: self.pos
size: self.size
Listing 7-22KV File for Displaying a Toast Method Using Python
显示 toast 的 Python 文件如清单 7-23 所示。使用jnius.autoclass仅反射一次PythonActivity。从该类返回mActivity属性。在show_toast()方法内部,使用mActivity变量调用toastError()方法。
import kivy.app
import jnius
import kivy.uix.button
PythonActivity = jnius.autoclass("org.kivy.android.PythonActivity")
mActivity = PythonActivity.mActivity
class ToastApp(kivy.app.App):
def show_toast(self):
mActivity.toastError("Test Toast :)")
app = ToastApp()
app.run()
Listing 7-23Displaying a Toast Message Within Python by Reflecting the PythonActivity Class
松开按钮后,吐司显示如图 7-6 所示。
图 7-6
使用 Python 显示的 toast 消息
在 Android Studio 中打开 Kivy Android 项目
从前面的讨论中,我们至少对 Buildozer 创建的 Android 项目是如何工作的有了基本的了解。下一步是在 Android Studio 中编辑这个项目。
到目前为止,Linux 已经被用来开发项目。这是因为 Buildozer 只在 Linux 中受支持。生成 Android 项目后,我们可以使用我们选择的操作系统在 Android Studio 中操作它。在本节中,Windows 用于导入和编辑项目。在开始之前,确保 Android Studio 工作正常,并且在gradle.build文件中列出了构建工具和 SDK 的版本。
在编辑项目之前,我们需要导入它。只需从文件菜单中选择打开的项目。出现一个窗口,如图 7-7 所示,用于导航到项目的路径。项目名称旁边会出现 Android Studio 图标,表示这是 Android Studio 理解的项目。单击“确定”打开项目。
图 7-7
在 Android Studio 中导入使用 Buildozer 构建的 Kivy Android Studio 项目
项目打开后,项目文件将出现在窗口的左侧。Android视图中文件的结构如图 7-8 所示。此视图帮助您以简单的方式查看文件,而不管它们在项目中的实际位置。例如,不必要的文件不会显示,任何相关的文件都被分组在一起。
图 7-8
Android Studio 中导入项目的结构
在manifests组中,列出了项目中使用的所有清单。这个项目有一个名为AndroidManifst.xml的清单文件。Java 主活动类(PythonActivity)位于java组中。我们很容易推断出这个类在org.kivy.android包中。build.gradle文件位于名为Gradle Scripts的组中。res组包含所有的资源,比如strings.xml文件。通过选择Project视图,您可以查看存储在磁盘上的项目文件。
导入项目后,我们可以根据图 7-9 在仿真器或 USB 连接设备上运行项目。
图 7-9
选择用于运行 Android 应用的 USB 连接设备
单击 OK 后,项目将在所选设备上构建、部署、安装和运行。图 7-10 显示了显示 toast 消息的修改后的加载屏幕。如果一切顺利,那么我们做得很好。
图 7-10
在应用加载时修改 Kivy presplash 图像并显示一条 toast 消息
加载屏幕持续几秒钟。当用户等待应用打开时,我们可以播放一些有趣的音乐或改变屏幕的布局。清单 7-24 中的代码在应用加载时播放音乐。
protected void onCreate(Bundle savedInstanceState) {
Log.v(TAG, "My oncreate running");
resourceManager = new ResourceManager(this);
Log.v(TAG, "About to do super onCreate");
super.onCreate(savedInstanceState);
Log.v(TAG, "Did super onCreate");
this.mActivity = this;
this.showLoadingScreen();
new UnpackFilesTask().execute(getAppRoot());
Toast.makeText(this, "Working on the Kivy Project in Android Studio", Toast.LENGTH_LONG).show();
int music_id = resourceManager.getIdentifier("music", "raw");
MediaPlayer music = MediaPlayer.create(this, music_id);
music.start();
}
Listing 7-24Playing Music While the Kivy Application Is Loading
音乐文件作为war添加到 Android 项目中。根据图 7-11 ,这是通过在res组内创建一个目录并在其中添加音乐文件来实现的。添加资源有助于在应用中轻松检索它们。这比使用资源在设备中的路径来引用资源要好。
图 7-11
将音乐文件作为原始资源添加到 Android Studio 项目中
在将原始资源添加到项目中之后,我们必须在 Java 代码中引用它。请记住,Kivy Android 项目中的资源是使用org.renpy.android包中名为ResourceManager的类来操作的。
在上一节中,名为presplash的可绘制资源的 ID 是使用该类中的getIdentifier()方法返回的。类似地,使用此方法返回原始资源的标识符。只需指定合适的name和kind参数。
因为资源文件名是music.mp3,所以name参数被设置为music。它的kind被设置为raw,因为它存在于raw文件夹中。因为资源的 ID 是整数,所以 ID 被返回给 integer 类型的music_id变量。
运行应用后,音乐将在加载屏幕出现时立即播放。文件加载完成后,它会自动停止。
向 PythonActivity 布局添加视图
默认情况下,预溅图像是加载屏幕时活动布局上显示的唯一视图。我们可以改变这个布局来添加更多的视图。请记住,该活动只有一个布局,因此,一旦不需要新添加的视图,我们就会将其删除。
当屏幕加载时,我们可以添加更多视图的地方是PythonActivity类的showLoadingScreen()方法。根据清单 7-25 中所示的修改方法,我们可以在活动布局中添加一个单独的TextView。在mImageView变量中定义的预渲染图像视图被添加到布局后,添加TextView。代码省略了一些部分,重点放在将TextView添加到布局的部分。
来自TextView类的一个实例被返回到在PythonActivity类中定义的名为loadingTextView的静态变量中。不将这个变量作为方法的局部变量的原因是,我们需要稍后在removeLoadingScreen()方法中访问它来移除它。
使用setText()方法指定文本视图中显示的文本。使用setTextSize()方法改变其文本大小。准备好文本视图后,使用addView()方法将其添加到布局中。
static TextView loadingTextView = null;
...
protected void showLoadingScreen() {
...
if (mLayout == null) {
setContentView(mImageView);
} else if (PythonActivity.mImageView.getParent() == null){
mLayout.addView(mImageView);
// Adding Custom Views to the Layout
loadingTextView = new TextView(this);
loadingTextView.setText("Kivy application is loading. Please wait ...");
loadingTextView.setTextSize(30);
mLayout.addView(loadingTextView);
}
}
Listing 7-25Adding a TextView to the Loading Screen While the Kivy Application Is Loading
在我们运行应用后,文本视图将会出现,如图 7-12 所示。
图 7-12
将 Android TextView 添加到 Kivy 应用的加载屏幕
假设文本视图没有从布局中移除,它将在应用启动后保持可见,如图 7-13 所示。
图 7-13
加载时添加到应用布局中的 TextView 在应用启动后仍然可见
请记住,发生这种情况是因为在加载屏幕和应用启动后,应用只有一种布局。因此,移除removeLoadingScreen()方法中的视图非常重要,如清单 7-26 所示。类似于移除预渲染图像视图,返回文本视图的父视图。父节点调用removeView()方法来移除它。
public void removeLoadingScreen() {
runOnUiThread(new Runnable() {
public void run() {
if (PythonActivity.mImageView != null &&
PythonActivity.mImageView.getParent() != null) {
((ViewGroup)PythonActivity.mImageView.getParent()).removeView(PythonActivity.mImageView);
PythonActivity.mImageView = null;
((ViewGroup)PythonActivity.loadingTextView.getParent()).removeView(PythonActivity.loadingTextView);
}
}
});
}
Listing 7-26Removing the TextView Inside the removeLoadingScreen() Method
总之,一个视图被添加到活动布局中,只要屏幕在加载,这个视图就一直存在。加载过程结束后,视图将被删除。如果视图没有从父视图中删除,那么在加载 Python 中定义的应用 UI 后,它仍然可见。
假设在加载步骤结束后,我们需要向活动布局添加一个视图。我们在 Java 代码的什么地方添加了这个视图?让我们在下一节讨论这个问题。
SDLSurface
为了确定在文件加载之后,应用布局可见之前,在项目中添加视图的合适位置,讨论一下SDLSurface类是很重要的。
根据这里显示的SDLSurface类头,在SDLActivity Java 文件中,有一个名为SDLSurface的类扩展了android.view.SurfaceView Android 类。
class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, View.OnKeyListener, View.OnTouchListener, SensorEventListener
SurfaceView是一个 Android 组件,我们可以在其中绘制应用 UI。Kivy 项目 KV 文件中指定的 UI 小部件绘制在这个SurfaceView中。
SurfaceView实现了一个名为SurfaceHolder.Callback的接口,它提供了许多回调方法来帮助接收关于表面的信息。例如,当创建表面时,回调方法surfaceCreated()被调用。当表面被破坏和改变时,分别调用surfaceDestroyed()和surfaceChanged()回调方法。发生变化的一个例子是创建这个类的一个实例,并在表面上画一些东西。
为了处理硬件按键,SurfaceView实现了名为View.OnKeyListener的第二个接口。它有一个名为onKey()的回调方法,当按下一个硬件键时就会被调用。为了处理触摸事件,实现了View.OnTouchListener。它有一个名为onTouch()的回调方法。
由SurfaceView类实现的最终接口是SensorEventListener。它监听传感器数据的变化。它有两种回调方法— onAccuracyChanged()和onSensorChanged()。两者都在类中实现了,但是onAccuracyChanged()是空的。onSensorChanged()用于监控器件方向。
作为在SDLSurface类中定义的所有方法的总结,它的头文件和方法签名如清单 7-27 所示。对SDLSurface类的组件有一个基本的概念会有所帮助。您可以查看所有这些方法的实现,以获得关于该类如何工作的更多信息。
class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
...
public SDLSurface(Context context) {
...
}
public void handleResume() {
...
}
public Surface getNativeSurface() {
...
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
...
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
...
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
...
final Thread sdlThread = new Thread(new SDLMain(), "SDLThread");
...
sdlThread.start();
...
}
// unused
@Override
public void onDraw(Canvas canvas) {}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
...
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
...
}
// Sensor events
public void enableSensor(int sensortype, boolean enabled) {
...
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
...
}
}
Listing 7-27Methods Inside the SDLSurface Class
在实现的SurfaceHolder.Callback接口的surfaceChanged()回调方法中有两行值得一提。这些代码行创建并启动了一个Thread类的实例到sdlThread变量中,该变量引用了主 SDL 线程。在Thread类构造函数中指定了两个参数,它们是目标Runnable对象和线程名。
目标是扩展了Runnable接口的SDLMain类的一个实例。注意,SDLMain类存在于SDLActivity Java 文件中。如清单 7-28 所示。
class SDLMain implements Runnable {
@Override
public void run() {
SDLActivity.nativeInit(SDLActivity.mSingleton.getArguments());
}
}
Listing 7-28Implementation of the SDLMain Class
这个类实现了run()方法,该方法调用一个名为nativeInit()的静态函数。它的头在SDLActivity类头中定义,如下所示。
public static native int nativeInit(Object arguments);
这不是 Java 方法,而是 C 函数。这个方法是 SDL 图书馆的入口点。这个方法和其他 C 方法可以在位于/NewApp/libs/rmeabi-v7a目录下的名为libSDL2.so的 SDL 的源文件中找到。
到了这一步,C 文件就可以执行了,应用将启动并准备好与用户交互。我们可以回到我们的问题,在屏幕加载完成后,我们在 Java 项目的什么地方添加出现在应用活动中的视图。让我们沿着执行链找到合适的位置。
现在可以清楚地看到,SDLMain类中的nativeInit()函数是执行 SDL 库和呈现应用 UI 的入口点。因此,我们必须在调用这个函数之前添加我们的视图。回到这个链,在SDLSurface类的surfaceChanged()方法中创建了一个SDLMain类的实例。记住surfaceChanged()方法是SurfaceHolder.Callback接口内部的回调方法,它监听SurfaceView中的变化。
因为我们希望在应用布局启动后添加视图,所以我们需要找到创建SurfaceView类实例的位置。这是此类课程的第一个变化。记住,有一个名为SDLSurface的类扩展了SurfaceView类。在SDLActivity类的finishLoad()方法中创建了SDLSurface的一个实例。该表面存储在一个名为mSurface的变量中,该变量被声明为静态的,并在类头中受到保护,如下所示:
protected static SDLSurface mSurface;
清单 7-29 显示了创建这样一个实例的finishLoad()方法部分。创建后,它将作为子视图添加到存储在mLayout变量中的应用布局中。最后,使用setContentView()方法将布局设置为活动布局。如果我们需要给活动布局添加一个视图,可以在调用setContentView()方法之前添加。
protected void finishLoad() {
...
mSurface = new SDLSurface(getApplication());
...
mLayout = new AbsoluteLayout(this);
mLayout.addView(mSurface);
setContentView(mLayout);
}
Listing 7-29Instantiating the SDLSurface Class Within the finishLoad() Method
记住,finishLoad()方法是在UnpackFilesTask类的onPostExecute()回调方法中调用的。那是在应用加载它的文件之后。我们可以假设SDLActivity类的finishLoad()方法是准备活动布局的起点。向活动布局添加视图是一个好主意,这些视图只有在应用加载步骤结束后才可见。
在finishLoad()方法的最后,如清单 7-30 所示,创建了两个视图,分别是TextView和Button。使用setTextSize()方法改变文本大小,使用setTextColor()方法将颜色设置为红色。
protected void finishLoad() {
...
// Set up the surface
mSurface = new SDLSurface(getApplication());
if(Build.VERSION.SDK_INT >= 12) {
mJoystickHandler = new SDLJoystickHandler_API12();
}
else {
mJoystickHandler = new SDLJoystickHandler();
}
mLayout = new AbsoluteLayout(this);
mLayout.addView(mSurface);
// Adding Custom Views to the Layout
TextView appTextView = new TextView(this);
appTextView.setText("Loaded successfully.");
appTextView.setTextColor(Color.parseColor("#ff0000"));
appTextView.setTextSize(20);
Button appButton = new Button(this);
appButton.setText("Show Toast");
appButton.setTextColor(Color.parseColor("#ff0000"));
appButton.setTextSize(20);
appButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getApplicationContext(), "Toast on Java Button Click.", Toast.LENGTH_LONG).show();
}
});
LinearLayout newLayout = new LinearLayout(this);
newLayout.setOrientation(LinearLayout.HORIZONTAL);
newLayout.addView(appTextView);
newLayout.addView(appButton);
mLayout.addView(newLayout);
setContentView(mLayout);
}
Listing 7-30Adding a TextView and a Button to the Application Layout Inside the finishLoad() Method
通过使用setOnClickListener()方法指定点击监听器来处理按钮点击动作。这个方法在实现了它的onClick()方法之后接受了一个OnClickListener()接口的实例。单击按钮时,将执行此方法中的代码。单击该按钮会显示一条提示消息。
这两个新视图作为子视图添加到水平方向的父视图LinearLayout中。最后,线性布局作为子布局添加到活动布局中。
图 7-14 显示了点击 Java 中添加的新按钮后的结果。
图 7-14
向 Kivy 应用添加 Android 视图
我们可以修改 toast 消息,以便在单击按钮时打印活动布局中的所有子视图。清单 7-31 中的代码为此修改了onClickListener()回调方法。使用getChildCount()方法返回布局中子视图的数量。为了检索特定的视图,getChildAt()方法接受一个引用视图在父视图中位置的索引,其中索引 0 引用添加到父视图中的第一个子视图。
类似地,除了它们的数量之外,还显示了在前面的例子中创建的LinearLayout中定义的子视图。
appButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int numLayoutChildren = mLayout.getChildCount();
String mLayoutChildren = "Num layout children : "+numLayoutChildren+
"\nChild 1: "+mLayout.getChildAt(0)+
",\nChild 2: "+mLayout.getChildAt(1);
LinearLayout childLayout = (LinearLayout) mLayout.getChildAt(1);
int numChildLayoutChildren = childLayout.getChildCount();
String childLayoutChildren = "Child LinearLayout children : "+
numChildLayoutChildren+"\nChild 1: "+
childLayout.getChildAt(0)+
",\nChild 2: "+childLayout.getChildAt(1);
String toastString = mLayoutChildren+"\n"+childLayoutChildren;
Toast.makeText(getApplicationContext(), toastString, Toast.LENGTH_LONG).show();
}
});
Listing 7-31Editing the onClickListener() Callback Method of the Button View to Print All Layout Child Views Inside a Toast Message
图 7-15 显示点击按钮后的提示信息。它显示布局中的子元素数量为 2。第一个是SDLSurface类的实例,第二个是LinearLayout类的实例。LinearLayout里面的孩子也印出来了。SDLSurface视图包含 Kivy 应用中定义的所有小部件。在后面的小节中,我们将讨论如何处理这类小部件的事件。
图 7-15
单击按钮视图后,显示关于布局中子视图的信息
总之,为了在屏幕加载时向布局添加视图,在PythonActivity类的showLoadingScreen()方法中添加代码。加载完成后,SDL 调用了keepAlive()方法,该方法调用了一个名为removeLoadingScreen()的方法,在该方法中,视图可以在加载后从布局中删除。项目文件加载后,UnpackFilesTask类的onPostExecute()回调方法调用SDLActivity类内名为finishLoad()的方法。这是在屏幕加载后向布局添加视图的好地方。
SDL 如何检测 Kivy 中定义的 UI 小部件?
您可能已经注意到,上一个示例中添加的按钮隐藏了 Kivy 中定义的Label小部件上显示的单词server的一部分。为什么会这样?
当表面被创建时,它的大小根据surfaceChanged()回调方法中的onNativeResize()本地函数调用被设置为整个窗口,如清单 7-32 所示。SDLActivity表面的宽度和高度被设置为窗口的宽度和高度。这没有给在 Java 中添加新视图留下空间。视图堆叠在彼此的顶部。要添加到活动布局中的最新视图的 Z 顺序出现在它们之前添加的视图的顶部。
重要的是将变量mWidth和mHeight设置为等于表面的宽度和高度。稍后会引用它们来处理 Kivy 中添加的小部件的触摸事件。
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
...
mWidth = width;
mHeight = height;
SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate());
...
}
Listing 7-32Setting the Surface Size to the Window Size Inside the SurfaceChanged() Method
在前面的例子中,在添加了 SDL 表面(mSurface)之后,新的线性布局(newLayout)被添加到活动布局(mLayout)。这就是新视图出现在 SDL 表面顶部的原因。
LinearLayout的方向设置为水平。对于水平线性布局,默认宽度和高度设置为WRAP_CONTENT,这意味着线性布局不会填充整个活动布局大小。它只覆盖了容纳其子视图的最小空间。这就是为什么SDLSurface仍然可见。
如果根据清单 7-33 中的finishLoad()方法将 SDLSurface 添加到活动布局中,那么线性布局中的TextView和Button将被 SDL 表面隐藏。
protected void finishLoad() {
...
mLayout.addView(newLayout);
mLayout.addView(mSurface);
...
}
Listing 7-33Adding the SDL Surface to the Activity Layout Before Adding the Custom LinearLayout
我们想到的给每个视图提供独特空间的一个想法是改变 SDL 表面的大小,使其不填满窗口的整个高度。当添加到布局中时,addView()方法可以接受另一个指定给定视图的布局参数的参数。根据清单 7-34 ,当在finishLoad()方法中向布局添加 SDL 曲面时,我们可以将其高度更改为布局高度的 3/4。
屏幕宽度和高度返回到 width 和 height 变量。因为活动布局充满了它的屏幕,我们可以互换使用它们。它们是相对于布局定位 SDL 表面所必需的。addView()方法接受布局参数作为第二个参数。注意,使用了AbsoluteLayout类,因为活动布局是该类的一个实例。
protected void finishLoad() {
...
// Return screen size to position the SDL surface relative to it.
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
// Specifying the new height and Y position of the SDL surface.
mLayout.addView(mSurface, new AbsoluteLayout.LayoutParams(width, height-height/4, 0, height/4));
...
}
Listing 7-34Setting the Height of the SDL Surface to be 3/4 of the Layout Height
表面的宽度被设置为等于屏幕宽度,但是高度被设置为height-height/4。也就是说,布局高度的 3/4 用于 SDL 表面,而活动布局高度的 1/4 是空的,将容纳线性布局。布局从左上角(0,0)开始。如果不改变这个位置,SDL 表面和新的线性布局将出现在彼此之上,如图 7-16 所示。空白区域出现在布局的底部。
图 7-16
SDL 表面覆盖了布局高度的 3/4
Y 位置更改为height/4,以便在顶部为新添加的视图留出 1/4 的布局高度。该效果如图 7-17 所示。
图 7-17
从左下角开始将 SDL 表面放置到布局中
在 Java 中处理 Kivy 控件的触摸事件
活动布局包含作为子视图的SDLSurface。这个视图包含 Kivy 应用的 KV 文件中定义的所有小部件。在本节中,我们将讨论如何在 Android Studio 项目中处理这些小部件的事件。
在 Python 应用的 KV 文件中,四个小部件被添加到一个BoxLayout中。它有四个子部件(一个Button,两个TextInput,一个Label)。由于它的垂直方向,小部件跨越了BoxLayout的整个宽度,但是高度是相等的。屏幕高度除以 4,因此每个小工具的高度是屏幕高度的 25%(1/4)x1.0=0.25)。
因为 SDL 表面的(0,0)点从左上角开始,所以按钮小部件在布局中覆盖的区域的 Y 坐标范围从 0.75 到 1.0。SDL 将该范围内的任何触摸事件与按钮部件相关联。
按钮上方的TextInput控件的 Y 范围从 0.5 到 0.75。因此,SDL 将该区域中的任何触摸事件与该小部件相关联。对第二个TextInput小部件(0.25 到 0.5)和Label (0.0 到 0.25)重复相同的过程。
将表面高度改为屏幕高度的 3/4=0.75 后,小部件的 Y 范围会发生变化。例如,Label widget Y 坐标将从 0.0 开始,但结束于新表面高度的 1/4,即 0.75,也就是0.25x0.75=0.1875。第一个TextInput的 Y 坐标将从 0.1875 开始,到 0.375 结束。因此,从 0.1875 到 0.25 的屏幕 Y 范围现在与TextInput小部件相关联。
通过计算 SDL 表面中每个小部件的 Y 坐标范围,SDL 可以很容易地确定触摸事件的目的地,并采取适当的行动。例如,如果触摸事件的位置是(x=0.1,y=0.9),则是对Button小部件的触摸,因此将调用 Kivy 应用中定义的回调函数。Kivy 小部件的触摸事件处理是在SDLSurface类的onTouch()回调方法中完成的,该回调方法位于SDLActivity Java 文件中。该方法负责处理触摸事件的部分如清单 7-35 所示。
此方法接受目标视图和事件。触摸事件可能有多个要采取的动作,包括按压、释放、移动等等。出于这个原因,一个switch块过滤动作以做出正确的决定。
首先,使用整数变量action中的getActionMasked()方法从事件中返回事件的动作。例如,当触摸被按下时,返回的整数是 0。释放时,整数为 1。
与动作相关的数字在MotionEvent类头中被定义为静态变量。例如,ACTION_DOWN变量的值为 0,而ACTION_UP变量的值为 1。
public boolean onTouch(View v, MotionEvent event) {
...
int action = event.getActionMasked();
switch(action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
i = 0;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
if (i == -1) {
i = event.getActionIndex();
}
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
break;
case MotionEvent.ACTION_CANCEL:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
}
break;
default:
break;
...
}
Listing 7-35Handling the Touch Events of Kivy Widgets Inside Android Studio
在像ACTION_POINTER_DOWN这样的动作中,使用getX()和getY()方法返回触摸事件的 x 和 y 坐标。为了知道其相对于 SDL 表面大小的位置,将它们除以存储在mWidth和mHeight变量中的表面的宽度和高度。请记住,这些变量在surfaceChanged()回调方法中被设置为等于表面的宽度和高度。最后,调用本地函数onNativeTouch()根据 Python 编写的指令处理动作。该方法接受触摸坐标来决定哪个小部件接收事件。
对于动作ACTION_UP、ACTION_DOWN、ACTION_POINTER_UP和ACTION_POINTER_DOWN,执行相同的代码。这就是为什么与动作ACTION_UP、ACTION_DOWN和ACTION_POINTER_UP相关的情况没有break声明的原因。例如,如果动作号为 1,则switch程序块进入与ACTION_UP相关的案例。因为这个案例没有break语句,所以执行下一个案例,也就是ACTION_DOWN。此外,该案例没有break语句,因此进入下一个案例。
类似地,与ACTION_POINTER_UP关联的下一个案例没有break语句,因此与ACTION_POINTER_DOWN关联的案例进入。因为这个案例在结尾包含了一个break语句,所以它的代码被执行,开关断开。
至此,我们已经讨论了使用 Kivy 项目中的 Buildozer 创建的 Java 项目的最重要部分。这有助于我们理解事情是如何工作的,并修改项目以添加与项目一起工作的 Java 组件。
请记住,在 Android Studio 中从事 Kivy 项目的一个目标是,即使使用 Pyjnius,在 Python 中实现某些操作的复杂性也会增加。在下一节中,用 Java 处理一个 Kivy 按钮触摸事件。
在 Android Studio 中处理 Kivy 按钮点击
Python-for-Android 支持一系列可以打包在 Android APK 文件中的库。该列表可在 https://github.com/kivy/python-for-android/tree/master/pythonforandroid/recipes 找到。这些库的例子有 Plyer、requests、OpenCV、NumPy 和 Pyjnius。其中一些库得到了很好的支持并且运行良好,但其他的则不然。通常,从一个使用图像处理库(如 NumPy 和 OpenCV)的 Kivy 项目构建一个 APK 有点挑战性,并且预计会失败。GitHub 中的一些问题尚未修复,尽管 Kivy 开发人员正在努力解决它们。
通过实现在 Python 中不容易实现的组件,我们可以从在 Android Studio 中编辑 Kivy 项目中受益。
在本节中,将创建一个 Kivy 应用,用户在其中选择一个图像文件。Python 中有未处理的Button小部件。在 Android Studio 中,按钮的触摸事件在前面讨论过的onTouch()回调方法中处理。触摸按钮时,使用 OpenCV 库处理选定的图像。我们将从讨论 Kivy 应用开始,然后下载并导入 OpenCV,以便处理按钮触摸事件。
气馁
应用的 KV 文件如清单 7-36 所示。有一个名为FileChooserIconView的新小部件,它显示设备的文件系统。属性指定显示文件和文件夹的路径。它被设置为当前目录。dirselect决定用户是否可以选择目录。它被设置为 False 以防止用户选择目录。将multiselect属性设置为False以避免选择多个文件。我们只需要选择一个图像文件。当用户做出选择时,触发on_selection事件。使用名为load_file()的回调函数在 Python 文件中处理它。
选择图像文件后,通过将图像的 source 属性设置为选择的文件目录,图像显示在Image小部件上。一个Label小部件显示信息性消息,比如指示用户是否选择了图像。
Python 中不处理Button小部件。它将在 Android Studio 项目中处理。
BoxLayout:
orientation: "vertical"
Image:
id: img
FileChooserIconView:
id: fileChooser
path: "."
dirselect: False
on_selection: app.load_file()
Label:
font_size: 20
text_size: self.width, None
id: selectedFile
text: "Please choose an image (png/jpg)."
Button:
font_size: 20
text: "Process image in Java"
Listing 7-36KV File for a Kivy Application with a Button to be Handled Within Android Studio
图 7-18 显示了运行应用后的结果。注意,Image小部件上没有显示图像,因此它的颜色只是一个白色区域。
图 7-18
在清单 7-36 中创建的应用窗口
Python 文件如清单 7-37 所示。应用类被命名为AndroidStudioApp,因此 KV 文件名必须是androidstudio.kv,以便将其隐式链接到应用。该类实现了load_file()回调函数。即使选择了一个目录,也会调用该函数。
import kivy.app
import shutil
import os
class AndroidStudioApp(kivy.app.App):
def load_file(self):
fileChooser = self.root.ids['fileChooser']
selectedFile = self.root.ids['selectedFile']
img = self.root.ids['img']
file_dir = fileChooser.selection[0]
file_path, file_ext = os.path.splitext(file_dir)
file_name = file_path.split('/')[-1]
print(file_name, file_ext)
if file_ext in ['.png', '.jpg']:
img.source = file_dir
try:
shutil.copy(file_dir, '.')
selectedFile.text = "**Copied**\nSelected File Directory : " + file_dir
os.rename(".join([file_name, file_ext]), ".join(["processImageJavaOpenCV", file_ext]))
except:
selectedFile.text = "**Not Copied - File Already Exists**\nSelected File Directory : " + file_dir
print("File already exists")
os.rename(".join([file_name, file_ext]), ".join(["processImageJavaOpenCV", file_ext]))
else:
selectedFile.text = "The file you chose is not an image. Please choose an image (png/jpg)."
app = AndroidStudioApp()
app.run()
Listing 7-37Python File of the Application in which an Image File Is Loaded to be Processed in Android Studio Using OpenCV
该函数从访问 KV 文件中定义的小部件开始。文件选择器返回到fileChooser变量。即使禁用多选,所选的文件目录也会返回到一个列表中,可以使用selection属性访问该列表。Index 0 将所选文件的目录作为字符串返回到file_dir变量中。
因为不能保证所选文件是图像,所以我们需要通过检查其扩展名来验证它是图像。使用os.path模块中的splitext()函数,文件目录被分为扩展名和路径。扩展返回到file_ext变量,路径返回到file_path变量。文件路径可以如下所示:
NewApp/kivy/img
使用split()函数对file_path进行分割,以便将文件名返回给file_name变量。分离器是/。结果是如下所示的列表。文件名是可以使用-1索引的最后一个元素。该文件名稍后将用于重命名选定的文件。
['NewApp', 'kivy', 'img']
将选定的文件扩展名与目标扩展名列表进行比较。png和。jpg。如果列表等于其中任何一个,我们就可以确定选择的文件是一个图像。因此,Image小部件的 source 属性被设置为图像目录。
如果扩展名不是.png或.jpg,则在Label小工具上打印一条消息,通知用户选择不正确。
使用shutil库中的copy()函数将图像从其原始路径复制到当前目录。如果文件已经存在于目标目录中,这个副本将抛出异常。这就是为什么它被写在try块中的原因。在except块中,Label小工具指示该图像存在于当前目录中,如图 7-19 所示。
图 7-19
选择当前目录中存在的图像文件
在 Java 文件中,我们需要一种方法来表示图像文件的名称。我们可以通过将 Java 文件中要处理的图像的名称设置为一个固定的名称来标准化这个过程。选中的名字是processImageJavaOpenCV。使用os.rename()函数,指定旧的和新的文件名。使用join()函数,文件名及其扩展名被连接起来。这是在try和except模块内完成的。
在确保 Python 应用运行良好之后,我们需要使用 Buildozer 构建 Android 项目。产生的项目将在 Android Studio 中打开,就像上一个项目中所做的那样。请记住将编译 SDK 和构建工具版本设置为您的 PC 所需的版本。
检测 Java 内部按钮控件的触摸事件
请记住,所有 Kivy 小部件都绘制在SDLActivity类中定义的 SDL 表面内。当触摸表面时,在onTouch()回调方法中返回触摸事件位置。这个位置与每个 Kivy 小部件的区域进行比较。该事件与其区域中发生触摸的部件相关联。在本节中,我们将检测 Kivy 按钮的区域。
与上一节“在 Java 中处理 Kivy 小部件的触摸事件”中的例子类似,小部件被添加到 KV 文件中的一个垂直方向的BoxLayout中。因此,屏幕高度在所有子部件之间平均分配。注意,屏幕和BoxLayout可以互换使用,因为布局充满了屏幕。
因为屏幕有四个小部件,每个小部件的高度是屏幕高度的 25%。因此,按钮小部件相对于屏幕高度的 Y 坐标值从 Y=0.75 开始,到 Y=1.0 结束。在屏幕的这个区域发生的任何触摸事件都与Button小部件相关联。
修改后的处理按钮触摸事件的onTouch()方法如清单 7-38 所示。如果我们对触摸释放后的事件处理感兴趣,代码将被写入与ACTION_UP相关联的案例中。
使用if语句,将触摸位置的 Y 坐标与按钮的 Y 值范围进行比较。如果触摸落在此范围内,处理触摸事件所需的操作被添加到if块中。目前,只显示了一条祝酒词。稍后,在将 OpenCV 与项目链接后,一些操作将应用于所选图像。
因为 Kivy 项目中没有与小部件相关联的动作,所以我们不需要调用onNativeTouch()。返回触摸事件的 X 和 Y 坐标就足以确定目标小部件。
在if块的末尾有一个break语句,以避免执行更多的情况。如果触摸在范围之外,将通过执行与ACTION_POINTER_DOWN相关的案例中的代码来应用之前遵循的正常程序。
public boolean onTouch(View v, MotionEvent event) {
...
switch(action) {
case MotionEvent.ACTION_MOVE:
...
case MotionEvent.ACTION_UP:
if (i == -1) {
i = event.getActionIndex();
}
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
if (y >= 0.75){
Toast.makeText(this.getContext(), "Button Clicked", Toast.LENGTH_LONG).show();
break;
}
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
...
case MotionEvent.ACTION_CANCEL:
...
}
Listing 7-38Handling the Touch Event of the Kivy Button Widget Within the onTouch() Method of the Android Studio Project
用修改后的onTouch()方法构建并运行 Android 项目后,按下按钮,结果如图 7-20 所示。下一步是将项目与 OpenCV 链接起来,以便处理图像。
图 7-20
处理 Android Studio 中 Kivy 按钮小部件的触摸事件
在 Android Studio 中导入 OpenCV
OpenCV 支持一个可以在 Android 设备上工作的版本。目前 Android 的最新版本是 3.4.4。可以在 https://sourceforge.net/projects/opencvlibrary/files/3.4.4/opencv-3.4.4-android-sdk.zip 下载。
OpenCV 将作为一个模块导入 Android Studio,以便在 Java 代码中访问它。在 Android Studio 的文件菜单中,进入新建,然后选择导入模块。这将打开一个窗口,询问模块源目录。您可以复制并粘贴该目录,也可以在文件系统中导航找到它。如果将压缩文件解压到名为opencv-3.4.4-android-sdk的文件夹中,那么要复制粘贴的目录就是\opencv-3.4.4-android-sdk\OpenCV-android-sdk\sdk\java。当您进入目录时,模块名称会自动检测为openCVLibrary344。
由于 SDK 和构建工具的版本不一致,导入模块后可能会出现错误。导入的模块中有一个build.gradle文件。您可以更改编译、最低和目标 SDK 版本,以匹配您计算机上的版本。这是对构建工具版本的补充。这类似于我们之前在项目本身的build.gradle文件中所做的。
下一步是将导入的库作为依赖项添加到 Android 项目中。选择“文件”菜单,然后选择“项目结构”菜单项。根据图 7-21 ,这将打开一个窗口,其中项目名称位于模块部分下。OpenCV 库也在列表中。
图 7-21
Android Studio 项目的结构
转到“依赖项”选项卡,然后单击“添加”按钮。然后选择模块依赖,如图 7-22 。这将打开另一个窗口,其中列出了 OpenCV。选择它,然后单击“确定”将其添加为依赖项。再次单击“确定”关闭“项目结构”窗口。
图 7-22
在 Android Studio 项目中添加 OpenCV 作为依赖库
最后一步是在 OpenCV \OpenCV\opencv-3.4.4-android-sdk\OpenCV-android-sdk\sdk\native下的这个路径中复制一个名为libs的文件夹。将文件夹名称改为jniLibs后,粘贴到 Android Studio 项目下的\KivyAndroidStudio\src\main目录中。经过这一步,OpenCV 就可以使用了。
对所选图像应用 Canny 滤镜
在使用 OpenCV 之前,我们必须确保它已经加载。这可以使用清单 7-39 中的if语句来完成。如果库没有成功加载,您可以在它的块中处理它。它可以被添加到SDLActivity类的onCreate()方法中。
if (!OpenCVLoader.initDebug()) {
// Handle initialization error
}
Listing 7-39An if Statement to Ensure Loading OpenCV
清单 7-40 显示了加载 OpenCV 的修改后的onCreate()方法。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
screenWidth = displayMetrics.widthPixels;
screenHeight = displayMetrics.heightPixels;
if (!OpenCVLoader.initDebug()) {
// Handle initialization error
}
SDLActivity.initialize();
// So we can call stuff from static callbacks
mSingleton = this;
}
Listing 7-40Loading OpenCV Within the onCreate() Method
OpenCV 加载后,我们就可以使用它了。清单 7-41 中的代码将选中的图像读入一个名为inputImage的变量。它假设图像扩展名是.jpg。可以加一点修改支持.png和.jpg。
Canny 边缘检测器应用于该图像。Canny 的合成图像保存到另一个名为outputImage的变量中。该图像被保存为一个.jpg文件。
一个ImageView被添加到 SDL 活动布局中,显示保存的图像。ImageView被添加到与 KV 文件中定义的文件选择器小部件相关联的区域。
为了在屏幕上正确定位ImageView,需要屏幕宽度和高度。在SDLActivity类头中定义了两个名为screenWidth和screenHeight的静态整数变量。在onCreate()方法中,屏幕宽度和高度被计算并分配给它们。
Toast.makeText(this.getContext(), "Canny is being processed...", Toast.LENGTH_LONG).show();
Mat inputImage = Imgcodecs.imread("processImageJavaOpenCV.jpg");
Mat outputImage = inputImage.clone();
Imgproc.Canny(inputImage, outputImage, 100, 300, 3, true);
Imgcodecs.imwrite("RESULTImageJavaOpenCV.jpg", outputImage);
File cannyImage = new File("RESULTImageJavaOpenCV.jpg");
ImageView imageView = new ImageView(this.getContext());
if(cannyImage.exists()){
Bitmap myBitmap = BitmapFactory.decodeFile(cannyImage.getAbsolutePath());
imageView.setImageBitmap(myBitmap);
SDLActivity.mLayout.addView(imageView, new AbsoluteLayout.LayoutParams(SDLActivity.screenWidth, SDLActivity.screenHeight/4, 0, SDLActivity.screenHeight/4));
}
Listing 7-41Using OpenCV to Apply the Canny Edge Detector to the Loaded Image
清单 7-41 中的代码可以添加到与ACTION_UP关联的案例中的onTouch()方法中。这将在按下 Kivy 中定义的Button小部件后执行代码。图 7-23 显示了我们按下按钮后的结果。
图 7-23
OpenCV 用于将 Canny 边缘检测器应用于加载的图像
摘要
Python 本身不能构建丰富的 Android 应用,这就是为什么本章讨论了用 Kivy 以不同的方式丰富 Python 中创建的 Android 应用。我们讨论的第一个库是 Plyer,它允许我们使用原生 Python 代码访问 Android 特性。因为许多特性还没有在 Plyer 中实现,所以讨论了另一个名为 Pyjnius 的库,它反映了 Python 中访问 Java 特性的 Java 类。不幸的是,Pyjnius 增加了构建 Java 特性的复杂性,这不是丰富 Android 应用的推荐方法。最理想的方法是编辑使用 Buildozer 导出的 Android Studio 项目。这样,在 Android 应用中可以做的事情就没有限制了。在导入导出的 Android Studio 项目后,Python 中没有的任何内容都可以添加到 Java 中。
本章首先讨论了项目,在进行任何编辑之前突出显示了项目中的重要文件。这些文件包括主 Java 活动、清单文件、字符串资源文件等等。之后,这些文件经过编辑,为 Android 应用添加了更多功能。这包括编辑加载屏幕布局、向主活动添加 Android 视图、处理两个小部件的点击动作、显示 toast 消息等等。本章还在 Android Studio 项目中引入了 OpenCV,以便在图像上应用简单的滤镜。
最后,本章证明了即使 Python 在构建 Android 应用方面受到限制,用 Python 创建丰富的 Android 应用也是可能的。这是通过编辑导出的 Android Studio 项目并进行必要的更改以添加更多功能来实现的。