安卓环境下Lua安卓开发初体验

安卓环境下Lua安卓开发初体验

家里老人年纪大了,记不住手机怎么操作,两天问了五遍怎么全删图片,遂尝试安卓开发,一键清空图片/视频。一开始尝试 Java,虽然有过 Java 学习经验,但短时间上手安卓实在头大,选择了更简单灵活的Lua

推荐阅读:10分钟用安卓手机开发安卓

环境

工具:NeLuaJ+,可选 MT 管理器,一个好用的ai(ds,copilot…)

环境:安卓

所有用到的工具和成果 蓝奏云https://wwre.lanzouq.com/b0ul17nve 密码:8rqk

NeLuaJ+ Builder为配套构建工具,NeLuaJ+为编写工具

目标安卓版本:安卓9 & 安卓12

创建项目

进入 NeLuaJ+ ,右上角 竖着的三个点 选择 项目...创建项目

Screenshot_20250209_085812

根据需要修改软件名称和包名,选择需要的模块。以 一键清空图片/视频 为例,修改名字后直接创建

起名为 imgCleaner ,右上角 横着的三条杠 下进入根目录 /storage/emulated/0/LuaJ/Project/imgCleaner/

下文所有 ./ 即为 /storage/emulated/0/LuaJ/Project/imgCleaner/,也就是项目根目录下

image-20250209091439376

编写布局

预期效果为:两个按钮,一个一键删掉图片,一个一键删掉视频

先创建两个按钮(Button),进入 ./res/layout/main.lua ,这里编写布局

1
2
3
4
5
6
7
8
9
10
11
return {  
LinearLayout, --线性布局
orientation="vertical", --竖向排列
layout_width="match", --宽度最大
layout_height="match", --高度最大
gravity="center", --子空间居中
{
AppCompatTextView, --文本控件
text="Hello NeLuaJ+", --文字
},
}

默认提供了在屏幕正中央显示 Hello NeLuaJ+ 的样例文字。编写过程中可以随时在 屏幕正上方的黑色三角形运行工程 中测试软件。

删掉文本,添加两个Button控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "android.widget.*", "androidx.appcompat.widget.*";
return {
LinearLayout, --线性布局
orientation="vertical", --竖向排列
layout_width="match", --宽度最大
layout_height="match", --高度最大
gravity="center", --子空间居中
{
Button, --按钮控件
text="清理图片", --按钮显示的文字(可选,没有text的话会显示没有文字的按钮)
id="imgclean", --按钮id,这个id是独一无二指向这个button,支持中文(不建议中文编程)
layout_width = "200dp", --按钮宽度(可选)
layout_height = "100dp", --按钮高度(可选)
},
{
Button,
text="清理视频",
id="movclean",
layout_width = "200dp",
layout_height = "100dp",
}
}

添加好控件后尝试运行,成功显示两个按钮

Screenshot_20250209_092427

编写逻辑

现在按钮没有功能,需要返回到 ./main.lua 编写

1
2
3
4
5
6
7
import "java.lang.*","java.util.*"
import "android.os.*","android.app.*"
import "android.content.*","android.provider.*"
activity {
Title = res.string.app_title,
ContentView = res.layout.main
}

在默认提供的模板下方编写逻辑

首先编写两个按钮点击逻辑

1
2
3
4
5
6
7
imgclean.onClick=function() -- ButtonId.onClick=function() 为起始语句
deleteMedia("image") -- 调用函数,deleteMedia稍后编写
end -- end为结束语句

movclean.onClick=function()
deleteMedia("vedio")
end

这段相对好理解,当按下对应id的按钮后,对应的事件为:调用deleteMedia()函数,传入一个参数(需要删除的文件类型),然后end结束

下面编写 deleteMedia(),首先写个架子

1
2
3
4
5
6
7
8
9
10
11
12
13
function deleteMedia(type)
local uri
if type == "image" then
print("正在清理图片")
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
end
if type == "vedio" then
print("正在清理视频")
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
end

......
end

type即为按钮传进来的参数,如果学过其他语言,这里type应该好理解,就是形参

对文件进行操作,需要知道文件在哪。安卓提供了 访问共享存储空间中的媒体文件 的方法

访问共享存储空间中的媒体文件

科普

URI(Uniform Resource Identifier)统一资源标识符,一条用来标识抽象或物理资源的紧凑字符串,通过这个标识可以访问一个唯一的资源
URL(Uniform Resource Locator)统一资源定位符

访问媒体资源,可以通过 MediaStore.media-type.Media.EXTERNAL_CONTENT_URI 得到媒体资源uri

其中 media-type 就是媒体资源的类型

文件类型 media-type
图片 Images
视频 Video
音频 Audio
下载的文件 Downloads

以视频为例,视频的uri为 MediaStore.Video.Media.EXTERNAL_CONTENT_URI

lua 定义变量参考 Lua 变量

知道文件在哪(uri)了以后,就可以删除了。继续编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deleteMedia(type)
local uri
local Type
if type == "image" then
Type = "图片"
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
end
if type == "vedio" then
Type = "视频"
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
end

local contentResolver = activity.getContentResolver()
local rowsDeleted = contentResolver.delete(uri, nil, nil)
print(tostring(rowsDeleted).."个"..Type..清理完成)
end

local contentResolver = activity.getContentResolver() 获得了当前活动的内容解析器(Content Resolver)。内容解析器是一个接口,提供对应用程序内数据的访问。

local rowsDeleted = contentResolver.delete(uri, nil, nil):使用内容解析器删除指定 URI 的数据。

Content Resolverdelete() 函数有三个参数

原型:delete(Uri uri, String where, selectionArgs)

第一个参数为uri,第二三个参数为约束条件,返回被删除的行数,nil 表示没有选择条件

print内容为字符串,需要转换 rowsDeleted 类型,使用 tostring() 函数即可

在print内容中添加变量使用 .. (个人理解.. 相当于 +

主要逻辑便编写完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function deleteMedia(type)
local uri
local Type
if type == "image" then
print("正在清理图片")
Type = "图片"
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
end
if type == "vedio" then
--print("正在清理视频")
Type = "视频"
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
end

local contentResolver = activity.getContentResolver()
local rowsDeleted = contentResolver.delete(uri, nil, nil)
print(tostring(rowsDeleted).."个"..Type.."清理完毕")
end

imgclean.onClick=function()
deleteMedia("image")
end
movclean.onClick=function()
deleteMedia("vedio")
end

权限申请

安卓历代文件保护可以看这篇文章 【安卓基础】一文搞懂Android历代版本文件访问权限变化

./init.lua 中,有 target_sdk min_sdk user_permission 三个选项

target_sdk min_sdk 分别为目标sdk和最低sdk

科普:SDK是什么

“SDK” 是 Software Development Kit 的缩写,中文译为软件开发工具包。它是一组用于开发软件的工具、库、示例代码和文档的集合,旨在帮助开发者更容易地构建、集成和使用特定的软件或服务。

不同安卓版本对应不同的sdk,参考文章 Android版本与SDK/API版本、JDK对应关系

target_sdk过高,对文件操作不能只在user_permissionWRITE_EXTERNAL_STORAGE ,需要动态申请权限,也就是在程序运行时申请权限,诸如平常安装新应用,弹窗申请调用xxxx,申请xxxx访问权限一样

这里是动态申请权限模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
--Android动态权限申请,回调和检查
--权限的申请应当遵循“谁需要,谁申请”,权限通过则执行,不通过则不执行
--既不应该一启动就全申请,也不应该申请不应需要的权限
--假设有某个需求需要权限
function fun()
print("某个需要权限的操作")
end
--定义请求码
local permissionRequestCode = 10
--申请权限
function requestPermissions(permissions, requestCode)
local ActivityCompat = luajava.bindClass "androidx.core.app.ActivityCompat"
return ActivityCompat.requestPermissions(activity, permissions, requestCode);
end
--申请权限的回调在这里执行
onRequestPermissionsResult=function(requestCode, permissions, grantResults)
local PackageManager = luajava.bindClass "android.content.pm.PackageManager"
--判断是不是自己的权限申请
if requestCode==permissionRequestCode then
local count = 0
local Manifest = luajava.bindClass "android.Manifest"
for i=0,#permissions-1 do
if grantResults[i] == PackageManager.PERMISSION_GRANTED then
--print(permissions[i].."权限通过")
count = count + 1
--假如需要的权限被通过了
if permissions[i]==Manifest.permission.READ_EXTERNAL_STORAGE then
fun()
end
else
--print(permissions[i].."权限拒绝")
if permissions[i]==Manifest.permission.READ_EXTERNAL_STORAGE then
print("执行fun()需要的权限未通过")
end
end
end
print("申请了"..#permissions.."个权限,通过了"..count.."个权限")
end
end
--示例
--要申请的权限列表,请写成常量以免自己写错
--所有的权限常量定义在Manifest的内部类permission里,写法如下
local Manifest = luajava.bindClass "android.Manifest"
--以储存权限为例
local requirePermissions =
{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
}
--发起请求
requestPermissions(requirePermissions, permissionRequestCode)
--检查权限
function checkPermission(permission)
return 0==activity.checkSelfPermission(permission)
end
--单纯检查一下有没有指定权限
local flag = checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
print("WRITE_EXTERNAL_STORAGE检查结果"..tostring(flag))

将这些添加到 ./main.lua 中即可

./main.lua 完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import "java.lang.*","java.util.*"
import "android.os.*","android.app.*"
import "android.content.*","android.provider.*"
activity {
Title = res.string.app_title,
ContentView = res.layout.main
}
--Android动态权限申请,回调和检查
--权限的申请应当遵循“谁需要,谁申请”,权限通过则执行,不通过则不执行
--既不应该一启动就全申请,也不应该申请不应需要的权限
--假设有某个需求需要权限
function fun()
print("某个需要权限的操作")
end
--定义请求码
local permissionRequestCode = 10
--申请权限
function requestPermissions(permissions, requestCode)
local ActivityCompat = luajava.bindClass "androidx.core.app.ActivityCompat"
return ActivityCompat.requestPermissions(activity, permissions, requestCode);
end
--申请权限的回调在这里执行
onRequestPermissionsResult=function(requestCode, permissions, grantResults)
local PackageManager = luajava.bindClass "android.content.pm.PackageManager"
--判断是不是自己的权限申请
if requestCode==permissionRequestCode then
local count = 0
local Manifest = luajava.bindClass "android.Manifest"
for i=0,#permissions-1 do
if grantResults[i] == PackageManager.PERMISSION_GRANTED then
--print(permissions[i].."权限通过")
count = count + 1
--假如需要的权限被通过了
if permissions[i]==Manifest.permission.READ_EXTERNAL_STORAGE then
fun()
end
else
--print(permissions[i].."权限拒绝")
if permissions[i]==Manifest.permission.READ_EXTERNAL_STORAGE then
print("执行fun()需要的权限未通过")
end
end
end
print("申请了"..#permissions.."个权限,通过了"..count.."个权限")
end
end
--示例
--要申请的权限列表,请写成常量以免自己写错
--所有的权限常量定义在Manifest的内部类permission里,写法如下
local Manifest = luajava.bindClass "android.Manifest"
--以储存权限为例
local requirePermissions =
{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
}
--发起请求
requestPermissions(requirePermissions, permissionRequestCode)
--检查权限
function checkPermission(permission)
return 0==activity.checkSelfPermission(permission)
end
--单纯检查一下有没有指定权限
local flag = checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
print("WRITE_EXTERNAL_STORAGE检查结果"..tostring(flag))



function deleteMedia(type)
local uri
local Type
if type == "image" then
print("正在清理图片")
Type = "图片"
uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
end
if type == "vedio" then
--print("正在清理视频")
Type = "视频"
uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
end
local contentResolver = activity.getContentResolver()
local rowsDeleted = contentResolver.delete(uri, nil, nil)
print(tostring(rowsDeleted).."个"..Type.."清理完毕")
end
imgclean.onClick=function()
deleteMedia("image")
end
movclean.onClick=function()
deleteMedia("vedio")
end

这样一个简单的删除全部图片/视频程序便写好了

注意:调试时候默认获得权限,无需弹窗申请

如果在自己手机测试该程序,一定要提前备份文件!!!

构建导出

在右上角 竖着的三个点 选择 项目...构建

会自动跳到 NeLuaJ+ Builder 中,选择你的项目 → 下一步开始

构建的apk位于 /storage/emulated/0/LuaJ/Builds

美化布局

如果想要简单美化,参考最开头推荐阅读的那篇文章,这里只做隐藏标题处理

./main.lua中添加

activity.getSupportActionBar().hide()