Python开发Android新选择--Virgo库开发日记(1)

文章介绍了Python在安卓开发中的挑战,如Kivy和BeeWare的限制,以及p4a的复杂性。作者发现了chaquopy这一解决方案,允许在安卓中集成Python环境,但仍然需要Java代码配合。为解决这些问题,作者开发了Virgo库,利用Android原生控件构建界面,提供更接近原生应用的用户体验。文章通过代码示例展示了如何使用Virgo创建Activity和切换页面,并强调了Virgo库目前的限制和未来发展方向。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

因为我们学校被用作高考考场了,所以终于有10天时间发篇博客了(笑)

咳咳,进入正题

01 引入

众所周知,Python开发安卓好像就三个选择:Kivy,BeeWare,p4a(Python-for-android)。这三个,我体验了前两个,发现几个问题:

1.Kivy是采用自研的UI和设计,导致效率低不说,很多Python第三方库都不能用...

2.BeeWare也有这个问题,很多Python第三方库都不能用,并且虽然是原生UI,但是很多安卓中的特性都不支持

3.p4a的问题就更大了,前提就得是有台Linux机器,打包环境配置及其费劲,还极大概率会出错,捣鼓半天都不一定有效,这把我们宝贵的时间全都浪费了....

下面给大家稍稍看一下页面区别(部分图片来自网络)

 1.Kivy与原生开发的页面效果对比

Kivy的页面效果

2.BeeWare(图片来自网络)

 可以看到,Beeware页面确实好了不少,但是据我所知好像它是不支持安卓传感器的,也不适合开发大型项目,只适合开发小工具,效果也不佳

当然,也不能光说人家的缺点,它们对于跨平台都有着较好的支持,如果你需要一次编写,处处运行的效果,就可以试试这两个库

02 尝试

“Python开发安卓应用怎么这么费劲,我想写点东西方便移动使用都没办法....”

"网上也没有对应的支持,那怎么办....."

"要不,自己写一个库来实现Python跨平台-安卓开发?"

说干就干,还真让我找到了方法:chaquopy

这是一个开源的,免费的Android插件(虽然最近才刚开源),可以帮你在安卓中打包进Python环境,再利用JNI技术实现调用Python;理论上只要没有平台依赖的第三方库(比如pywin32肯定不行)都可以被调用,诸如requests,tensorflow,pytorch,numpy好像都可以!但是这毕竟只是插件,没办法完全使用Python开发,还要写Java代码,好麻烦的说

网上chaquopy资料很少,基本上都是复制粘贴官网的极个别样例,只有一份全英文的文档摆在那..

没办法,百度翻译+文档生啃了一星期终于学明白了,(吐槽:它的类继承写的真有够麻烦的,还有什么动态代理和静态代理,头大)

03 Virgo库

先看效果(原本速度不是这么慢的,因为转换为GIF导致速度变慢了。原本的速度和普通APP完全无区别)

此APP除去Virgo库的代码,页面的控制逻辑完全采用Python构建

"Virgo库是什么?"

"为什么我pip安装不了?"

额,这是我正在写的一个项目,还没有完全囊括安卓的方方面面,所以先不上传pip了

(这个名字翻译过来是处女座,我觉得挺好听的(笑))

 

所以这个是什么?为什么Python可以写出这么好的页面?

原因是:这些页面完全不是用Python构建的,而是用安卓本身提供的控件构建的!也就是说,你可以用这个库做到与其他大型软件完全没有区别的UI设计,只不过后台逻辑语言由Java换成了Python而已!所有的元素,诸如按钮,输入框,信息对话框等等全都是Android提供的!甚至,你可以自由使用网上其他人写的安卓页面拓展!

03-1代码分析

让我们一部分一部分地看代码

.0 前提条件,准备两个XML文件用于写页面,完全使用Android开发者的方法写页面(后期我打算用我自制的一门标记语言来简化这个过程,已经写好了),这里只放出一小部分,大家看看就好

第一个页面的XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:id="@+id/mylayout">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="60dp"
        android:paddingBottom="40dp"
        android:text="@string/login"
        android:textColor="#E6941A"
        android:textSize="30sp" />

    <EditText
        android:id="@+id/usr_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autofillHints="username"
        android:background="@drawable/et_style"
        android:drawableStart="@mipmap/usr"
        android:drawablePadding="10dp"
        android:hint="@string/usr_hint"
        android:minLines="1"
        android:paddingLeft="10dp"
        android:paddingTop="10dp"
        android:paddingRight="10dp"
        android:paddingBottom="10dp"
        android:textColor="#2BD5B3"
        tools:ignore="InvalidId" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/pwd_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:autofillHints="username"
        android:background="@drawable/et_style"
        android:drawableStart="@mipmap/password"
        android:drawablePadding="10dp"
        android:hint="@string/pwd_hint"
        android:minLines="1"
        android:paddingLeft="10dp"
        android:paddingTop="10dp"
        android:paddingRight="10dp"
        android:paddingBottom="10dp"
        android:selectAllOnFocus="true"
        android:textColor="#2BD5B3" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />


    <Button
        android:id="@+id/login_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/btu"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:text="@string/login_btn"
        android:textColor="#FFFFFFFF" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/register_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/register_btn"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:text="@string/register_btn"
        android:textColor="#FFFFFFFF" />

</LinearLayout>

第二个页面的XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/mylayout2"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="5dp"
    android:paddingTop="20dp"
    android:paddingRight="5dp"
    android:paddingBottom="20dp">

    <TextView
        android:id="@+id/textView3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="欢迎来到用于测试的第二页!" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="切回到上一页" />

    <Switch
        android:id="@+id/switch1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开启测试开关1"
        tools:ignore="UseSwitchCompatOrMaterialXml" />

    <Switch
        android:id="@+id/switch2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="开启测试开关2"
        tools:ignore="UseSwitchCompatOrMaterialXml" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="感谢" />

</LinearLayout>

大家看看就好,关键的是类似 android:id="@+id/.."的代码,里面的【..】对应ID名称我们后面在Python代码中要用到;是不是这种构造方法挺费劲的,还需要写一大堆没啥意义的属性,很麻烦,所以我正准备移植自制的标记语言

1. 导入必要的模块和类,我都已经写注释了,大家可以自己看看

from Virgo.android.APP.app import AndroidAPP     # 导入AndroidAPP类,用于调控和管理各个Activity和切换
from Virgo.android.ui.Element.Toast import toast      # 导入吐司消息条(额,就是那个底部出现的小黑条消息,一会就没了的那种)
from Virgo.android.core_components.Activity import ControlActivity       # Activity对象
from Virgo.android.core_components.View_Listeners import VOnClickListener       # 按钮回调监听器
from Virgo.android.ui.Element.AlertDialog import Builder        # 导入信息对话框控件
from Virgo.android.core_components.Bundle import VBundle        # 导入Bundle,用于传输数据

 如果你用Java写过Android,那你肯定对这些东西很熟悉!因为这根本就是Java的Python绑定诶!

 2. 写自己的Activity类,继承ControlActivity类,用于定义不同页面的逻辑

第一个Activity

class MyActivity(ControlActivity):  # 首先,写一个自己的Activity,名字随便,但要继承ControlActivity
    def __init__(self, APP):
        super().__init__("MainActivity", APP)  # 注意,父类的构造方法有两个参数,第一个为Activity名(必填,以后有用),第二个是APP对象,下面会实例化,不要着急
        self.weights = {}  # 这个是自己定义的,用来存控件的

    def check_password(self):  # 自己定义的函数,用来检测账号密码
        usr = self.weights["usr_input"].getText().toString()  # 这个字典对象后文会赋值,获取目标控件并获取内部文字
        pwd = self.weights["pwd_input"].getText().toString()
        if usr == "cemeye" and pwd == "pwd":  # 检测是否是对应的账号密码
            toast(self, "账户密码正确!")  # 用吐司消息显示提示信息
        else:
            # 如果账号密码错误,显示提示对话框
            # Builder需要传入Activity对象,这里用self传入即可(对应代码:Builder(self))
            # set_title("来自开发者的提醒~")设置消息题目
            # set_message("..")设置信息内容
            # set_positive_button("确定", None)  设置按钮参数,如果需要,第二个参数的None可改为VOnClickListener并绑定回调函数,None表示按下按钮什么都不做
            # showMessage()显示消息,进入消息循环,此时这个Activity会进入Pause状态,会调用onPause()函数,如果你有需要,可以重写这个类的onPause()函数来执行对应功能
            # 类似的,还有onCreate等方法,在外部列出
            Builder(self).set_title("来自开发者的提醒~").set_message(
                "您输入的账号或密码有误。对了,这个库是我开发出来玩的哦~我觉得Java太麻烦,还是咱Python最好用对吧?所以我写了这个库").set_positive_button(
                "确定", None).showMessage()
            # 为输入框设置文字
            self.weights["usr_input"].setText("")
            self.weights["pwd_input"].setText("")

    def change_activity_to(self):
        # 又是自定义函数,用于切换Activity
        data = self.weights["usr_input"].getText().toString()
        # bundle用于在不同的Activity间传递额外的消息,这个是安卓必须得写法
        bundle = VBundle().putString("name", data)  # 添加字符串类型的键值对,"name"为键,data为值,可在后文第二个Activity中通过键获取值
        self.APP.change_activity("Activity2",
                                 self_defined_bundle=bundle)  # 切换Activity到"Activity2",这里是前面那个【Activity名(必填,以后有用)】的伏笔
        # self.finish()   这个是结束当前Activity的方法,也就是说你调用这个方法后再按返回键返回不了前一个页面了,而是直接关闭了

    def onCreate(self, savedInstanceState):
        # 重写的方法,在Activity创建时会调用基本上所有Activity都会重写这个方法
        # 题外话:在安卓的Java中也是这种方式,也需要重写Activity的这个方法!

        # 设置Activity的XML布局文件,为其设置布局
        self.setContentView("activity_main")
        # 用你在XML文件中填写的ID,获取对应的控件,并保存在self.weights这个字典里
        self.weights["usr_input"] = self.findViewById("usr_input")
        self.weights["pwd_input"] = self.findViewById("pwd_input")
        self.weights["login_btn"] = self.findViewById("login_btn")
        self.weights["rgr_btn"] = self.findViewById("register_btn")
        # 设置按钮回调函数的方法,挺麻烦的,不过这是安卓的写法移植,也就是说,安卓Java中也是这样,甚至要更麻烦
        self.weights["login_btn"].setOnClickListener(VOnClickListener().register_onClick(
            lambda view: self.check_password()
        ))
        self.weights["rgr_btn"].setOnClickListener(VOnClickListener().register_onClick(
            # lambda view: self.APP.startActivityForResult("Activity2", 100)
            
            # 这里用来开启第二个Activity,这就是为什么点击了第二个按钮后会跳转
            lambda view: self.change_activity_to()
        ))

    def onActivityResult(self, requestCode, resultCode, data):
        """
        当目的Activity结束并且返回结果时调用
        :param requestCode: 期望的状态码(用于和resultCode做比较,判断结果是不是自己想要的)
        :param resultCode: 返回的状态码
        :param data: 数据Intent对象
        :return: None
        """
        # 这个是一个挺高级的用法了,如果前文采用注释中的【self.APP.startActivityForResult("Activity2", 100)】取代【lambda view: self.change_activity_to()】
        # 则第二个Activity的启动是带有目的性的,是为了返回结果给这个Activity的,所以当第二个Activity被销毁或暂停时,会返回结果给这个函数,也会执行这个函数
        # 别急,这也是Android Java的标准写法,都得这么写,Virgo只不过是移植了这种写法,或者说封装了这种写法?也就是你学过Android开发的话,开发Virgo就跟玩一样,
        # 里面的方法基本上都是一样的,流程也是一样的
        try:
            # Intent获取数据的方法,返回的是个Bundle对象,她有个方法是get,可以通过键获取值,前文也提到过
            data = data.getExtras()
            # 吐司消息显示~
            toast(self, str(data.get("data")))
        except Exception as e:
            ...

第二个Activity(我就略写了哈)

class Activity2(ControlActivity):
    def __init__(self, APP):
        # 一样的,前面
        super().__init__("Activity2", APP)
        # 设置是否调用onBackPressed的super方法?
        # 这里的原因是,如果想实现【再按一次退出】的效果,必须要取缔原来onBackPressed的父类方法
        # 因为她父类的方法是点一次就退出当前Activity,不调用父类方法的效果就是,不管你按多少次返回键都退不出页面,只能自己写处理逻辑
        # 相应地,其他的可重写函数也可以通过这种方式不调用super,但不建议这样做
        self.if_call_super["onBackPressed"] = False

        # 数一下按了几次?
        self.press_times = 0

    def try_me(self):
        # 瞎起的方法名,self.finish()用于结束当前Activity
        self.finish()

    def onCreate(self, savedInstanceState):
        # 也是重写onCreate方法,不解释
        # 这个savedInstanceState,就是上文提到的传家宝,你可以通过 self.getBeforeData("name")的方法获取值
        self.setContentView("activity2")
        self.btn = self.findViewById("button")
        self.btn.setOnClickListener(VOnClickListener().register_onClick(
            lambda view: self.try_me()
        ))
        # 传家宝写法
        data = self.getBeforeData("name")
        print("!来自二号Activity!数据传过来的是:", data)
        self.findViewById("textView3").setText("你好!亲爱的[" + str(data) + "]")

    def onBackPressed(self):
        # 诺,重写的BackPressed方法,用来执行【按下返回键后的操作】
        self.press_times += 1
        # 恶搞一下,必须按够3次才能退出页面。你要是设置成100我也不拦着你(笑)
        if self.press_times >= 3:
            # 设置结果(如果当前Activity是以【目的性Activity】的方式启动【即前文startActivityForResult】)
            # 一般用Bundle设置结果
            self.set_result(100, bundle=VBundle().putString("data", "这是来自第二个Activity的数据!"))
            # 结束当前Activity
            self.finish()

额,说是略写,写着写着突然注释写多了

3.创建AndroidAPP对象,添加你创建的Activity

class MainAPP(AndroidAPP):
    def __init__(self):
        super().__init__()
        self.append_activities([
            MyActivity(self),
            Activity2(self)
        ])
        # 绑定主Activity
        self.bind_activity("MainActivity")

这个,顾函数名思义就理解了,不写注释了

重要提醒:

当前项目是无法直接执行的,必须要经过Virgo打包APK才能执行,所以你直接右键【运行】10000%会报一个ModuleNotFoundError: No module named 'android',因为这个模块是在Android环境中动态赋予的!

emm,Virgo写的还是太简陋,所以不大好意思上传到pip上(笑)

下一步打算开发传感器支持!

 附录,Activity的可重写函数与对应作用(有删减)

1. onCreate():当Activity被创建时调用,通常在此方法中进行布局的初始化和数据的加载。

2. onStart():当Activity可见但未获得焦点时调用。

3. onResume():当Activity获得焦点并开始活动时调用。

4. onPause():当Activity失去焦点但仍可见时调用,通常在此方法中保存数据和释放资源。

5. onStop():当Activity不再可见时调用,通常在此方法中释放资源。

6. onDestroy():当Activity被销毁时调用,通常在此方法中释放所有资源。

7. onRestart():当Activity从停止状态重新启动时调用。

10. onActivityResult():当Activity启动的子Activity返回结果时调用,用于处理返回的结果。

11. onBackPressed():当用户按下返回键时调用,通常在此方法中处理返回键的逻辑。

14. onConfigurationChanged():当设备配置发生改变时调用,例如旋转屏幕或改变语言设置。

 

作者最近才开始用优快云,给个赞再走吧

(有问题或建议欢迎提出!不过我可能只有周末才能回复)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值