为Glide增加加载视频缩略图的feature

基本概念

因为Glide支持gif的缘故,这段时间准备把项目整体由UIL替换为Glide。没想到之前在我眼里没啥特点的UIL还有个了不得的feature,它支持直接传入视频的绝对路径然后加载视频缩略图的功能,而Glide并没有这个功能,但是看过Glide的源码之后,感觉完全可以利用Glide的ModelLoader的接口动手实现一个。

代码实现

首先看代码之前必须对Glide的源码有个大概的了解,推荐看这篇Glide源码分析。然后开始编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

public class VideoThumbnailLoader implements ModelLoader<Uri, InputStream> {

public VideoThumbnailLoader() {
}

@Override
public DataFetcher<InputStream> getResourceFetcher(Uri model, int width, int height) {
return new VideoThumbnailFetcher(model);
}
}

public class VideoThumbnailFetcher implements DataFetcher<InputStream> {

private final Uri mUri;
private InputStream inputStream;

public VideoThumbnailFetcher(Uri uri) {
this.mUri = uri;
}

@Override
public InputStream loadData(Priority priority) throws Exception {
Bitmap bitmap = ThumbnailUtils.createVideoThumbnail(mUri.getPath(), MediaStore.Images.Thumbnails.MINI_KIND);
if (bitmap != null) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos);
inputStream = new ByteArrayInputStream(bos.toByteArray());
}
return inputStream;
}

@Override
public void cleanup() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Do nothing.
}
}
}

@Override
public String getId() {
return mUri.toString();
}


@Override
public void cancel() {

}

代码很简单,在关键的loadData方法里面实现将视频的绝对路径转换为InputStream的工作,使用的工具是SDK提供的ThumbnailUtils这个类,这个工具类能方便地将路径转换为缩略图。然后再看怎么使用:

1
2
3
4
5
6

Glide.with(context)
.using(new VideoThumbnailLoader(),InputStream.class)
.load(Uri.parse(uri))
.as(Bitmap.class)
.into(imageView);

ok,大功告成,这样使用就可以直接传入视频路径加载为缩略图了。

但是在研究ModelLoader的过程中,我发现Glide已经有一个类似的实现了MediaStoreStreamLoader,为了方便我直接贴出对应的MediaStoreThumbFetcher的完整实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238

public class MediaStoreThumbFetcher implements DataFetcher<InputStream> {
private static final String TAG = "MediaStoreThumbFetcher";
private static final int MINI_WIDTH = 512;
private static final int MINI_HEIGHT = 384;
private static final ThumbnailStreamOpenerFactory DEFAULT_FACTORY = new ThumbnailStreamOpenerFactory();

private final Context context;
private final Uri mediaStoreUri;
private final DataFetcher<InputStream> defaultFetcher;
private final int width;
private final int height;
private final ThumbnailStreamOpenerFactory factory;
private InputStream inputStream;

public MediaStoreThumbFetcher(Context context, Uri mediaStoreUri, DataFetcher<InputStream> defaultFetcher,
int width, int height)
{

this(context, mediaStoreUri, defaultFetcher, width, height, DEFAULT_FACTORY);
}

MediaStoreThumbFetcher(Context context, Uri mediaStoreUri, DataFetcher<InputStream> defaultFetcher, int width,
int height, ThumbnailStreamOpenerFactory factory) {
this.context = context;
this.mediaStoreUri = mediaStoreUri;
this.defaultFetcher = defaultFetcher;
this.width = width;
this.height = height;
this.factory = factory;
}

@Override
public InputStream loadData(Priority priority) throws Exception {
ThumbnailStreamOpener fetcher = factory.build(mediaStoreUri, width, height);

if (fetcher != null) {
inputStream = openThumbInputStream(fetcher);
}

if (inputStream == null) {
inputStream = defaultFetcher.loadData(priority);
}

return inputStream;
}

private InputStream openThumbInputStream(ThumbnailStreamOpener fetcher) {
InputStream result = null;
try {
result = fetcher.open(context, mediaStoreUri);
} catch (FileNotFoundException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to find thumbnail file", e);
}
}

int orientation = -1;
if (result != null) {
orientation = fetcher.getOrientation(context, mediaStoreUri);
}

if (orientation != -1) {
result = new ExifOrientationStream(result, orientation);
}
return result;
}

@Override
public void cleanup() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Do nothing.
}
}
defaultFetcher.cleanup();
}

@Override
public String getId() {
return mediaStoreUri.toString();
}

@Override
public void cancel() {
// Do nothing.
}

private static boolean isMediaStoreUri(Uri uri) {
return uri != null
&& ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
&& MediaStore.AUTHORITY.equals(uri.getAuthority());
}

private static boolean isMediaStoreVideo(Uri uri) {
return isMediaStoreUri(uri) && uri.getPathSegments().contains("video");
}

static class FileService {
public boolean exists(File file) {
return file.exists();
}

public long length(File file) {
return file.length();
}

public File get(String path) {
return new File(path);
}
}

interface ThumbnailQuery {
Cursor queryPath(Context context, Uri uri);
}

static class ThumbnailStreamOpener {
private static final FileService DEFAULT_SERVICE = new FileService();
private final FileService service;
private ThumbnailQuery query;

public ThumbnailStreamOpener(ThumbnailQuery query) {
this(DEFAULT_SERVICE, query);
}

public ThumbnailStreamOpener(FileService service, ThumbnailQuery query) {
this.service = service;
this.query = query;
}

public int getOrientation(Context context, Uri uri) {
int orientation = -1;
InputStream is = null;
try {
is = context.getContentResolver().openInputStream(uri);
orientation = new ImageHeaderParser(is).getOrientation();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open uri: " + uri, e);
}
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
return orientation;
}

public InputStream open(Context context, Uri uri) throws FileNotFoundException {
Uri thumbnailUri = null;
InputStream inputStream = null;

final Cursor cursor = query.queryPath(context, uri);
try {
if (cursor != null && cursor.moveToFirst()) {
thumbnailUri = parseThumbUri(cursor);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (thumbnailUri != null) {
inputStream = context.getContentResolver().openInputStream(thumbnailUri);
}
return inputStream;
}

private Uri parseThumbUri(Cursor cursor) {
Uri result = null;
String path = cursor.getString(0);
if (!TextUtils.isEmpty(path)) {
File file = service.get(path);
if (service.exists(file) && service.length(file) > 0) {
result = Uri.fromFile(file);
}
}
return result;
}
}

static class ImageThumbnailQuery implements ThumbnailQuery {
private static final String[] PATH_PROJECTION = {
MediaStore.Images.Thumbnails.DATA,
};
private static final String PATH_SELECTION =
MediaStore.Images.Thumbnails.KIND + " = " + MediaStore.Images.Thumbnails.MINI_KIND
+ " AND " + MediaStore.Images.Thumbnails.IMAGE_ID + " = ?";

@Override
public Cursor queryPath(Context context, Uri uri) {
String imageId = uri.getLastPathSegment();
return context.getContentResolver().query(
MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,
PATH_PROJECTION,
PATH_SELECTION,
new String[] { imageId },
null /*sortOrder*/);
}
}

static class VideoThumbnailQuery implements ThumbnailQuery {
private static final String[] PATH_PROJECTION = {
MediaStore.Video.Thumbnails.DATA
};
private static final String PATH_SELECTION =
MediaStore.Video.Thumbnails.KIND + " = " + MediaStore.Video.Thumbnails.MINI_KIND
+ " AND " + MediaStore.Video.Thumbnails.VIDEO_ID + " = ?";

@Override
public Cursor queryPath(Context context, Uri uri) {
String videoId = uri.getLastPathSegment();
return context.getContentResolver().query(
MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
PATH_PROJECTION,
PATH_SELECTION,
new String[] { videoId },
null /*sortOrder*/);
}
}

static class ThumbnailStreamOpenerFactory {

public ThumbnailStreamOpener build(Uri uri, int width, int height) {
if (!isMediaStoreUri(uri) || width > MINI_WIDTH || height > MINI_HEIGHT) {
return null;
} else if (isMediaStoreVideo(uri)) {
return new ThumbnailStreamOpener(new VideoThumbnailQuery());
} else {
return new ThumbnailStreamOpener(new ImageThumbnailQuery());
}
}
}
}

根据代码我们开一看出来在这里Glide是使用CursorContentResolver去获取路径对应的缩略图,属于官方推荐的方法。但是坏处是它只能传入content:这种格式的路径,不支持传入绝对路径。但是无论如何这个思路输入官方推荐的思路,而且以后为了兼容Android N 恐怕我们项目里相关的代码都要这么写,我提供的这个方案只能作为临时的应急方案。