原标题:GifVIew在android的应用指南
背景
目前,大部分市场应用在展示产品的时候都会选择图片配文字的形式,显得更加直观。随着人们手机设备性能的提高与Wifi以及4G网络的提速,为了能让用户的体验更加立体,很多APP在”秀“自己的产品的时候都会直接展示视频。然而图片和视频之间还是有一定的流量差距,为了让用户可以更好的过渡这一差距,图片展示gifview,点击gifview观看视频这样的用户行为正在慢慢的被接受。
58部落目前是非常大的用户群体,他们也会经常发表一些自己的作品,看法。目前也是列表页展示图片,点击进入后展示详情。那么如果需要有这个过渡,就需要在列表页上增加gifview来达到更好的曝光率。
大众点评&马蜂窝 点评 & 马蜂窝 效果展示 效果分析
我们先来自己想想,如果要是我们自己来实现这个效果应该如何来做:
两种方法:
方案一:
1.使用recyclerview实现列表页用于展示;
2.自定义GifView,包含展示静态图和gif图的功能;
3.进入页面,请求首页,获取json得到gif;
4.解析gif的第一帧,得到Image的比特流,让GifView展示图片;
5.图片展示完成后,自定义GifView播放GifView;
优点:
简化json输出,json里面的返回值返回一套gif就可以,自己解析gif的第一帧用于展示图片;
缺点:
速度慢,本来列表页快速滑动展示大图片都考虑加载时间,如果再去自己解析,成本太高,内存要求大;
方案二:
1.使用recyclerview实现列表页用于展示;
2.自定义ImageView,展示Image;
3.自定义GifView,展示gif图;
4.进入页面,请求首页,获取json得到image和gif;
5.自定义ImageView展示imageview占位,然后紧接着加载gif;
优点:
1.速度快;
2.解耦,一旦出现问题,可以快速降级;
另外,从版本的迭代的上来考虑,我个人更倾向于方案二:
点评效果深入研究
接下来,先上常规操作让我们看一下大众点评是不是酱样婶的吧:
dump一下,你不知道
Running activities (most recent first):
Run #1: ActivityRecord{1b8520 u0 com.dianping.v1/.NovaMainActivityt15792}
Run #0: ActivityRecord{2a47964 u0 com.tencent.mm/.ui.LauncherUIt15793}
Running activities (most recent first):
Run #0: ActivityRecord{945d44 u0 com.miui.home/.launcher.Launchert1}
Running activities (most recent first):
Run #0: ActivityRecord{52d96ba u0 com.android.systemui/.recents.RecentsActivityt15788}
首先,我们来看一下dump信息,NovaMainActivity,是它的首页,但是显然根据这个我们没有任何头绪。正向查一个控件我们要知道哪个布局,哪个控件,哪个View。所以,我想能不能看看Log,结果还真的让我发现了蛛丝马迹。
logcat
2019-09-1410:59:54.30431909-31909/? D/GifImageView: gifIv has already been stoped:
2019-09-1410:59:54.30431909-31909/? D/GifImageView: gifIv has already been stoped:
2019-09-1410:59:54.30431909-31909/? D/GifImageView: gifIv has already been started:
在我快速滑动的时候,我发现居然有这么些可爱的代码在控制台打印出来。于是,我就看到了新的曙光。
万幸的是,我还在logcat里面额外看到了webp格式的图片和动图的日志:
//动图
https://img.xxx.net/coverpic/2d348f2ea08616ab1e8c652800373740.webp
//非动图
https://img.xxx.net/coverpic/4cf4cfa5469f95444986f83f194f6acb35706.jpg%40320w_426h_1e_1c_1l%7Cwatermark%3D0.webp
这个日志初步印证了我的想法,我决定看一下”GifImageVIew”都干了啥。点评是有混淆和做了加壳的,常规的jd-gui查看的代码看不到。通过脱壳,获取其相关代码,为了更好地理解,里面的关键代码做了注释:
publicclassGifImageViewextendsFrameLayout{
publicGifImageView(Context context){
super(context);
}
publicstaticfinalString TAG = "GifImageView";
publicPicassoImageView gifImageView; //Picasso
privateString gifIvGroup;
privatedoublegifPriority; //gif的优先级
privateString gifUrl; //gif的url
publicPicassoImageView imageView; //又来一个Picasso
//构造函数
publicGifImageView(Context context){
this(context, null);
}
//构造函数
publicGifImageView(Context context, AttributeSet attributeSet){
this(context, attributeSet, 0);
}
//构造函数
publicGifImageView(Context context, AttributeSet attributeSet, inti){
super(context, attributeSet, i);
init(context);
}
//初始化 *关键*
privatevoidinit(Context context){
LayoutParams layoutParams = newFrameLayout.LayoutParams(-1, -1);
this.imageView = newPicassoImageView(context);
this.gifImageView = newPicassoImageView(context);
this.gifImageView.setFadeInDisplayEnabled(false);
addView(this.gifImageView, layoutParams);
addView(this.imageView, layoutParams);
//开始进行gif加载的设置
this.gifImageView.setChangeListener(newu {
//gif加载开始
publicvoidonImageLoadStart{
GifImageView.this.imageView.setVisibility(View.VISIBLE);
}
//gif加载完成
publicvoidonImageLoadSuccess(Bitmap bitmap){
StringBuilder stringBuilder = newStringBuilder;
stringBuilder.append("load gif success : ");
stringBuilder.append(GifImageView.this.gifImageView.getURL);
b.a(GifImageView.class, stringBuilder.toString);
GifImageView.this.imageView.setVisibility(View.GONE);
}
//加载失败如何处理
publicvoidonImageLoadFailed{
StringBuilder stringBuilder = newStringBuilder;
stringBuilder.append("load gif failed : ");
stringBuilder.append(GifImageView.this.gifImageView.getURL);
b.a(GifImageView.class, stringBuilder.toString);
GifImageView.this.imageView.setVisibility(View.VISIBLE);
}
});
}
//设置布局
publicvoidsetLayoutParams(LayoutParams layoutParams){
super.setLayoutParams(layoutParams);
setViewParams(this.imageView, layoutParams);
setViewParams(this.gifImageView, layoutParams);
}
//设置布局
privatevoidsetViewParams(View view, LayoutParams layoutParams){
LayoutParams layoutParams2 = view.getLayoutParams;
if(layoutParams2 instanceofFrameLayout.LayoutParams) {
layoutParams2.width = layoutParams.width;
layoutParams2.height = layoutParams.height;
view.setLayoutParams(layoutParams2);
}
}
//开始执行gif播放
publicvoidstartGif{
if(this.gifImageView.isImageAnimating) {
Log.d(TAG, "gifIv has already been started: ");
} else{
this.gifImageView.setAnimatedImageLooping(-1);
this.gifImageView.startImageAnimation;
Log.d(TAG, "gifIv has been started: ");
}
}
//停止执行gif播放
publicvoidstopGif{
if(this.gifImageView.isImageAnimating) {
this.gifImageView.setAnimatedImageLooping(0);
this.gifImageView.stopImageAnimation;
Log.d(TAG, "gifIv has been stoped: ");
} else{
Log.d(TAG, "gifIv has already been stoped: ");
}
}
//设置image和gif图像地址
publicvoidsetGifImage(String str, String str2){
this.imageView.setImage(str);
this.gifImageView.setAnimatedImageLooping(0);
this.gifImageView.setImage(str2);
this.imageView.setVisibility(0);
this.gifUrl = str2;
if(TextUtils.isEmpty(str2)) {
GifImageViewManager.getInstance.addGifIv(this);
} else{
GifImageViewManager.getInstance.removeGifIv(this);
}
}
publicvoidsetAnimatedImageLooping(inti){
this.imageView.setAnimatedImageLooping(i);
}
publicvoidsetScaleType(ImageView.ScaleType scaleType){
this.imageView.setScaleType(scaleType);
this.gifImageView.setScaleType(scaleType);
}
//支持直接设置drawable
publicvoidsetImageDrawable(Drawable drawable){
this.imageView.setVisibility(0);
this.imageView.setImageDrawable(drawable);
}
.....此处省略1000字
}
另外,还发现它的自定义图片PicassoImageView(好像跟git上面的Picasso没什么关系)。
importcom.dianping.imagemanager.DPImageView;
public classPicassoImageViewextendsDPImageViewimplementsClippable{
}
public classDPImageViewextendsImageViewimplementsOnClickListener{
}
还有。。。。咳咳,让我们点到为止吧。
点评效果总结
所以,点评的基本逻辑跟我之前说的第二种方案几乎无差,我们再来回顾一下:
1.自定义View命名为PicassoImageView,可以展示Gif图也可以展示ImageView;
2.封装GifImageView,里面包含两个PicassoImageView;
3.其中一个PicassoImageView展示imageview占位,同时另外一个PicassoImageView进行gif的加载,加载完成后,把第一个PicassoImageView消失;
so,感谢点评为我们提供宝贵的思路,接下来让我们去看看马蜂窝是怎么实现的吧。
马蜂窝效果深入研究 dump之后依然没有有用信息
也不是完全没有用,至少你知道了程序的入口在哪里。
看看logcat
2019-09-1816:53:11.78625404-25919/? D/SoLoader:About to load:libgifimage.so
2019-09-1816:53:11.78725404-25919/? D/SoLoader:libgifimage.so not found on /data/data/com.mfw.roadbook/lib-main
2019-09-1816:53:11.78725404-25919/? D/SoLoader:libgifimage.so found on /data/app/com.mfw.roadbook-U4eSwGYqvGPqtvkBe8R4gw==/lib/arm
2019-09-1816:53:11.78725404-25919/? D/SoLoader:Not resolving dependencies forlibgifimage.so
2019-09-1816:53:11.79525404-25919/? D/SoLoader:Loaded:libgifimage.so
嗯,果然,看看还是有收获的。又Get到一个新知识,可以通过加载so(libgifimage.so)的方式,提升GifView加载速度。
马蜂窝效果总结
在实际体验的过程中,我发现滑动到没有加载的图片时,马蜂窝会用一个加载的灰色占位图占位,然后去下载gif,它旁边的图片都展示出来了,gif还没有下载完,体验不是很好。这点可以借鉴一下大众点评的。
由于马蜂窝也加壳了,脱壳其实是很开(fei)心(shen)的,有了点评的思路,我就没有特别深入的研究马蜂窝的内部实现,其实直接看效果也能看出个大概。
有没有开源的库呢?
我们并不想把点评或者马蜂窝的代码直接拷过来,毕竟人家没有开源,而且也不一定契合我们的风格。在git上找一个demo实现以下?
于是,我看到了这个android-gif-drawable,这个看起来还不错,7K的赞,Fork了1.6K,正合我意,来吧,研究一波。
API解读
android-gif-drawable是通过JNI来渲染帧的,比使用WebView或者Movie性能要好一些。
依赖
dependencies {
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.19'
}
repositories {
mavenCentral
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
dependencies {
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.+'
}
基本使用
//1. asset文件
GifDrawable gifFromAssets = newGifDrawable( getAssets, "anim.gif");
//2. resource (drawable or raw)
GifDrawable gifFromResource = newGifDrawable( getResources, R.drawable.anim );
//3. byte array
byte[] rawGifBytes = ...
GifDrawable gifFromBytes = newGifDrawable( rawGifBytes );
//4. FileDeor
FileDeor fd = newRandomAccessFile( "/path/anim.gif", "r").getFD;
GifDrawable gifFromFd = newGifDrawable( fd );
//5. file path
GifDrawable gifFromPath = newGifDrawable( "/path/anim.gif");
//6. file
File gifFile = newFile(getFilesDir,"anim.gif");
GifDrawable gifFromFile = newGifDrawable(gifFile);
//7. AssetFileDeor
AssetFileDeor afd = getAssets.openFd( "anim.gif");
GifDrawable gifFromAfd = newGifDrawable( afd );
//8. InputStream (it must support marking)
InputStream sourceIs = ...
BufferedInputStream bis = newBufferedInputStream( sourceIs, GIF_LENGTH );
GifDrawable gifFromStream = newGifDrawable( bis );
//9. direct ByteBuffer
ByteBuffer rawGifBytes = ...
GifDrawable gifFromBytes = newGifDrawable( rawGifBytes );
额外的API
- 停止GIF动画
·stop
- 开始GIF动画
·start
- GIf动画是否在执行
isRunning
- 重置GIF动画
reset
- 控制执行动画的速度
setSpeed(float factor)
- 从该动画的执行位置开始执行
seekTo(int position)
- 动画的持续时间
getDuration
- 当前动画的播放时间
getCurrentPosition
调用方法如下:
try{
GifDrawablegifFromResDrawable = newGifDrawable( mContext.getResources, getIntGifRes(imageData.gifUrl));
viewHolder.gifImageView.setImageDrawable(gifFromResDrawable);
viewHolder.gifImageView.setVisibility(View.VISIBLE);
} catch(Exceptione) {
e.printStackTrace;
}
所以,我们看到,本质上还是这个GifDrawable在起作用,因为GifImageView继承的是ImageView。
效果如下 撸一个Demo
我们看完了大众点评、马蜂窝、github上的实现效果。它们有自己的优点,结合58自己的技术特点,我打算采用的技术架构:FRESCO + RecyclerView的StaggeredGridLayoutManager,具体实现思路如下:
使用StaggeredGridLayoutManager实现瀑布布局;
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
StaggeredGridLayoutManager layoutManager = newStaggeredGridLayoutManager(2,
StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
FrescoAdapter adapter = newFrescoAdapter(this, DataUtils.getFrescoImageData);
recyclerView.setAdapter(adapter);
自定义Adapter加载图片和GIF:
static classViewHolderextendsRecyclerView.ViewHolder{
SimpleDraweeViewdraweeImage;
SimpleDraweeViewdraweeGif;
TextViewtextView;
public ViewHolder(@NonNullViewitemView) {
super(itemView);
draweeImage = (SimpleDraweeView) itemView.findViewById(R.id.item_draweeview);
draweeGif = (SimpleDraweeView) itemView.findViewById(R.id.item_draweeview_gif);
textView = (TextView) itemView.findViewById(R.id.item_draweeview_text);
}
}
加载图片和GIF
/**
* Fresco 加载webp图片
* @param draweeView
* @param imageUrl
*/
publicstaticvoidloadWebpImage(finalContext context, finalSimpleDraweeView draweeView, finalImageData imageData, StringimageUrl, finalbooleanreSize, finalintposition) {
DraweeController controller = Fresco.newDraweeControllerBuilder
.setUri(Uri.parse(imageUrl))
.setAutoPlayAnimations(true)
.setOldController(draweeView.getController)
.setControllerListener(newControllerListener {
@Override
publicvoidonSubmit(Stringid, ObjectcallerContext) {
}
@Override
publicvoidonFinalImageSet(Stringid, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
if(imageInfo == null) {
return;
}
if(imageData.getScale == 0){
intwidth= imageInfo.getWidth;
intheight= imageInfo.getHeight;
floatscale= (float) width/ (float) height;
imageData.setScale(scale);
}
finalViewGroup.LayoutParams layoutParams = draweeView.getLayoutParams;
layoutParams.width= DisplayUtils.getScreenWidth((Activity) context) / 2- DisplayUtils.dp2px(context,10);
layoutParams.height= (int) (layoutParams.width/ imageData.getScale);
imageData.setWidth(layoutParams.width);
imageData.setHeight(layoutParams.height);
imageData.setPosition(position);
draweeView.setLayoutParams(layoutParams);
}
@Override
publicvoidonIntermediateImageSet(Stringid, @Nullable ImageInfo imageInfo) {
}
@Override
publicvoidonIntermediateImageFailed(Stringid, Throwable throwable) {
}
@Override
publicvoidonFailure(Stringid, Throwable throwable) {
}
@Override
publicvoidonRelease(Stringid) {
}
})
.build;
draweeView.setController(controller);
}
/**
* Fresco 加载webpGID
* @param imageView
* @param imageUrl
*/
publicstaticvoidloadWebpGif(finalContext context, finalSimpleDraweeView imageView,finalSimpleDraweeView gifView, finalImageData imageData, StringimageUrl, finalbooleanreSize, finalintposition) {
DraweeController controller = Fresco.newDraweeControllerBuilder
.setUri(Uri.parse(imageUrl)).setAutoPlayAnimations(true).setOldController(gifView.getController)
.setControllerListener(newControllerListener {
@Override
publicvoidonSubmit(Stringid, ObjectcallerContext) {
}
@Override
publicvoidonFinalImageSet(Stringid, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
if(imageInfo == null) {
return;
}
finalViewGroup.LayoutParams layoutParams = imageView.getLayoutParams;
finalViewGroup.LayoutParams gifLayoutParams = gifView.getLayoutParams;
gifLayoutParams.width= layoutParams.width;
gifLayoutParams.height= layoutParams.height;
gifView.setLayoutParams(gifLayoutParams);
}
@Override
publicvoidonIntermediateImageSet(Stringid, @Nullable ImageInfo imageInfo) {
}
@Override
publicvoidonIntermediateImageFailed(Stringid, Throwable throwable) {
}
@Override
publicvoidonFailure(Stringid, Throwable throwable) {
}
@Override
publicvoidonRelease(Stringid) {
}
})
.build;
gifView.setController(controller);
}
效果如下 缺点与不足
Demo里面还有很多的异常边界情况没有考虑,比如各类的容错判断,性能问题监控,等等。
有兴趣的小伙伴可以看这里:
传送门:https://github.com/Mozziehh/GifView
https://www.jianshu.com/p/057f48df855b
https://github.com/koral–/android-gif-drawable
https://www.dev2qa.com/how-to-play-gif-file-use-android-graphics-movie-class/
https://blog.csdn.net/feather_wch/article/details/79558240
责任编辑: