Open fred-ye opened 6 years ago
最近因为工作上的需要看了一些网上开源的图片选择器,有大厂如知乎出品的Matisse, 也有一些个人开发者写的。看完后发现,因为这个功能是和具体的产品设计风格相关,所以网上的图片选择器很多情况下是不能满足我们的实际项目开发。比如UI设计差距较大,有的图片选择器不支持预览网络图片,不支持全屏(沉浸式导航)。所以我们的做法通常是参考网上的一个图片选择器,然后再自己根据产品设计修改相应UI,添加一些新的功能。有时候因为项目的进度原因,部份代码都没有仔细看。刚好最近有打算梳理一下自己的Android体系结构的想法,就想着自己重写一个图片选择器,借此来梳理一下相关的知识点。
Matisse
这里想要实现的图片选择器具体的需求我就不列出来了,直接脑补,或者看最终的成品。这里先来列举一下这个功能所需要的知识点
再来看一下具体的技术点,先看数据相关,相册的查询,图片的查询,我们需要用到Content Provider, 同时考虑到它是一个耗时的操作,我们采用CursorLoader来进行处理。我们先来看一下从Content Provider中获取我们想要的数据:
数据相关
Content Provider
CursorLoader
我们的目的是系统中的每一个相册的名称,该相册有多少张图片。同时考虑UI因素,我们需要拿一个图片作为该相册的封面。从显示的效果上看,只要这几个数据就可以了。
对于相册,图片相关的信息,Android是采用Content Provider为我们提供数据,Content Provider相关的操作在这里, 我们先来看一下查询相册,直接上代码:
private static final String COLUMN_COUNT = "count"; private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); private static final String BUCKET_ORDER_BY = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"; private static final String[] PROJECTION = { MediaStore.Files.FileColumns._ID, MediaStore.Images.ImageColumns.BUCKET_ID, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.DATA, "COUNT(*) AS " + COLUMN_COUNT }; public void searchAllAlbum() { String args[] = new String[]{String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE)}; Cursor albums = getContentResolver().query(QUERY_URI, ALBUM_PROJECTION, ALBUM_SELECTION, args, ORDER_BY_DATE); int totalCount = 0; if (albums != null) { while (albums.moveToNext()) { totalCount += albums.getInt(albums.getColumnIndex(COLUMN_COUNT)); Log.i("MainActivity", "id:" + albums.getInt(albums.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_ID))); Log.i("MainActivity", "displayName:" + albums.getString(albums.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME))); Log.i("MainActivity"," pic:" + albums.getString(albums.getColumnIndex(MediaStore.MediaColumns.DATA))); } albums.close(); }; }
通过以上代码,我们可以查出相册的id, display name, 封面以及相册中的图片的个数。
private static final String[] IMAGE_PROJECTION = { MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.DATA }; private static final String IMAGE_SELECTION = MediaStore.Files.FileColumns.MEDIA_TYPE + " = ? " + " AND " + " bucket_id= ? " + " AND " + MediaStore.MediaColumns.SIZE + " > 0"; private static String [] getImageSelectionArgs(String albumId) { return new String[]{ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), albumId }; }; //查询: public void searchImage() { //传入相册的id String args[] = getImageSelectionArgs("-1617409521"); Cursor images = getContentResolver().query(QUERY_URI, IMAGE_PROJECTION, IMAGE_SELECTION, args, ORDER_BY_DATE); if (images != null) { while (images.moveToNext()) { Log.i(TAG, "id:" + images.getInt(images.getColumnIndex(MediaStore.MediaColumns._ID))); Log.i(TAG, "displayName:" + images.getString(images.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME))); Log.i(TAG, "mimeType:" + images.getString(images.getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE))); Log.i(TAG, "size:" + images.getString(images.getColumnIndex(MediaStore.Images.ImageColumns.SIZE))); //该属性返回的是图片在sd卡上的物理路径 Log.i(TAG," data:" + images.getString(images.getColumnIndex(MediaStore.MediaColumns.DATA))); } } }
Android系统在创建图片的时候,会为图片生成一个缩略图,查询缩略图的方法:
String[] projection = new String[] {MediaStore.Images.Thumbnails._ID, MediaStore.Images.Thumbnails.DATA}; Cursor thumbnails = getContentResolver().query(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, projection, null, null, null); if (thumbnails != null) { while(thumbnails.moveToNext()) { String data = thumbnails.getString(thumbnails.getColumnIndex(MediaStore.Images.Thumbnails.DATA)); Log.i(TAG, "data:" + data); } }
private void galleryAddPic() { Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); File f = new File(mCurrentPhotoPath); Uri contentUri = Uri.fromFile(f); mediaScanIntent.setData(contentUri); this.sendBroadcast(mediaScanIntent); }
以上代码参看这里
考虑到图片的查询是一个IO操作,我们需要做异步的查询, 需要自己写AsyncTask或子线程进行处理。而实际上Google为我们提供了Loaders来处理这种情况,关于Loaders的介结可以看这里, 简单来讲,loader比AsyncTask的功能更强大,它能够保留并缓存数据,避免configuration变化后的重复查询。Loaders的使用需要熟悉两个类Loader和LoaderManager。 [虽然说Google 在android P中已经废弃了Loaders, 建议用LiveData和ViewModels来代替,但我的代码已经写了,就还是先用这个吧,再说一时半会公司怕也不会用Google的这一套新的,后面自己会考虑用LiveData和ViewModels重构]
AsyncTask
Loaders
loader
Loader
LoaderManager
LiveData
ViewModels
每个Activity 或 Fragment中都可以通过getSupportLoaderManager()来获取一个唯一的LoaderManager, 而这个LoaderManager可以管理多个Loader
getSupportLoaderManager()
这是Loader运行时的回调,它包含三个方法onCreateLoader, onLoadFinished, onLoaderReset。通常由调用了initLoader, restartLoader方法的Activity或Fragment来实现这个接口
onCreateLoader
onLoadFinished
onLoaderReset
initLoader
restartLoader
Loader会执行具体的数据加载操作,它有两个子类,AsyncTaskLoader和CursorLoader, 其中的CursorLoader是用来从ContentProvider中加载数据,它返回一个Cursor
AsyncTaskLoader
ContentProvider
Cursor
用一个Demo来看一下具体的使用, 使用CursorLoader来查询相册:
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{ public static final String TAG = "MainActivity"; public static final String COLUMN_COUNT = "count"; private static final String ORDER_BY_DATE = MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"; private static final String ALBUM_SELECTION = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0" + ") GROUP BY ("+ MediaStore.Images.ImageColumns.BUCKET_ID; private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); private static final String[] ALBUM_PROJECTION = { MediaStore.MediaColumns._ID, MediaStore.Images.ImageColumns.BUCKET_ID, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.DATA, "COUNT(*) AS " + COLUMN_COUNT}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // getSupportLoaderManager().initLoader(0, null, this); } @Override public Loader onCreateLoader(int id, Bundle args) { String selectionArgs[] = new String[]{String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE)}; return new CursorLoader(this, QUERY_URI, ALBUM_PROJECTION, ALBUM_SELECTION, selectionArgs, ORDER_BY_DATE); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor albums) { if (albums != null) { while (albums.moveToNext()) { Log.i(TAG, "id:" + albums.getInt(albums.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_ID))); Log.i(TAG, "displayName:" + albums.getString(albums.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME))); Log.i(TAG," pic:" + albums.getString(albums.getColumnIndex(MediaStore.MediaColumns.DATA))); } }; } @Override public void onLoaderReset(Loader loader) { } }
以上是一些零碎的关于图片数据的技术点,记录下来供以后参考。接下来我们再梳理一下相关UI界面的开发 本文同步更新到 http://fredye.cn/index.php/archives/android-image-picker-1.html
最近因为工作上的需要看了一些网上开源的图片选择器,有大厂如知乎出品的
Matisse
, 也有一些个人开发者写的。看完后发现,因为这个功能是和具体的产品设计风格相关,所以网上的图片选择器很多情况下是不能满足我们的实际项目开发。比如UI设计差距较大,有的图片选择器不支持预览网络图片,不支持全屏(沉浸式导航)。所以我们的做法通常是参考网上的一个图片选择器,然后再自己根据产品设计修改相应UI,添加一些新的功能。有时候因为项目的进度原因,部份代码都没有仔细看。刚好最近有打算梳理一下自己的Android体系结构的想法,就想着自己重写一个图片选择器,借此来梳理一下相关的知识点。这里想要实现的图片选择器具体的需求我就不列出来了,直接脑补,或者看最终的成品。这里先来列举一下这个功能所需要的知识点
数据相关:
UI显示相关:
其它
再来看一下具体的技术点,先看
数据相关
,相册的查询,图片的查询,我们需要用到Content Provider
, 同时考虑到它是一个耗时的操作,我们采用CursorLoader
来进行处理。我们先来看一下从Content Provider
中获取我们想要的数据:获取相册相关的信息
我们的目的是系统中的每一个相册的名称,该相册有多少张图片。同时考虑UI因素,我们需要拿一个图片作为该相册的封面。从显示的效果上看,只要这几个数据就可以了。
对于相册,图片相关的信息,Android是采用
Content Provider
为我们提供数据,Content Provider
相关的操作在这里, 我们先来看一下查询相册,直接上代码:通过以上代码,我们可以查出相册的id, display name, 封面以及相册中的图片的个数。
根据相册获取里面的图片数据
获取缩略图相关的数据
Android系统在创建图片的时候,会为图片生成一个缩略图,查询缩略图的方法:
将最新拍摄的照片添加进图库
以上代码参看这里
使用
CursorLoader
进行数据的查询考虑到图片的查询是一个IO操作,我们需要做异步的查询, 需要自己写
AsyncTask
或子线程进行处理。而实际上Google为我们提供了Loaders
来处理这种情况,关于Loaders
的介结可以看这里, 简单来讲,loader
比AsyncTask
的功能更强大,它能够保留并缓存数据,避免configuration变化后的重复查询。Loaders
的使用需要熟悉两个类Loader
和LoaderManager
。 [虽然说Google 在android P中已经废弃了Loaders, 建议用LiveData和ViewModels来代替,但我的代码已经写了,就还是先用这个吧,再说一时半会公司怕也不会用Google的这一套新的,后面自己会考虑用LiveData
和ViewModels
重构]LoaderManger
每个Activity 或 Fragment中都可以通过
getSupportLoaderManager()
来获取一个唯一的LoaderManager
, 而这个LoaderManager
可以管理多个Loader
LoaderManager.LoaderCallbacks
这是Loader运行时的回调,它包含三个方法
onCreateLoader
,onLoadFinished
,onLoaderReset
。通常由调用了initLoader
,restartLoader
方法的Activity或Fragment来实现这个接口Loader
Loader
会执行具体的数据加载操作,它有两个子类,AsyncTaskLoader
和CursorLoader
, 其中的CursorLoader
是用来从ContentProvider
中加载数据,它返回一个Cursor
用一个Demo来看一下具体的使用, 使用
CursorLoader
来查询相册:以上是一些零碎的关于图片数据的技术点,记录下来供以后参考。接下来我们再梳理一下相关UI界面的开发 本文同步更新到 http://fredye.cn/index.php/archives/android-image-picker-1.html