在前两篇中已经完成了数据的展示,就剩下图片显示功能。可以自己写图片加载库,或者使用当前使用比较广泛的图片加载库例如Picasso,Glide或Fresco。自己写的话还是比较麻烦,需要处理内存管理,性能,图片处理等方面,而且如gif等动图就无法加载。选择库的话Picasso有些弱了,Fresco又比较大 ,暂时就用Glide为例来处理。
缩略图的加载
图片选择页面,文件夹列表以及媒体文件的列表都是缩略图。使用glide加载图片和视频的缩略图的代码
GlideApp.with(view.getContext())
.asBitmap()
.override(width, height)
.load(path)
.placeholder(R.drawable.ic_image_default)
.error(R.drawable.ic_image_default)
.fallback(R.drawable.ic_image_default)
.into(view);
不管是png,gif还是webp格式的图片都可以加载。而对于视频,Glide的加载方式是尝试从数据库中查找缩略图,如果没有会使用MediaMetadataRetriever加载视频的第一帧(也可以设置RequestOptions中的frameTimeMicros参数来设置取第几秒的封面)。
而如果是音频的话则需要自定义了,方法如下
首先定义一个AudioCoverModel类
public class AudioCoverModel {
private final String path;
public AudioCoverModel(String path) {
this.path = path;
}
public String getPath() {
return path;
}
@Override
public int hashCode() {
int code = 1;
if (!TextUtils.isEmpty(path)){
code += path.hashCode();
}
return code;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AudioCoverModel) {
AudioCoverModel item = (AudioCoverModel) obj;
return TextUtils.equals(item.path, path);
}
return false;
}
}
注意的是需要重写equals方法和hashCode方法。
然后定义一个AudioCoverLoader,可以 注册到Glide中,代码如下
public class AudioCoverLoader implements ModelLoader<AudioCoverModel, InputStream> {
private final Context context;
public AudioCoverLoader(Context context) {
this.context = context;
}
@NonNull
@Override
public LoadData<InputStream> buildLoadData(@NonNull AudioCoverModel coverModel, int width, int height, @NonNull Options options) {
return new LoadData<>(new ObjectKey(coverModel), new AudioCoverFetcher(coverModel, context));
}
@Override
public boolean handles(@NonNull AudioCoverModel AudioCoverModel) {
return true;
}
public static class Factory implements ModelLoaderFactory<AudioCoverModel, InputStream> {
private final Context context;
public Factory(Context context) {
this.context = context;
}
@NonNull
@Override
public ModelLoader<AudioCoverModel, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new AudioCoverLoader(context);
}
@Override
public void teardown() {
}
}
}
最后需要定义AudioCoverFetcher类来进行封面的加载,代码如下
public class AudioCoverFetcher implements DataFetcher<InputStream> {
private final AudioCoverModel model;
private InputStream stream;
private final Context context;
AudioCoverFetcher(AudioCoverModel model, Context context) {
this.model = model;
this.context = context;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataFetcher.DataCallback<? super InputStream> callback) {
String thumbnail = FileUtil.getAudioThumbnail(context, model.getPath());
if (!TextUtils.isEmpty(thumbnail)) {
try {
File file = new File(thumbnail);
if (file.exists() && file.length() > 0) {
stream = new FileInputStream(thumbnail);
callback.onDataReady(stream);
return;
}
} catch (Exception e) {
LogUtil.d("getAudioThumbnail", "path is not null:" + e.getMessage() + ", path:" + model.getPath());
}
}
MediaMetadataRetriever retriever = null;
try {
retriever = new MediaMetadataRetriever();
retriever.setDataSource(model.getPath());
byte[] picture = retriever.getEmbeddedPicture();
if (null != picture) {
stream = new ByteArrayInputStream(picture);
callback.onDataReady(stream);
} else {
callback.onLoadFailed(new FileNotFoundException());
LogUtil.d("getEmbeddedPicture", "is null, path:" + model.getPath());
}
} catch (Exception e) {
callback.onLoadFailed(e);
LogUtil.d("onLoadFailed", e.getMessage() + ", path:" + model.getPath());
} finally {
if (retriever != null) {
retriever.release();
}
}
}
@Override
public void cleanup() {
try {
if (null != stream) {
stream.close();
}
} catch (IOException ignore) {
}
}
@Override
public void cancel() {
// cannot cancel
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.LOCAL;
}
}
其实是参考了视频加载的逻辑,先从媒体库中加载专辑封面,如果没有的话则使用MediaMetadataRetriever加载。最后需要自定义一个AppGlideModule,在registerComponents方法中进行注册。
@GlideModule
public class MyGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.append(AudioCoverModel.class, InputStream.class, new AudioCoverLoader.Factory(context));
}
}
这样加载音频时就会走自定义的代码,与视频都有封面图不同的是,不是所有的音频都有封面,加载音频的代码是这样的,
GlideApp.with(view.getContext())
.asBitmap()
.override(width, height)
.load(new AudioCoverModel(path))
.placeholder(R.drawable.ic_audio_default)
.error(R.drawable.ic_audio_default)
.fallback(R.drawable.ic_audio_default)
.into(view);
加载大图(预览图)
在预览功能中,需要缩放功能,常见的开源库有PhotoView,subsampling-scale-image-view(简称SSIV)等等,其中SSIV对于大图处理比较好,不会模糊,占用内存小,但是却无法显示GIF等动图。
那么预览时就需要使用两种图片,如果是静态图则使用SSIV,如果是GIF等动图就使用ImageView,只是无法进行放大缩小。
SSIV直接加载静态图的代码如下
ssiv.setImage(ImageSource.uri(Uri.fromFile(new File(path))));
而对于音视频封面则需要使用Glide加载出bitmap后再加载到SSIV中,
GlideApp.with(view.getContext())
.asBitmap()
.load(isVideo ? path : new AudioCoverModel(path))
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
ssiv.setImage(ImageSource.cachedBitmap(resource));
}
});
对于GIF来说Glide可以加载,加载GIF的代码
GlideApp.with(imageView.getContext())
.asGif()
.load(path)
.placeholder(R.drawable.ic_image_default)
.error(R.drawable.ic_image_default)
.fallback(R.drawable.ic_image_default)
.into(imageView);
而APNG,动画webp等动态图片的加载需要引入另外的库,如果没有则只能使用静态图了。
这样大图的加载和预览功能也完成了。到此整个媒体文件选择库就完成了,源码已经分享到github,源码地址是https://github.com/jklwan/MediaPicker。
还待解决的问题,当前设计的是support库版本,还需一个使用androidx的版本;android 10的兼容问题;gif因为使用的是普通ImageView不能缩放;这些问题还要慢慢来解决。