前言
本篇文章是基于Android 11 文件管理器(com.android.documentsui)的源码,在实际项目中定位加载图片和视频文件显示缩略图的相关问题时,做的总结,文章中缩略图加载显示原理,查看过Android 10 和 12的源码,流程基本一致。
一、文件图标
文管在手机系统中扮演一个管理,分类,操作内部存储中所有类型文件的角色,把存储中的图片,音频,视频,文档,下载apk等各种文件通过归类整理,然后呈现给用户。
文档,音频,下载apk,其显示图标一般是对应的固定图标, 而图片和视频为了更好的区分其内容,展现给用户是缩略图, 如下图:
二. 显示原理
2.1 文管中界面显示框架也是采用 Activity + Fragment 实现的, 具体的Activity为:com.android.documentsui.files.FilesActivity.java Fragment为:com.android.documentsui.dirlist.DirectoryFragment.java
文管中文件显示有两种视图:MODE_GRID(网格模式) 和 MODE_LIST(列表模式)
每次点击一个菜单或者新进入一个文件夹,都会创建一个新的Fragment,然后替换之前的Fragment,好了,我们来具体看看这个DirectoryFragment.java文件:
整个界面显示是使用Recyclerview这个控件来完成的,核心代码如下:
./src/com/android/documentsui/dirlist/DirectoryFragment.java
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
//当视图回收的时候,取消加载缩略图的task
mRecView.setRecyclerListener(
new RecyclerListener() {
@Override
public void onViewRecycled(ViewHolder holder) {
cancelThumbnailTask(holder.itemView);
}
});
//设置Item动画
mRecView.setItemAnimator(new DirectoryItemAnimator());
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// 适配器实例化
mAdapter = new DirectoryAddonsAdapter(
mAdapterEnv,
new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper, mInjector.fileTypeLookup)
);
// 设置适配器
mRecView.setAdapter(mAdapter);
// 这里的mColumnCount是通过calculateColumnCount(mode)计算得来,显示有几列
// MODE_LIST视图下只显示1列
mLayout = new GridLayoutManager(getContext(), mColumnCount) {
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mFocusManager.onLayoutCompleted();
}
};
//设置网格LayoutManager
mRecView.setLayoutManager(mLayout);
}
2.2 那数据信息是从哪里来的呢? 请留意DirectoryAddonsAdapter.java这个文件,我们知道:
RecyclerView显示View的控件,Adapter从模型层获取数据,然后提供给RecyclerView显示,它是沟通的桥梁。
Adapter的主要任务是:创建自定义的RecylerView.ViewHolder,然后将模型层的数据绑定到
ViewHolder上, 在实现上通常要复写以下三个方法:
1. onCreateViewHolder(ViewGroup parent, int viewType)
当需要新的ViewHolder来显示列表项时,会调用onCreateViewHolder方法去创建新的ViewHolder。
2. onBindViewHolder(CrimeHolder holder, int position)
将数据绑定在ViewHolder上。
3. getItemCount()
返回总共要显示的列表的数量
相关文件:
./src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
./src/com/android/documentsui/dirlist/DocumentsAdapter.java
./src/com/android/documentsui/dirlist/DocumentHolder.java
// 继承DocumentsAdapter
final class DirectoryAddonsAdapter extends DocumentsAdapter {}
public abstract class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder> {}
//元素载体
public abstract class DocumentHolder
extends RecyclerView.ViewHolder implements View.OnKeyListener {
/**
* Binds the view to the given item data.
* @param cursor
* @param modelId
* @param state
*/
// 此抽象方法在子类中去覆写,不同类型的文件加载不同的缩略图
public abstract void bind(Cursor cursor, String modelId);
}
2.3 与之相关的GridDocumentHolder.java类
相关文件
./src/com/android/documentsui/dirlist/GridDocumentHolder.java
// 这里是网格模式下各个文件缩略图容器
final class GridDocumentHolder extends DocumentHolder {
@Override
public void bind(Cursor cursor, String modelId) {
assert(cursor != null);
mModelId = modelId;
mDoc.updateFromCursor(cursor, getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY));
mIconHelper.stopLoading(mIconThumb);
mIconMimeLg.animate().cancel();
mIconMimeLg.setAlpha(1f);
mIconThumb.animate().cancel();
mIconThumb.setAlpha(0f);
//缩略图加载显示的代码
mIconHelper.load(mDoc, mIconThumb, mIconMimeLg, mIconMimeSm);
}
}
2.4 最终加载是在ThumbnailLoader.java 类中完成的
相关文件:
./src/com/android/documentsui/dirlist/IconHelper.java
./src/com/android/documentsui/ThumbnailLoader.java
public void load(Uri uri, String mimeType, int docFlags, int docIcon, long docLastModified,
ImageView iconThumb, ImageView iconMime, @Nullable ImageView subIconMime) {
if (showThumbnail) {
loadedThumbnail =
loadThumbnail(uri, docAuthority, docLastModified, iconThumb, iconMime);
}
}
private boolean loadThumbnail(Uri uri, String docAuthority, long docLastModified,
ImageView iconThumb, ImageView iconMime) {
// 通过 ThumbnailLoader 这个task 去创建缩略图
final ThumbnailLoader task = new ThumbnailLoader(uri, iconThumb,
mCurrentSize, docLastModified,
bitmap -> {
if (bitmap != null) {
iconThumb.setImageBitmap(bitmap);
animator.accept(iconMime, iconThumb);
}
}, true /* addToCache */);
ProviderExecutor.forAuthority(docAuthority).execute(task);
}
public final class ThumbnailLoader extends AsyncTask<Uri, Void, Bitmap> implements Preemptable {
@Override
protected Bitmap doInBackground(Uri... params) {
//返回缩略图的Bitmap对象
}
}
当然里面还有个缩略图的缓存机制,如果此文件已经创建过缩略图,下次可以直接从ThumbnailCache中拿出来,具体实现请查看源码文件ThumbnailCache.java
三、流程图
我们来梳理一下流程图:
四. 小结:
上述就是文件缩略图的显示流程,供大家参考!
1. 在实际项目中有时会遇到视频文件缩略图显示不出来的问题?
可能原因:DocumentsContract.getDocumentThumbnail(wrap(client), mUri, mThumbSize, mSignal)这个方法无法创建缩略图,返回为空。
解决方法:如果为空,我们可以通过传入文件路径然后生成缩略图,如下:
/**
* 获取视频缩略图(获取第一帧)
* @param filePath
* @return bitmap
*/
public Bitmap getVideoThumbnail(String filePath) {
Bitmap bitmap = null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(filePath);
bitmap = retriever.getFrameAtTime(TimeUnit.MILLISECONDS.toMicros(1));
} catch(IllegalArgumentException e) {
e.printStackTrace();
} finally {
try {
retriever.release();
}
catch (RuntimeException e) {
e.printStackTrace();
}
}
return bitmap;
}
2. 同一个视频文件,在不同文件夹显示的缩略图不一致的问题?
有时候外置SD卡/U盘 和 手机文件夹中的同一个视频,发现缩略图显示不一致现象,可以试试如下这个方法:获取视频中间的一帧来创建缩略图
/**
* 获取视频的缩略图
* 先通过ThumbnailUtils来创建一个视频的缩略图,然后再利用ThumbnailUtils来生成指定大小的缩略图。
* 如果想要的缩略图的宽和高都小于MICRO_KIND,则类型要使用MICRO_KIND作为kind的值,这样会节省内存。
* @param videoPath 视频的路径
* @param width 指定输出视频缩略图的宽度
* @param height 指定输出视频缩略图的高度度
* @param kind 参照MediaStore.Images(Video).Thumbnails类中的常量MINI_KIND和MICRO_KIND。
* 其中,MINI_KIND: 512 x 384,MICRO_KIND: 96 x 96
* @return 指定大小的视频缩略图
*/
public static Bitmap getVideoThumbnail(String videoPath, int width, int height,int kind) {
Bitmap bitmap = null;
// 获取视频的缩略图
bitmap = ThumbnailUtils.createVideoThumbnail(videoPath, kind); //調用ThumbnailUtils類的靜態方法createVideoThumbnail獲取視頻的截圖;
if(bitmap!= null){
bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height,
ThumbnailUtils.OPTIONS_RECYCLE_INPUT);//調用ThumbnailUtils類的靜態方法extractThumbnail將原圖片(即上方截取的圖片)轉化為指定大小;
}
return bitmap;
}