更新:
- 修改了一些逻辑错误
- 添加了一些图示说明
这两天弄了一下android相册的相关功能。还是花了挺长时间的,这里总结一下,避免以后再踩坑。同时也在这篇文章里面补齐一些android开发的基础支持
打开Android相册并选一个图片进行显示
分为几个步骤:
-
QtCreator新建Android工程
本例使用的是arm64-v8 Android开发套件。
-
构建工程并在构建目录中找到AndroidManifest.xml
创建的Android工程build之后都会在android-build根目录下生成一个AndroidManifest.xml文件。这个文件是android开发很重要个的一个文件,是应用清单。项目中引用的java包、app的横屏和竖屏、app的是否全屏等等很多功能都是在里面设置的。下面有一些详细的参考文章:
-
在工程中添加AndroidManifest.xml和java文件。
这里需要详细说一下Qt创建android工程的特殊步骤,如下图所示:
创建android工程我们需要点击上图的按钮来创建模板,创建好的工程目录如下图:
QtCreator会自动为我们创建如下一系列文件其中我们暂时用到的是AndroidManifest.xml文件。之后我们要把步骤2中提到的相同文件拷贝到该目录下面做替换。同时我们生成的调用相册方法的java文件也需要拷贝到这个目录下面,存放的目录结构如下:
那么这么一长串的目录的创建依据是什么呢,就是我们的java文件里面定义的包名
package org.qtproject.example.OpenAndroidAlbum;
程序运行起来之后会根据目录结构找到我们的java包。当然我们也可以自己在工程根目录下创建android目录,只放入java后缀的文件和AndroidManifest.xml文件。这个目录的最终作用还是在程序构建的时候会把目录下面的文件拷贝到我们的构建目录下面,这里红色框框里面的东西会被我们创建的工程目录下的东西给替换掉,里面的一些文件具体是做什么的感兴趣的小伙伴可以自己去研究一下,总之我们需要的主要就是两个文件。
java文件是自己创建的用来写一些java代码调用android原生功能的相当于c++中的一个namespace的文件。
java文件可以从网上找一个来参考着写。在文章的末尾我会附上gitee的地址供大家参考。
放出java的代码简单看一下吧:
public class OpenAndroidAlbum extends QtActivity { public static native void fileSelected(String fileName); static final int REQUEST_OPEN_IMAGE = 1; public String lastCameraFileUri; static final int REQUEST_CAPTURE_IMAGE = 2; private static OpenAndroidAlbum m_instance; public OpenAndroidAlbum() { m_instance = this; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onDestroy() { super.onDestroy(); } static void openAnImage() { m_instance.dispatchOpenGallery(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { System.out.println("===dispatchOpenGallery1==="); if (resultCode == RESULT_OK) { if(requestCode == REQUEST_OPEN_IMAGE) { String filePath = getRealPathFromURI(getApplicationContext(), data.getData()); fileSelected(filePath); } } else { // fileSelected(":("); } super.onActivityResult(requestCode, resultCode, data); } private void dispatchOpenGallery() { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, REQUEST_OPEN_IMAGE); } public String getRealPathFromURI(Context context, Uri contentUri) { Cursor cursor = null; try { String[] proj = { MediaStore.Images.Media.DATA }; cursor = context.getContentResolver().query(contentUri, proj, null, null, null); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); System.out.println(column_index); return cursor.getString(column_index); } finally { if (cursor != null) { cursor.close(); } } } }
-
fileSelected这个静态函数是我们在c++代码中定义的。java和c++的混合编程是通过JNI来实现的,
#ifdef Q_OS_ANDROID #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_org_qtproject_example_OpenAndroidAlbum_OpenAndroidAlbum_fileSelected(JNIEnv */*env*/, jobject /*obj*/, jstring results) { selectedFileName = QAndroidJniObject(results).toString(); qDebug() << "fileName = " << selectedFileName; } #ifdef __cplusplus } #endif #endif
名字看起来很长Java是固定头部,org_qtproject_example_OpenAndroidAlbum这个是java包名,OpenAndroidAlbum是类名,最后fileSelected这个才是函数名。需要注意的是由于jni函数名映射成java函数名的时候是依靠“”来间隔包、类、方法的,如果你的函数中有“”字符的话,jni必须能够区分函数名中的“_”是字符还是分隔符,所以在函数名前面需要加1用于区分。
由于JNI的函数是c函数,所以要加上
extern "C"
。这样定义好之后的函数就可以在java中直接调用了,还是很方便的。 -
openAnImage是我们定义打开图片按钮的响应函数
java中定义的函数在c++中调用的方法是通过Qt的QAndroidJniObject类的一个静态方法实现的:
QAndroidJniObject::callStaticMethod<void>("org/qtproject/example/OpenAndroidAlbum/OpenAndroidAlbum", "openAnImage", "()V");
第一个参数是类名其实也是包名,是在java文件中通过
package org.qtproject.example.OpenAndroidAlbum;
定义的。
-
dispatchOpenGallery这个方法用来调用相册
通过Intent对象和startActivityForResult实现调用。这里有一个坑,
Intent intent = new Intent(Intent.ACTION_PICK);
创建对象的时候ACTION_PICK这个枚举要用对,4.4以后的版本好像要用这个,我也是之前怎么都打不开相册,改了这个枚举之后就可以了。 -
onActivityResult在相册中选中一张图片之后会调用这个回调
很自然就会想到我们在c++中定义的fileSelected函数要在这个地方调用了。把路径转换成java的String类型,调用
fileSelected(filePath)
就可以在Qt代码中处理图片路径了。
-
在Qt代码中调用java的打开相册的方法,同时利用JNI定义一个c++的处理方法
这个我们在上一条中也提到了。这里提一下编码中容易出错的地方
QAndroidJniObject::callStaticMethod<void>("org/qtproject/example/OpenAndroidAlbum/OpenAndroidAlbum", "openAnImage", "()V");
第一个字符串代表java包,相当于一个c++的类。如果遇到编译过程中遇到“找不到类"的类似错误提示检查一下第一个字符串,最后是以OpenAndroidAlbum.java包的前缀结尾的,切记!!!
-
AndroidManifest.xml要做相应的修改
完成了以上步骤还不算完,如果这时候直接执行程序会报错:
JNI DETECTED ERROR IN APPLICATION: JNI GetStaticMethodID called with pending exception java.lang.NullPointerException: Attempt to invoke direct method 'void org.qtproject.example.OpenAndroidAlbum.OpenAndroidAlbum.dispatchOpenGallery()' on a null object reference
从错误提示上可以推断出是调用了空对象的方法导致的。分析java代码
m_instance.dispatchOpenGallery();
打开相册的时候我们调用了这句话,而m_instance
我们是在public OpenAndroidAlbum() { m_instance = this; }
构造函数进行初始化的,我们调用的是静态方法,确实没有进行实例化。这里怎么解决呢?这就要用到两个地方了,我们定义的java类继承于QtActivity,而清单文件AndroidManifest.xml里面有一个activity标签,这个标签就是用来指定activity对应的类的,清单文件此时发挥了它”清单“的作用。此标签介绍如下:
该元素声明一个实现应用可视化界面的Activity(Activity类子类)。这是
元素中必要的子元素。所有Activity都必须由清单文件中的 元素表示。任何未在该处声明的Activity对系统都不可见,并且永远不会被执行。 作者:闪电的蓝熊猫
链接:https://www.jianshu.com/p/3b5b89d4e154
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。我们把activity标签下的属性 修改成
org.qtproject.example.OpenAndroidAlbum.OpenAndroidAlbum
即可。这个是不是很眼熟呢,没错就是我们在cpp中写的QAndroidJniObject::callStaticMethod<void>("org/qtproject/example/OpenAndroidAlbum/OpenAndroidAlbum", "openAnImage", "()V");
第一个参数。这样的对应关系保证没错的话就可以顺利打开相册了。
-
完成了上述5点之后只是能打开相册而已,如果要读取里面的图片还要修改软件的权限,获取允许打开相册的权限之后才能正常的打开相册图片
gitee地址:https://gitee.com/guiguzicom/Demo/tree/master/OpenAndroidAlbum