安卓获取视频缩略图,展示于ListView中,完美实现
本篇博客为原创,来自于vitamio,转载请注明出处。
应用场景:
获取安卓手机外部存储视频列表,适配器继承至CursorAdapter,利用ViewHolder进行优化;并利用异步加载和缓存机制,在加上一个绑定TAG机制。在ListView中展示视频某一帧的图片,视频名称,视频大小以及视频时长。
分析说明:
在ListView中展示视频某一帧的画面,有以下几种方式。
1.从媒体库中查询
2. android 2.2以后使用ThumbnailUtils类获取
3.调用jni文件,实现MediaMetadataRetriever类
三种方法各有利弊:
第一种方法,新视频增加后需要SDCard重新扫描才能给新增加的文件添加缩略图,灵活性差,而且不是很稳定,适合简单应用
第二种方法,实现简单,但2.2以前的版本不支持
第三种方法,实现复杂,但比较灵活
为了在UI界面中展示视频缩略图不卡顿,不乱跳,不重复加载,简单方便的前提下,我选择第二种方式实现。
先定义一个MyVideoCursorAdapter类继承至CursorAdapter
package cn.lsj.mypalyer.adapter;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import cn.lsj.mypalyer.R;
import cn.lsj.mypalyer.bean.VideoBean;
import cn.lsj.mypalyer.utils.MyUtils;
import cn.lsj.mypalyer.utils.MyVideoThumbLoader;
import cn.lsj.mypalyer.view.MyImageView;
public class MyVideoCursorAdapter extends CursorAdapter {
private MyVideoThumbLoader mVideoThumbLoader;
public MyVideoCursorAdapter(Context context, Cursor c) {
super(context, c);
mVideoThumbLoader = new MyVideoThumbLoader();// 初始化缩略图载入方法
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final VideoBean vb = VideoBean.getInstance(cursor);
View view = View.inflate(mContext, R.layout.video_list_item, null);
ViewHolder vh = new ViewHolder();
vh.title = (TextView) view.findViewById(R.id.video_list_item_tv_title);
vh.duration = (TextView) view
.findViewById(R.id.video_list_item_tv_duration);
vh.size = (TextView) view.findViewById(R.id.video_list_item_tv_size);
vh.iv = (MyImageView) view.findViewById(R.id.iv);
view.setTag(vh);
vh.iv.setTag(vb.path);
return view;
}
@Override
public void bindView(View view, final Context context, Cursor cursor) {
final ViewHolder vh = (ViewHolder) view.getTag();
final VideoBean vb = VideoBean.getInstance(cursor);
vh.title.setText(vb.title);
vh.duration.setText(MyUtils.DurationByMs(vb.duration));
vh.size.setText(Formatter.formatFileSize(context, vb.size));
if (vb.duration == 0) {
vh.iv.setImageResource(R.drawable.btn_audio_play);
} else {
mVideoThumbLoader.showThumbByAsynctack(vb.path, vh.iv);
}
}
private class ViewHolder {
private TextView title;
private TextView duration;
private TextView size;
private MyImageView iv;
}
}
其中的布局才用LinearLayout布局,左边展示视频缩略图,右边展示视频大小,中间展示视频名称和时长
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/main_list_selector"
android:orientation="horizontal"
android:padding="6dp" >
<!-- icon -->
<cn.lsj.mypalyer.view.MyImageView
android:id="@+id/iv"
android:layout_width="45dp"
android:layout_height="45dp"
android:src="@drawable/btn_audio_play" />
<!-- 视频信息 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="6dp"
android:layout_marginTop="6dp"
android:layout_weight="1"
android:orientation="vertical" >
<!-- 文件名 -->
<TextView
android:id="@+id/video_list_item_tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="文件名"
android:textColor="@color/white"
android:textSize="16sp" />
<!-- 视频时长 -->
<TextView
android:id="@+id/video_list_item_tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="时长"
android:textColor="@color/halfwhite"
android:textSize="14sp" />
</LinearLayout>
<!-- 文件大小 -->
<TextView
android:id="@+id/video_list_item_tv_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:singleLine="true"
android:text="文件大小"
android:textColor="@color/halfwhite"
android:textSize="16sp" />
</LinearLayout>
其中的cn.lsj.mypalyer.view.MyImageView是继承至ImageView,之所以要自定义,是因为在展示列表时,会报异常(这点很重要,后面会讲)
java.lang.IllegalArgumentException: Cannot draw recycled bitmapsat
Android.view.GLES20Canvas.drawBitmap(GLES20Canvas.java:778)at
android.view.GLES20RecordingCanvas.drawBitmap
(GLES20RecordingCanvas.java:117)at
android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:393)at
android.widget.ImageView.onDraw(ImageView.java:979)at
android.view.View.draw(View.java:13458)at
android.view.View.getDisplayList(View.java:12409)at
android.view.View.getDisplayList(View.java:12453)at
android.view.View.draw(View.java:13182)at
android.view.ViewGroup.drawChild(ViewGroup.java:2929)at
android.view.ViewGroup.dispatchDraw(ViewGroup.java:2799)at
android.view.View.draw(View.java:13461)at
android.view.View.getDisplayList(View.java:12409)at
android.view.View.getDisplayList(View.java:12453)at
android.view.View.draw(View.java:13182)at
android.view.ViewGroup.drawChild(ViewGroup.java:2929)at
android.widget.ListView.drawChild(ListView.java:3226)at
android.view.ViewGroup.dispatchDraw(ViewGroup.java:2799)at
android.widget.AbsListView.dispatchDraw(AbsListView.java:2433)at
android.widget.ListView.dispatchDraw(ListView.java:3221)at
android.view.View.draw(View.java:13461)at android.widget.AbsListView.draw
(AbsListView.java:3759)
为了解决这个异常,我自定义MyImageView类继承至ImageView
package cn.lsj.mypalyer.view;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.ImageView;
public class MyImageView extends ImageView {
public MyImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyImageView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
try {
super.onDraw(canvas);
} catch (Exception e) {
System.out.println("trying to use a recycled bitmap");
}
}
}
接下来,需要填充数据到列表展示
设置适配器代码如下:
MyVideoCursorAdapter adapter = new MyVideoCursorAdapter(mContext, null);
lv.setAdapter(adapter);
请求数据:
MyQueryHandler myQueryHandler = new MyQueryHandler(
mContext.getContentResolver());
myQueryHandler.startQuery(100, adapter, Media.EXTERNAL_CONTENT_URI,
new String[] { Media._ID, Media.TITLE, Media.DATA,
Media.DURATION, Media.SIZE }, null, null, null);
MyQueryHandler类继承至AsyncQueryHandler,用于异步请求数据
package cn.lsj.mypalyer.utils;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.database.Cursor;
import android.support.v4.widget.CursorAdapter;
public class MyQueryHandler extends AsyncQueryHandler {
public MyQueryHandler(ContentResolver cr) {
super(cr);
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
super.onQueryComplete(token, cookie, cursor);
if (token == 100 && cookie instanceof CursorAdapter) {
CursorAdapter adapter = (CursorAdapter) cookie;
adapter.swapCursor(cursor);
}
}
}
请求数据,然后展示在列表中,UI界面之所以不卡顿,是因为我用到了异步请求视频缩略图,异步加载和缓存,关键点就在MyVideoCursorAdapter类中,下面三行代码:
vh.iv.setTag(vb.path);
mVideoThumbLoader = new MyVideoThumbLoader();
mVideoThumbLoader.showThumbByAsynctack(vb.path, vh.iv);
自定义MyVideoThumbLoader类,主要的实现机制就是 异步加载 和 缓存机制 在加上一个绑定TAG机制
package cn.lsj.mypalyer.utils;
import android.graphics.Bitmap;
import android.media.ThumbnailUtils;
import android.os.AsyncTask;
import android.provider.MediaStore.Video.Thumbnails;
import android.support.v4.util.LruCache;
import cn.lsj.mypalyer.R;
import cn.lsj.mypalyer.activity.MainActivity;
import cn.lsj.mypalyer.view.MyImageView;
public class MyVideoThumbLoader {
// 创建cache
private LruCache<String, Bitmap> lruCache;
public MyVideoThumbLoader() {
int maxMemory = (int) Runtime.getRuntime().maxMemory();// 获取最大的运行内存
int maxSize = maxMemory / 4;
// 拿到缓存的内存大小
lruCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
// 这个方法会在每次存入缓存的时候调用
return value.getByteCount();
}
};
}
public void addVideoThumbToCache(String path, Bitmap bitmap) {
if (getVideoThumbToCache(path) == null) {
// 当前地址没有缓存时,就添加
lruCache.put(path, bitmap);
}
}
public Bitmap getVideoThumbToCache(String path) {
return lruCache.get(path);
}
public void showThumbByAsynctack(String path, MyImageView imgview) {
if (getVideoThumbToCache(path) == null) {
// 异步加载
new MyBobAsynctack(imgview, path).execute(path);
} else {
imgview.setImageBitmap(getVideoThumbToCache(path));
}
}
class MyBobAsynctack extends AsyncTask<String, Void, Bitmap> {
private MyImageView imgView;
private String path;
public MyBobAsynctack(MyImageView imageView, String path) {
this.imgView = imageView;
this.path = path;
}
@Override
protected Bitmap doInBackground(String... params) {
Bitmap bitmap = null;
try {
ThumbnailUtils tu = new ThumbnailUtils();
bitmap = tu.createVideoThumbnail(params[0],
Thumbnails.MICRO_KIND);
System.out.println("111111path: " + path + " bitmap: "
+ bitmap);
if (bitmap == null) {
bitmap = android.graphics.BitmapFactory.decodeResource(
MainActivity.mainActivity.getResources(),
R.drawable.btn_audio_play);
System.out.println("5555555path: " + path + " bitmap: "
+ bitmap);
}
// //直接对Bitmap进行缩略操作,最后一个参数定义为OPTIONS_RECYCLE_INPUT ,来回收资源
Bitmap bitmap2 = tu.extractThumbnail(bitmap, 50, 50,
ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
System.out.println("path: " + path + "bitmap2: " + bitmap2);
// 加入缓存中
if (getVideoThumbToCache(params[0]) == null) {
addVideoThumbToCache(path, bitmap2);
}
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imgView.getTag().equals(path)) {// 通过 Tag可以绑定 图片地址和
// imageView,这是解决Listview加载图片错位的解决办法之一
imgView.setImageBitmap(bitmap);
}
}
}
}
这里一定要注意下面这几行行代码,因为得到bitmap有可能为空,当视频是mkv格式时,就会为null,所以加个判断,设置默认的图片。而且,ImageView要用自定义的MyImageView,否则会报异常java.lang.IllegalArgumentException: Cannot draw recycled bitmapsat
bitmap = tu.createVideoThumbnail(params[0],Thumbnails.MICRO_KIND);
if (bitmap == null) {
bitmap = android.graphics.BitmapFactory.decodeResource(
MainActivity.mainActivity.getResources(),
R.drawable.btn_audio_play);
System.out.println("5555555path: " + path + " bitmap: "
+ bitmap);
}
至此,视频缩略图可以完美的展示于ListView中,不卡顿,不重复,不乱跳,并加入异步加载 和 缓存机制, 在加上一个绑定TAG机制。