在 Unity 中实现 Android 分享文本、图片、文件功能

🥦前言

本人对于安卓开发是完完全全的新手, 什么 file:// 什么 content:// 都是第一次听说, 因为本文也是针对完全新手的小白

🎈使用 Asset Store 插件实现分享功能

既然是实现自己从未接触过领域的功能, 首先想到的就是尝试使用插件, 但是 ...

商店中并没有免费且好用的分享插件, 基本都是收费的, 而且还很贵, 都在 25 美元以上! 于是只好自己想办法编写功能

  • Native Screen Share : $25
  • Native Share Screenshot (iOS and Android) : $20
  • Native Screen Share - Try It : Free, 好像不兼容 Android 7.0, 而且已经是 2019 年的了
  • Native Web Screen Capture, Save, and Share : $45

但是有一个很不错的导入导出功能的插件, 注意, 导入导出功能和分享功能可不是不一回事哦!

  • Native File Picker for Android & iOS
  • Runtime File Browser

🥝使用原生安卓包

Unity 支持直接引用 AAR 文件 (Android Archive)

可以看下官方对于 Unity 如何进行 Android 开发的说明

官方手册

当然这份说明大而全, 我们这里只需要看 Unity 如何引用 Android 原生代码的部分即可

如何引用原生代码

从这个页面中 Android plug-in types 可以看出, Unity 支持很多种引用原生代码的方式

  • 引用 Android Library Projects
  • 引用 Android Archive plug-ins 也就是 AAR 包
  • 引用 JAR plug-ins 也就是 jar 包
  • 引用 Native plug-ins for Android 我未涉足过此领域, 猜测应该是用 C++ 写的包
  • 引用 Java and Kotlin source plug-ins 也就是直接的 java 代码文件和 kotlin 代码文件

我这次使用的就是 AAR 包的方式, 使用 Java 编写代码, 打包为 AAR 供 Unity 调用即可

🍀关于安卓分享的说明

在 Android 7.0 之前, 应用可以使用 file://URI 的方式将自己的文件共享给其他应用访问。

然而从 Android 7.0 开始, 为了进一步提高私有目录的安全性, Google 不再允许通过 file://URI 的形式直接共享文件给其他应用, 否则会触发 FileUriExposedException 异常, 导致应用崩溃

那么, 如果我现在想要实现文件分享功能, 该怎么办?在 Android 7.0 及以上, 是否还有办法安全地进行文件共享?

🌴FileProvider

在 Android 7.0 进行更加严格地权限管理的同时, Google 提供了 FileProvider 机制, 这个机制允许应用通过 content:// 方式安全地共享文件, 并通过 grantUriPermissions 机制控制文件访问权限

有了上面的理论基础, 下面开始分步骤讲解操作

🍓创建一个 Android 项目

打开 Android Studio, 点击 New Project, 此时需要选择一个项目模板, 直接选择 No Activity

之后需要填写工程信息

Name: Unity-Plugin-Android

工程名字我写的是 Unity-Plugin-Android, 表示这是我给 Unity 用的 Android 插件, 之所以这样命名是因为我所有的项目都是这样命名的

Package name: com.kuroha.unity_plugin_android

这里的包名不需要和 Unity 中打包时的包名一致, 可以按照自己的规则编写即可

比如我的 Unity 项目包名是 com.kuroha.swordrequiem

这里我填写的则是 com.kuroha.unity_plugin_android

Save location: F:\Unity-Plugin-Android

填写工程文件的存放目录, 因为我整个 F 盘都是用来放置工程的, 于是我直接放在了 F:\Unity-Plugin-Android

Language: Java

编程语言, 我从来没有学习过 Kotlin, 所以选择了 Java

Minimum SDK: API 24

重点来了, 这里需要设置最小的 API 版本, 这个数字必须和 Unity 中的 Minimum API Level 保持一致! 否则会打包失败!

我当时的情况是, Unity 设置了 23, 插件设置了 24, 打包时报错了, 因为不清楚是否只是这里的数字比 Unity 的小即可还是必须保持一致, 强烈建议保持一致! 更省心!

另外因为后面要用的 FileProvider 是 Android 7.0 推出的, 因为建议这里选择 24, Unity 中的 Minimum API Level 也设置为 24

Build configuration language: Kotlin DSL

这个选项我了解的并不深, 于是选择了推荐的选项, 即 Kotlin DSL, 后面的 [Recommended] 就是推荐的意思

🌱调整项目目录的显示模式

前面新建完成 Android 工程后, Android Studio 中可以看到一个空的 Android 项目

左侧文件目录的默认显示模式是 : Android, 这个显示的其实是项目的逻辑结构, 并不是实际的文件结构, 因此对于新手而言, 还是切换到 Project 模式更好一些.

切换到 Project 模式后, 仅有 Android, External Libraries, Scratches and Consoles 三项.

🌳下载 SDK, NDK

据 Android Studio 官方说明, 会在首次构建时自动下载安装

自动安装 NDK 和 CMake
Android Gradle 插件 4.2.0 及更高版本可在您首次构建项目时自动安装所需的 NDK 和 CMake,前提是您已预先接受其二者的许可。
安装 NDK 和 CMake
当您安装 NDK 时,Android Studio 会选择可用的最新 NDK 版本。对于大多数项目,安装此默认版本的 NDK 已经足够。

所以我们来执行一次构建, 打开主菜单中的 Build 菜单, 第一个选项就是 Make Project, 点击这个选项会直接开始构建, 触发下载 SDK

需要等待一段时间, 还挺长的, 我等了接近一个小时吧.

不仅仅是构建需要下载 SDK, 接下来我们要需要的使用 File/New/New Module... 选项, 同样需要 SDK, 所以乖乖等资源下载完吧, 不然 File/New/New Module... 选项直接是灰色的, 根本点不了...

🍨新建 Module

资源下载完成后, 就可以点击 File/New/New Module... 选项了.

点击后首先需要选择 Module 的模板, 这里我们选择安卓原生库, 即 Android Native Library, 之后填写 Module 的具体信息

Module name: Android-Library

设置一下模块名称, 我填写了 Android-Library

Package name: com.kuroha.android_library

使用了自动生成的 com.kuroha.android_library

Language: Java

选择 Java 作为编程语言

C++ Standard: Toolchain Default

我选择了让工具链自动使用默认值

Minimum SDK: 24

必须和 Unity 中的 Minimum API Level 保持一致! 否则会打包失败!

建议这里选择 24, Unity 中的 Minimum API Level 也设置为 24

Build configuration language: Kotlin DSL

同样使用推荐设置

🍉配置 FileProvider

模块建好后, 在工程目录下可以直接找到模块的文件夹, 比如我的就是 Android-Library 文件夹, 如果你找不到说明你的目录显示模式不对, 默认是 Android 模式, 改为 Project 模式就可以看到了

AndroidManifest

那么先来配置一下 FileProvider, 将模块目录中的 /src/main/AndroidManifest.xml 文件改为以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.file_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
</application>
</manifest>

此时最后一行的 @xml/file_path 会是红色的, 因为我们还没有新建对应的 file_path 文件

file_path

在模块目录中新建以下文件 /src/main/res/xml/file_path.xml, 如果中间某个层级没有, 直接新建对应名称的层级即可

这里的文件名其实就是前面 /src/main/AndroidManifest.xml 文件中最后写的那句 android:resource="@xml/file_path"

file_path 文件新建成功后, 粘贴以下内容

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="files" path="." />
<cache-path name="cache" path="." />
<external-path name="external" path="." />
<external-files-path name="external_file_path" path="." />
<external-cache-path name="external_cache_path" path="." />
</paths>

到此 FileProvider 就配置完成了, 是不是很简单, 此时再回去看 AndroidManifest 文件就会发现最后那里不会再变红了. 接下来就需要开始写代码了

🍇使用 Java 编写分享逻辑

以我项目中的模块为例, 我模块中已经新建好了一个 \src\main\java\com\kuroha\android_library\NativeLib.java 文件

找到这个 Java 文件, 修改其中的内容为:

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
package com.kuroha.android_library;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;

import androidx.core.content.FileProvider;

import java.io.File;

public class ShareUtil {
public static void ShareFile(Context context, String filePath, String title) {
File file = new File(filePath);

if (file.exists()) {
String authority = context.getPackageName() + ".file_provider";
Uri uri = FileProvider.getUriForFile(context, authority, file);

Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Intent chooser = Intent.createChooser(intent, title);
context.startActivity(chooser);
}
}
}

因为 Java 中要求类名必须和文件名一致, 所以要么你修改类名为它默认提供的 NativeLib, 要么将文件重命名为 ShareUtil, 重命名这种操作总归不用再教了吧...

至此就已经完成了, 直接构建, 在模块目录中就可以看到打包的 AAR 包了, 我的是: /build/outputs/aar/Android-Library-debug.aar

为什么带一个 debug 呢, 是因为我们的项目是 debug 项目, 你改为 release 项目, 那么后面就是跟着一个 release 了, 不过 release 项目需要提供签名, 比较麻烦不是吗, 所以看你自己的选择啦 ~ 设置为 debug 和 release 都可以, 反正这个包不是直接暴露给玩家的, 只是我们自己内部使用罢了

🍒Unity 引用 AAR 包

据 Unity 官方的使用说明, 只要将打包的 AAR 包放到 Unity 项目内的 Assets 目录内的任意目录即可, 不再要求不许放在 Plugins 文件夹内, 因此我放在了自己的游戏框架目录内, 这样可以重复使用嘛

这里是官方说明: Import an Android Archive plug-in

我也直接把链接内的内容贴在下面:

Import an Android Archive plug-in

This page describes how to import an Android Archive (AAR) plug-in into your Unity Project.

  1. Copy the AAR file to your Unity Project’s Assets folder.
  2. Select the AAR in Unity and view it in the Inspector.
  3. In the Select platforms for plugin section, select Android.
  4. Select Apply.

在 Unity 中新建一个脚本 FileUtilAndroid.cs

复制粘贴以下内容, 其中命名空间之类的按照自己项目的要求改一下

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
using UnityEngine;

namespace Kuroha.Utility
{
public static class FileUtilAndroid
{
private static bool isProcessing;
private static AndroidJavaClass intentClass;
private static AndroidJavaObject intentObject;
private static AndroidJavaClass unityClass;
private static AndroidJavaClass shareUtilClass;

/// <summary>
/// 分享一段文本
/// </summary>
/// <param name="shareTitle">分享页面的标题</param>
/// <param name="shareMessage">要分享的文本</param>
/// <param name="shareSubject">要分享的主题</param>
/// <returns>成功标志</returns>
public static void ShareText(string shareTitle, string shareMessage, string shareSubject)
{
if (isProcessing)
{
return;
}

isProcessing = true;

unityClass ??= new AndroidJavaClass("com.unity3d.player.UnityPlayer");
intentClass ??= new AndroidJavaClass("android.content.Intent");
intentObject ??= new AndroidJavaObject("android.content.Intent");

intentObject.Call<AndroidJavaObject>("setAction", intentClass.GetStatic<string>("ACTION_SEND"));
intentObject.Call<AndroidJavaObject>("setType", "text/plain");
intentObject.Call<AndroidJavaObject>("putExtra", intentClass.GetStatic<string>("EXTRA_SUBJECT"), shareSubject);
intentObject.Call<AndroidJavaObject>("putExtra", intentClass.GetStatic<string>("EXTRA_TEXT"), shareMessage);

var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
var chooser = intentClass.CallStatic<AndroidJavaObject>("createChooser", intentObject, shareTitle);
currentActivity.Call("startActivity", chooser);

isProcessing = false;
}

/// <summary>
/// 分享一个文件
/// </summary>
/// <param name="shareTitle">分享页面的标题</param>
/// <param name="filePath">要分享的文件</param>
/// <returns>成功标志</returns>
public static void ShareFile(string shareTitle, string filePath)
{
if (isProcessing)
{
return;
}

isProcessing = true;

unityClass ??= new AndroidJavaClass("com.unity3d.player.UnityPlayer");
shareUtilClass ??= new AndroidJavaClass("com.kuroha.android_library.ShareUtil");

var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
shareUtilClass.CallStatic("ShareFile", currentActivity, filePath, shareTitle);

isProcessing = false;
}
}
}

这里面就包含了如何分享文本和文件的代码, 分享图片也属于分享文件

那么如何使用这两个 API 呢 ? 下面分别下一个调用的例子

1
2
3
4
5
6
7
8
9
10
11
private void ShareText()
{
FileUtilAndroid.ShareText("测试分享文本", "成功了!", "分享主题");
}

private async void ShareFile()
{
var path = $"{UnityEngine.Application.persistentDataPath}/123.txt";
File.WriteAllText(path, "Hello World!");
FileUtilAndroid.ShareFile("测试分享文件", path);
}

至此, 教程结束!

🍈附录

Android Studio 简体中文汉化包

简体中文汉化包