前言
在现在的手机使用情境中,有越来越多的机会让使用者去运用他们的图片或影片内容,例如现在绝大多数的社群软体都会需要用到图片/影片的上传,而这相对应的就会需要一个方便且适合的档案选取方式,或许是单一选取、複数选取等等,也可能会因为需要根据自己的 UI 设计而有所调整,所以如何将图片/影片选择器进行客製化也是个重要的应用。
在这篇文章中,我会先介绍最基本 Default 选择器的使用方式,要是没有特殊需求使用预设的选择器也不失是一个简单且方便的方式。
另外也会介绍一个类似现在大多社群软体(e.g.: Line, FB, IG),所使用的网格式清单选择器,并且同时可以选择图片和影片。
系统预设选择器
在实际开始运作我们的选择器功能前,我们必须去询问使用者是否愿意提供我们存取的权限,这边我们先在 AndroidManifest.xml
中加入权限需求。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application ... </application></manifest>
然后我们回到 MainActivity.java 中,在这边我们可以相应的在 activity_main.xml 里相对做画面调整就不赘述,首先我们先加入一个权限检查的动作,来确认是否正确取得需要的权限,若是没有的话我们也可以选择关闭 APP或是再次索取等等的动作。
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 未取得权限,向使用者索取 ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);} else { // 以取得权限,做出相应反应亦可不做反应}
接着,我们再加入一个 button 来进行我们选择器的触发,并且在 button 的 OnClickListener 中加入我们打算进行的预设图片选择器跳转功能。
public class MainActivity extends AppCompatActivity { Button defaultButton; @Override protected void onCreate(Bundle savedInstanceState) { ... defaultButton = (Button) findViewById(R.id.default_btn); ... defaultButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setType("image/*"); // 若要选取影片则改为 intent.setType("video/*"); // 设置选取的档案的 MIME 属性,要是想过滤特定的格式也可以把 * 换成目标档案格式 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 设置可以多选的选取属性,若不需要多选可以移除 intent.setAction(Intent.ACTION_GET_CONTENT); // 设置动作类型,这边只进行读取 startActivityForResult(Intent.createChooser(intent,"Select Picture"), PICK_IMAGE_MULTIPLE); // 交办 intent 以及接收回传选取结果 } }); }}
基本上到这边,其实就已经可以做到开启预设选择器的动作了,不过今天既然称为选择器,那就是要做选取的动作,因此如何处理回传回来的结果也是一个重点,在上面我们是以 startActivityForResult() 启动的,因此在处理回传的地方我们也就会是使用 onActivityResult 来处理回传。
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { try { if (requestCode == PICK_IMAGE_MULTIPLE && resultCode == RESULT_OK && null != data) { Toast.makeText(this, "Success to pick image", Toast.LENGTH_LONG).show(); // 这边便可以对输入的 data 进行我们想要做的处理 } } catch (Exception e) { Toast.makeText(this, "Something wrong", Toast.LENGTH_LONG).show(); } super.onActivityResult(requestCode, resultCode, data); }
以上便是针对预设选择器部分的使用方式,既然是预设那就是最基础的方式,所以他的限制也较多,自由度没有那么高,因此我们接下来就来试着做一个自己的选择器吧。
自订义图片/影片选择器
在这个方法中,我们会用到的几个比较重要重点有:
因为我们在读档的显示有可能会是大量的内容,因此在这边我选择使用 RecyclerView ,并搭配 GridLayoutManager 来进行管理和显示。因为我们不用预设的选择器,因此读取档案的工作我们必须自己处理。OK,在简单介绍完我们接下来要做的工作后,我们就开始吧!
首先我们先将档案读取的方式做好,后面就可以根据我们要的资料来做显示。
在读取多媒体档案的这个部分,我採用 MediaStore 这个 Android 提供的方法来进行,并且结合 CursorLoader 来将我们资料进行载入。
其实在这个过程的动作有点像是在进行资料库的读取,所以我们先上程式码来看一下。
private void getLocalMediaUri() { Uri queryUri = MediaStore.Files.getContentUri("external"); String[] projection = { MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.DATE_ADDED, MediaStore.Files.FileColumns.MEDIA_TYPE, MediaStore.Files.FileColumns.MIME_TYPE, "duration", MediaStore.Files.FileColumns.TITLE }; String selection = MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=" + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO; String order = MediaStore.Files.FileColumns.DATE_ADDED + " DESC"; CursorLoader cursorLoader = new CursorLoader(this, queryUri, projection, selection, null, order); Cursor cursor = cursorLoader.loadInBackground(); // MEDIA_TYPE: IMAGE = 1; VIDEO = 3; int indexFileTitle = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.TITLE); int indexFilePath = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA); int indexFileTpye = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE); int indexFileMime = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE); int indexVideoDuration = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DURATION); while (cursor.moveToNext()) { inputData.add(cursor.getString(indexFilePath)); String title = cursor.getString(indexFileTitle); String uri = cursor.getString(indexFilePath); String type = cursor.getString(indexFileTpye); String mime = cursor.getString(indexFileMime); Integer duration = cursor.getInt(indexVideoDuration); SelectMedia file; if (type.equals("1")) { file = new SelectMedia(title, SelectMedia.FileType.IMAGE, uri, mime, null, null); } else { file = new SelectMedia(title, SelectMedia.FileType.VIDEO, uri, mime, null, duration); } inputMediaArrayList.add(file); } cursor.close(); }
这编最重要的部分是 CursorLoader
的使用,所以我们先看一下他的原始码:
/** * Creates a fully-specified CursorLoader. See * {@link ContentResolver#query(Uri, String[], String, String[], String) * ContentResolver.query()} for documentation on the meaning of the * parameters. These will be passed as-is to that call. */ public CursorLoader(@NonNull Context context, @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { super(context); mObserver = new ForceLoadContentObserver(); mUri = uri; mProjection = projection; mSelection = selection; mSelectionArgs = selectionArgs; mSortOrder = sortOrder; }
我们在这里面可以看到他其实可以塞入的变数有 6 个,context、uri、projection、selection、selectionArgs、sortOrder
,除了context之外他们分别代表着:
了解完这一个部分之后,我们再回头看我们的程式码,也根据这几个栏位分别建立了相对的变数,由于这边我希望影片跟图片同时读取一并显示在我们自定义的画面中,所以可以看到我使用的 constant MediaStore.Files.FileColumns.
下面的 constant,要是单纯想使用图片、影片、或是音讯档案的话也可以考虑使用 MediaStore.Video.VideoColumns.
或是 MediaStore.Images.ImageColumns.
这类的 constant 他们可以更有针对性的去使用资源。另外这些 constant 下面都还有许多属性可以依据你需要的功能或是资讯相对应的去取用,这边就不赘述。
接着在指派完我们的 CursorLoader 后便是开始进行我们把资讯读出后的处理工作,像是对我们需要的资讯做好宣告并藉由 while 移动 CursorLoader 的指标来将我们读出的所有档案讯息加以处理,在这里比较需要注意的是我有宣告一个自己的类别 SelectMedia 让我我后面在使用的时后可以更方便地做存取。
其实到了这边我们所有需要做的工作基本已经完成,接下来就只是将接出来的档案资讯根据画面需求依序放入画面元件中就可以了。
最后附上我依照 line 的图片影片选择器刻出来的画面:
不过在这边我先补充说一下在把图档塞进自定义选择画面时一定要去注意图片的大小,因为现在相机拍出的图片每一张都动辄1MB、2MB甚至更大的,所以一定要做缩图的动作才不会让资源一次使用过多而城市被强制关闭。
另外就是因为要进行缩图的处理,所以相对应的缩图製作时机、recycleview 在资源释放与重用时的管理也都是可以好好研究的问题,这边的使用我也只是使用了一个我认为比较不影响使用体验的方式,但我相信还会有更好的资源重用方法或是缩图管理的方式可以提升使用者体验。这部分就留给大家自己研究~未来有机会我再填上这个坑!