因项目需求,需要在Unity打包到移动端的APP上做一个拍照/录屏分享的功能。由于第一次尝试开发公共的unity3d native unmanaged plugin,过程中还是踩了不少的坑,在此总结一下。
1.拍照分享
首先想到的是使用第三方SDK来接入社交分享功能,网上查了一下发现有Mob、友盟还有一些国外的开源SDK,调研了一番,选择Mob的SDK来开发拍照分享的功能。
首先在Unity中写一段脚本用于保存当前屏幕像素,并保存为图片
1 | IEnumerator CaptureScreenshot() |
这里要注意的是整个过程需要在协程中做,首先隐藏所有UI组件,在下一帧中执行保存像素的逻辑,在下一帧显示UI组件。 协程中使用WaitForSeconds以保证拍照所得的图片中没有UI组件。
http://wiki.mob.com/Unity3D%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E6%8C%87%E5%8D%97/
下一步按照Mob的文档,导入ShareSDK组件,将刚刚保存的图片分享出去,这一步不需要多说,值得注意的就是需要在各家社交平台的开放平台上去申请id key。
各家社交平台的开放平台地址汇总,可以选择需要分享到的平台来申请。
2.录制视频分享
录制视频分享比较麻烦,直接用SDK分享的话,分享出去的是一个包含视频的网页链接,视频需要上传到Mob的云平台去重新编码,这个速度会比较慢,而且画质有损失,Mob的平台不稳定的时候,分享出去的视频还会无法观看。
体验了AR相机等带有视频录制分享的应用后,发现这些应用基本都是采用的Native API的发送文件来分享,于是考虑录制的过程用ShareSDK,拿到本地的mp4文件路径之后,再自己写一个Android的Plugin来做分享的功能
2.1Android中调用Native 文件分享API
在Android Studio中新建一个工程,创建一个Java Library模块
核心代码如下:1
2
3
4
5
6
7
8
9
10 public void ShareFile(String filePath){
Intent shareIntent = new Intent(Intent.ACTION_SEND);
File file = new File(filePath);
Uri uri = Uri.fromFile(file);
startShare(context, shareIntent, uri);
String fileType = context.getContentResolver().getType(uri);
shareIntent.setType(fileType);
shareIntent.putExtra(Intent.EXTRA_STREAM,uri);
UnityPlayer.currentActivity.startActivity(shareIntent);
}
为了在Android中以插件的方式调用,需要让类继承Unity的UnityPlayerActivity。
从Unity的安装目录Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes
下拷贝出Unity的Android class jar包,classes.jar(Unity 5.5.1)
在工程的libs中导入classes.jar,类中继承Unity的UnityPlayerActivity。
1 | public class TarShareFile extends UnityPlayerActivity { |
将上面的代码打包为jar包
2.2Unity 调用Android Unmanaged Plugin
将jar包拷贝到Untiy工程的 Assets/Plugins/Android
目录下
Unity代码中:
1 | AndroidJavaObject tarShareFile = new AndroidJavaObject("com.tx.tar.TarShareFile"); |
即可打开Android Native的分享面板,向支持文件分享的应用发送文件了
2.3Android 7.0适配
然而在Android 7.0下事情并不会那么简单。
为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问,这就意味着直接通过Uri.fromFile(file)
来访问文件不可行了。上面的代码在Android7.0下会报 FileUriExposedException
,下面是Android7.0下的适配方法
2.3.1 在AndroidManifext.xml下加入Provider配置
1 | <provider |
2.3.2 在res/xml下新建provider_paths.xml
文件(与上面android:resource中配置的名称相同)
1 | <?xml version="1.0" encoding="utf-8"?> |
2.3.3拷贝support-core-utils-24.2.0.aar
包
为了访问在Unity中访问FileProvider类,需要在Android SDK中的support-core-utils-24.2.0.aar
包拷贝至Unity工程的/Assets/Plugins/Android
目录
3.3.4 适配Android的分享API
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
29
30
31
32 public void ShareFile(String filePath, Context context){
Intent shareIntent = new Intent(Intent.ACTION_SEND);
File file = new File(filePath);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String providerName = context.getPackageName() + ".fileProvider";
uri = FileProvider.getUriForFile(context,providerName , file);
}
else{
uri = Uri.fromFile(file);
}
startShare(context, shareIntent, uri);
String fileType = context.getContentResolver().getType(uri);
shareIntent.setType(fileType);
shareIntent.putExtra(Intent.EXTRA_STREAM,uri);
UnityPlayer.currentActivity.startActivity(shareIntent);
}
```
注意provider的名称需要和AndroidManifest中的`android:authorities`属性的配置一致
由于在Unity中是直接new出来的java类对象,在类中没有办法获取应用的context,所以需要在Unity代码中获取应用context,并传递给jar包中的方法。
```csharp
AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject currentActivity = unity.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject context = currentActivity.Call<AndroidJavaObject>("getApplicationContext");
AndroidJavaObject tarShareFile = new AndroidJavaObject("com.tx.tar.TarShareFile");
String videoPath = GetVideoPath();
tarShareFile.Call("ShareFile", videoPath, context);
上面的工作完成后,重新打包jar包,拷贝至Unity工程的/Assets/Plugins/Android
目录,并将jar包工程下的res目录、AndroidManifest.xml文件都拷贝至/Assets/Plugins/Android
目录,重新打包Unity应用,发现在Android7.0下已经可以调起分享面板了。
3.3.5 让目标应用认识content://xxx/xxx 虚拟目录
上面的步骤之后,已经可以在Android7.0下走完分享流程了,但是测试一下分享到QQ,会发现QQ提示“找不到文件”
将文件路径打出来,Uri.From的方式获取的路径是file://storage/emulated/0/包名/xxx/xxx.mp4
这样的绝对路径,而FileProvider访问文件,则会像一些Web服务器一样,用一个虚拟目录的路径来访问文件,刚刚的绝对路径,FileProvider读取之后,则是content://provider_paths.xml中配置的路径名称/包名/xxx/xxx.mp4
这个视频文件路径在Android中可以被本应用的Intent对象识别,但是无法被其他应用识别,所以在手Q中测试还是提示文件不存在。
解决方案有两种,第一种是在Application的OnCreate中添加下面的代码
1 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { |
由于需要在Unity3D中以Unmanaged Plugin的方式来调用这段代码,不方便在Application的OnCreate中注入逻辑,所以我采用了第二种方案。
2.4更好的插件库文件组织方式
经过前面的步骤这个社交分享的插件已经可以使用了。但是要将整个项目打包成一个unitypackage作为一个公共插件,还会有一点问题: plugins目录下会有一堆本插件依赖的库和代码、配置文件,其他用户导入package之后,很容易和用户之前的资源其冲突。
所以考虑将本插件所有的代码和库、配置文件都放在一个plugis目录下一个以插件名字命名的目录里,然而这样做打包运行之后,发现报异常ClassNotFound
,说明我们的jar包是必须放在Assets/Plugins/Andoird
根目录下的。
如何解决这个问题呢?
参考一下Unity3D Building and using plug-ins for Android 文档 https://docs.unity3d.com/Manual/PluginsForAndroid.html
发现Unity调用android native代码的方式有4种:
- jar包调用
- aar调用
- Android Libraries工程调用
- .so动态库调用
而此插件需要的调用方式特性是:
- 可以用自己的目录组织文件
- 需要包含能被Unity在Android平台打包时识别的AndroidManifest.xml文件和res目录
阅读文档并对比几种调用方式后,发现只有Android Libraries工程调用是合适需求的。
在/Assets/Plugins/Android
目录下新建以插件名字命名的目录,在此目录中创建子目录存放jar包模块的工程
其中插件jar包编译好之后,放到libs中去,project.properties
文件是一个关键的文件,其中的内容:
target=android-8
android.library=true
有了这个文件在目录下,Unity就会认为这是一个Android Library项目,在打包APK的时候将其中的库和配置文件考虑进来。
至此,分享插件开发完毕。
后续再一套分享的交互组件,放到一个GameObject然后保存为一个预制件, 再将Unity工程导出为一个unitypackage,其他用户直接导入此package,拖入预制件即可一秒钟接入社交分享组件!
本文链接:https://www.zoucz.com/blog/2017/07/21/unity-android-native-share/