Android ViewPager中显示图片与播放视频的填坑记录
ViewPager介绍
ViewPager的功能就是可以使视图滑动,就像Lanucher左右滑动那样。
ViewPager用于实现多页面的切换效果,该类存在于Google的兼容包android-support-v4.jar里面.
ViewPager:
1)ViewPager类直接继承了ViewGroup类,所有它是一个容器类,可以在其中添加其他的view类。
2)ViewPager类需要一个PagerAdapter适配器类给它提供数据。
3)ViewPager经常和Fragment一起使用,并且提供了专门的FragmentPagerAdapter和FragmentStatePagerAdapter类供Fragment中的ViewPager使用。
4)在编写ViewPager的应用的使用,还需要使用两个组件类分别是PagerTitleStrip类和PagerTabStrip类,PagerTitleStrip类直接继承自ViewGroup类,而PagerTabStrip类继承PagerTitleStrip类,所以这两个类也是容器类。但是有一点需要注意,在定义XML的layout的时候,这两个类必须是ViewPager标签的子标签,不然会出错。
本文将详细介绍关于AndroidViewPager中显示图片与播放视频填坑的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。
一.需求来源与实现思路
1.最近项目需求中有用到需要在ViewPager中播放视频和显示图片的功能,视频是本地视频,最开始的实现思路是ViewPager中根据当前item位置对应的是图片还是视频去初始化PhotoView和SurfaceView,同时销毁时根据item的位置去判断移除PhotoView和SurfaceView。
2.上面那种方式确实是可以实现的,但是存在2个问题,第一,MediaPlayer的生命周期不容易控制并且存在内存泄漏问题。第二,连续三个item都是视频时,来回滑动的过程中发现会出现上个视频的最后一帧画面的bug。
3.未提升用户体验,视频播放器初始化完成前上面会覆盖有该视频的第一帧图片,但是发现存在第一帧图片与视频第一帧信息不符的情况,后面会通过代码给出解决方案。
4.图片和视频尺寸如何适配以保证不变形。
二.需要填的坑
1.对于MediaPlayer的生命周期不容易控制的本质原因是这种实现思路上我的播放器只有1个,频繁的初始化和销毁造成了问题,所以后面我更改了实现方式,一个item的视频对应一个播放器。
2.对于滑动过程中发现会出现上个视频的最后一帧画面的bug,发现是surfaceView这个控件造成的,后面通过将播放的载体更换为TextureView完美解决该问题。
3.SurfaceView与TextureView的本质异同
第一:两者都能在独立的线程中绘制和渲染,在专用的GPU线程中大大提高渲染的性能。
第二:SurfaceView专门提供了嵌入视图层级的绘制界面,开发者可以控制该界面像Size等的形式,能保证界面在屏幕上的正确位置。但也有局限:
1.由于是独立的一层View,更像是独立的一个Window,不能加上动画、平移、缩放;
2.两个SurfaceView不能相互覆盖。
第三:Texture更像是一般的View,像TextView那样能被缩放、平移,也能加上动画。TextureView只能在开启了硬件加速的Window中使用,并且消费的内存要比SurfaceView多,并伴随着1-3帧的延迟。
第四:屏幕锁屏时SurfaceView会销毁重建,TextureView不会!
三.具体实现核心代码
1.ViewPager的初始化
mAdapter=ImageBrowseFragmentPagerAdapter(supportFragmentManager,this,imgs) imgs_viewpager.offscreenPageLimit=1 imgs_viewpager.adapter=mAdapter imgs_viewpager.currentItem=mPosition //为了处理首次点击时视频播放的问题 valmessage=Message.obtain() message.what=START_PLAY_VIDEO mHandler.sendMessageDelayed(message,200)
2.Handler处理消息
privatevalSTART_PLAY_VIDEO=0 privatevarDELETE_VIDEO=1 privatevarDELETE_VIDEO_START_PLAY=2 privatevarmHandler=Handler(Handler.Callback{msg-> when(msg.what){ //开始播放视频 START_PLAY_VIDEO->NotifyDispatch.dispatch(PreviewPlayVideoEvent(mPosition)) //删除视频时刷新ui DELETE_VIDEO->{ mAdapter?.setImgs(imgs) } //解决删除视频时之后跳转到另一个item,当它是视频时不继续播放的问题 DELETE_VIDEO_START_PLAY->NotifyDispatch.dispatch(PreviewPlayVideoEvent(mDeletePosition)) } true })
3.删除视频或图片的处理逻辑
privatefundeletePhotos(position:Int){ if(imgs!!.isEmpty()){ return } ThreadDispatch.right_now.execute({ varfile:File? file=File(imgs.get(position)) if(file!=null&&file?.exists()!!){ valintent=Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) valuri=Uri.fromFile(file) intent.data=uri sendBroadcast(intent) file?.delete() imgs.removeAt(position) } if(position==imgs.size){ mDeletePosition=position-1 }else{ mDeletePosition=position } valmessage=Message.obtain() message.what=DELETE_VIDEO mHandler.sendMessage(message) NotifyDispatch.dispatch(DeletePreviewPhotoEvent(imgs)) valmessage1=Message.obtain() message1.what=DELETE_VIDEO_START_PLAY mHandler.sendMessageDelayed(message1,200) if(imgs.isEmpty()){ finish() } }) //} }
4.ViewPager对应的Adapter
packagecom.immomo.camerax.gui.view.adapter; importandroid.content.Context; importandroid.os.Bundle; importandroid.support.v4.app.Fragment; importandroid.support.v4.app.FragmentManager; importandroid.support.v4.app.FragmentStatePagerAdapter; importandroid.support.v4.app.FragmentTransaction; importandroid.view.View; importandroid.view.ViewGroup; importcom.immomo.camerax.gui.fragment.PreviewImgFragment; importcom.immomo.camerax.gui.fragment.PreviewVideoFragment; importjava.util.ArrayList; importjava.util.List; /** *Createdbyliuxuon2018/3/26. */ publicclassImageBrowseFragmentPagerAdapterextendsFragmentStatePagerAdapter{ privateContextmContext; privateListdatas; privateintmCurrentSelectedPosition=-1; privateFragmentManagermFragmentManager; privateFragmentTransactionmFragmentTransaction; privateArrayList mFragments=newArrayList<>(); publicImageBrowseFragmentPagerAdapter(FragmentManagerfm,Contextcontext,List datas){ super(fm); mFragmentManager=fm; mContext=context; this.datas=datas; } publicvoidremoveContext(){ mContext=null; } @Override publicvoidsetPrimaryItem(ViewGroupcontainer,intposition,Objectobject){ mCurrentSelectedPosition=position; } @Override publicvoidstartUpdate(ViewGroupcontainer){ super.startUpdate(container); } publicvoidsetImgs(List imgs){ this.datas=imgs; notifyDataSetChanged(); } //处理更新无效----删除条目 @Override publicintgetItemPosition(Objectobject){ returnPOSITION_NONE; } publicintgetPrimaryItemPosition(){ returnmCurrentSelectedPosition; } publicImageBrowseFragmentPagerAdapter(FragmentManagerfm){ super(fm); } @Override publicbooleanisViewFromObject(Viewview,Objectobject){ returnview==((Fragment)object).getView(); } @Override publicFragmentgetItem(intposition){ Bundlebundle=newBundle(); bundle.putString("url",datas.get(position)); bundle.putInt("position",position); if(datas.get(position).endsWith(".jpg")){ PreviewImgFragmentpreviewImgFragment=newPreviewImgFragment(); previewImgFragment.setArguments(bundle); returnpreviewImgFragment; }else{ PreviewVideoFragmentpreviewVideoFragment=newPreviewVideoFragment(); previewVideoFragment.setArguments(bundle); returnpreviewVideoFragment; } } @Override publicintgetCount(){ returndatas==null?0:datas.size(); } @Override publicObjectinstantiateItem(ViewGroupcontainer,intposition){ returnsuper.instantiateItem(container,position); } @Override publicvoiddestroyItem(ViewGroupcontainer,intposition,Objectobject){ super.destroyItem(container,position,object); } }
5显示图片对应的Fragment
packagecom.immomo.camerax.gui.fragment; importandroid.os.Bundle; importandroid.support.annotation.Nullable; importandroid.support.v4.app.Fragment; importandroid.view.LayoutInflater; importandroid.view.View; importandroid.view.ViewGroup; importcom.bumptech.glide.Glide; importcom.immomo.camerax.R; importcom.immomo.camerax.foundation.util.StatusBarUtils; importcom.immomo.camerax.gui.view.ResizablePhotoView; /** *Createdbyliuxuon2018/3/27. */ publicclassPreviewImgFragmentextendsFragment{ @Nullable @Override publicViewonCreateView(LayoutInflaterinflater,@NullableViewGroupcontainer,@NullableBundlesavedInstanceState){ Viewview=inflater.inflate(R.layout.fragment_preview_photo,null); ResizablePhotoViewresizablePhotoView=view.findViewById(R.id.customPhotoView); Stringurl=getArguments().getString("url"); Glide.with(getContext()).load(url).into(resizablePhotoView); resizablePhotoView.setOnClickListener(newView.OnClickListener(){ @Override publicvoidonClick(Viewview){ getActivity().finish(); } }); returnview; } @Override publicvoidonPause(){ super.onPause(); } @Override publicvoidonResume(){ super.onResume(); } @Override publicvoidonDetach(){ super.onDetach(); } @Override publicvoidonDestroyView(){ super.onDestroyView(); } @Override publicvoidonStart(){ super.onStart(); } @Override publicvoidonDestroy(){ super.onDestroy(); } @Override publicvoidsetUserVisibleHint(booleanisVisibleToUser){ super.setUserVisibleHint(isVisibleToUser); } }
6.图片根据宽度适配高度的自定义View
packagecom.immomo.camerax.gui.view; importandroid.content.Context; importandroid.graphics.drawable.Drawable; importandroid.util.AttributeSet; importcom.github.chrisbanes.photoview.PhotoView; /** *Createdbyliuxuon2018/4/7. */ publicclassResizablePhotoViewextendsPhotoView{ publicResizablePhotoView(Contextcontext){ super(context); } publicResizablePhotoView(Contextcontext,AttributeSetattr){ super(context,attr); } publicResizablePhotoView(Contextcontext,AttributeSetattr,intdefStyle){ super(context,attr,defStyle); } @Override protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ Drawabled=getDrawable(); if(d!=null){ intwidth=MeasureSpec.getSize(widthMeasureSpec); //高度根据使得图片的宽度充满屏幕计算而得 intheight=(int)Math.ceil((float)width*(float)d.getIntrinsicHeight()/(float)d.getIntrinsicWidth()); setMeasuredDimension(width,height); }else{ super.onMeasure(widthMeasureSpec,heightMeasureSpec); } } }
7.播放视频对应的Fragment
/** *Createdbyliuxuon2018/3/27. */ publicclassPreviewVideoFragmentextendsFragment{ privateImageViewmPhotoView; privateTextureViewmTextureView; privateStringmUrl; privateintmPosition; privateAndroidMediaPlayermIjkVodMediaPlayer; privatebooleanmIsSelected; privatebooleanmIsFirstPrepared; privatePreviewPlayVideoSubscribermPreviewPlayVideoSubscriber=newPreviewPlayVideoSubscriber(){ @Override publicvoidonEventMainThread(PreviewPlayVideoEventevent){ super.onEventMainThread(event); MDLog.e("liuxu",event.getPosition()+""); if(event!=null&&event.getPosition()==mPosition){ //说明是当前条目 if(mIjkVodMediaPlayer!=null&&!mIjkVodMediaPlayer.isPlaying()){ if(mTextureView!=null){ mIjkVodMediaPlayer.setSurface(mSurface); mIjkVodMediaPlayer.prepareAsync(); mPhotoView.setVisibility(View.VISIBLE); } } mIsSelected=true; }else{ if(mIjkVodMediaPlayer!=null&&mIjkVodMediaPlayer.isPlaying()){ mIjkVodMediaPlayer.pause(); mIjkVodMediaPlayer.stop(); } if(mPhotoView!=null){ mPhotoView.setVisibility(View.VISIBLE); } mIsSelected=false; } } }; privateStringmWidth; privateStringmHeight; @Nullable @Override publicViewonCreateView(LayoutInflaterinflater,@NullableViewGroupcontainer,@NullableBundlesavedInstanceState){ mPreviewPlayVideoSubscriber.register(); Viewview=inflater.inflate(R.layout.fragment_preview_video,null); mPhotoView=view.findViewById(R.id.photoView); mTextureView=view.findViewById(R.id.surfaceView); mUrl=getArguments().getString("url"); mPosition=getArguments().getInt("position"); layoutPlayer(); loadVideoScreenshot(getContext(),mUrl,mPhotoView,1); view.setOnClickListener(newView.OnClickListener(){ @Override publicvoidonClick(Viewview){ release(); getActivity().finish(); } }); initTextureMedia(); returnview; } privatevoidinitTextureMedia(){ mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } privatevoidplay(Stringurl){ try{ mIjkVodMediaPlayer=newAndroidMediaPlayer(); mIjkVodMediaPlayer.reset(); mIjkVodMediaPlayer.setDataSource(url); //让MediaPlayer和TextureView进行视频画面的结合 mIjkVodMediaPlayer.setSurface(mSurface); //设置监听 mIjkVodMediaPlayer.setOnBufferingUpdateListener((mp,percent)->{ }); mIjkVodMediaPlayer.setOnCompletionListener(mp->{ mp.seekTo(0); mp.start(); }); mIjkVodMediaPlayer.setOnInfoListener((mp1,what,extra)->{ if(what==IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START){ mPhotoView.setVisibility(View.GONE); mIsFirstPrepared=true; } returnfalse; }); mIjkVodMediaPlayer.setOnErrorListener((mp,what,extra)->false); mIjkVodMediaPlayer.setOnPreparedListener(mp->{ mp.start(); if(!mIsFirstPrepared){ }else{ mPhotoView.setVisibility(View.GONE); } }); mIjkVodMediaPlayer.setScreenOnWhilePlaying(true);//在视频播放的时候保持屏幕的高亮 if(mIsSelected){ //异步准备 mIjkVodMediaPlayer.prepareAsync(); } }catch(Exceptione){ e.printStackTrace(); } } privateSurfacemSurface; privateTextureView.SurfaceTextureListenermSurfaceTextureListener=newTextureView.SurfaceTextureListener(){ @Override publicvoidonSurfaceTextureAvailable(SurfaceTexturesurface,intwidth,intheight){ mSurface=newSurface(surface); play(mUrl); } @Override publicvoidonSurfaceTextureSizeChanged(SurfaceTexturesurface,intwidth,intheight){ } @Override publicbooleanonSurfaceTextureDestroyed(SurfaceTexturesurface){ if(mSurface!=null){ mSurface.release(); mSurface=null; } if(mTextureView!=null){ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ mTextureView.releasePointerCapture(); } } release(); returnfalse; } @Override publicvoidonSurfaceTextureUpdated(SurfaceTexturesurface){ } }; @Override publicvoidonStart(){ super.onStart(); } @Override publicvoidonAttach(Contextcontext){ super.onAttach(context); } @Override publicvoidonDetach(){ super.onDetach(); } @Override publicvoidonPause(){ MDLog.e("liuxu","onPause"+mPosition); //处理锁屏时播放器停止播放 if(mIjkVodMediaPlayer!=null&&mIjkVodMediaPlayer.isPlaying()){ mIjkVodMediaPlayer.pause(); mIjkVodMediaPlayer.stop(); } super.onPause(); } //屏幕打开时重新播放 @Override publicvoidonResume(){ MDLog.e("liuxu","onResume"+mPosition); super.onResume(); if(mIsSelected&&mIjkVodMediaPlayer!=null&&!mIjkVodMediaPlayer.isPlaying()){ mIjkVodMediaPlayer.prepareAsync(); } } @Override publicvoidonDestroy(){ MDLog.e("liuxu","onDestroy"); release(); if(mPreviewPlayVideoSubscriber.isRegister()){ mPreviewPlayVideoSubscriber.unregister(); } super.onDestroy(); } privatevoidrelease(){ if(mIjkVodMediaPlayer==null){ return; } if(mIjkVodMediaPlayer.isPlaying()){ mIjkVodMediaPlayer.stop(); } mIjkVodMediaPlayer.release(); mIjkVodMediaPlayer=null; } @Override publicbooleangetUserVisibleHint(){ returnsuper.getUserVisibleHint(); } /** *动态设置视频宽高信息 */ privatevoidlayoutPlayer(){ //获取视频宽高比 getPlayInfo(mUrl); floatratio=Float.parseFloat(mHeight)/Float.parseFloat(mWidth); MDLog.e("type",mPosition+"ratio"+ratio); inttype=0; //添加容错值 if(ratioMediaConstants.INSTANCE.getASPECT_RATIO_1_1().toFloat()-MediaConstants.INSTANCE.getSCREEN_DEFAULT_VALUE()){ type=ScreenAdapterUtils.INSTANCE.getSCALE_TYPE_11(); }elseif(ratio MediaConstants.INSTANCE.getASPECT_RATIO_4_3().toFloat()-MediaConstants.INSTANCE.getSCREEN_DEFAULT_VALUE()){ type=ScreenAdapterUtils.INSTANCE.getSCALE_TYPE_43(); MDLog.e("type","43"); }elseif(ratio MediaConstants.INSTANCE.getASPECT_RATIO_16_9().toFloat()-MediaConstants.INSTANCE.getSCREEN_DEFAULT_VALUE()){ type=ScreenAdapterUtils.INSTANCE.getSCALE_TYPE_169(); MDLog.e("type","169"); } FrameLayout.LayoutParamslayoutParams=(FrameLayout.LayoutParams)mTextureView.getLayoutParams(); layoutParams.height=ScreenAdapterUtils.INSTANCE.getSurfaceHeight(type); mTextureView.setLayoutParams(layoutParams); FrameLayout.LayoutParamsparams=(FrameLayout.LayoutParams)mPhotoView.getLayoutParams(); params.height=ScreenAdapterUtils.INSTANCE.getSurfaceHeight(type); mPhotoView.setLayoutParams(params); MDLog.e("params.height",params.height+""); } privatevoidgetPlayInfo(StringmUri){ android.media.MediaMetadataRetrievermmr=newandroid.media.MediaMetadataRetriever(); try{ if(mUri!=null){ mmr.setDataSource(mUri); }else{ //mmr.setDataSource(mFD,mOffset,mLength); } //宽 mWidth=mmr.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); //高 mHeight=mmr.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); //mBitmap=mmr.getFrameAtTime(1); }catch(Exceptionex){ }finally{ mmr.release(); } } publicstaticvoidloadVideoScreenshot(finalContextcontext,Stringuri,ImageViewimageView,longframeTimeMicros){ //这里的时间是以微秒为单位 RequestOptionsrequestOptions=RequestOptions.frameOf(frameTimeMicros); requestOptions.set(FRAME_OPTION,MediaMetadataRetriever.OPTION_CLOSEST); requestOptions.transform(newBitmapTransformation(){ @Override protectedBitmaptransform(@NonNullBitmapPoolpool,@NonNullBitmaptoTransform,intoutWidth,intoutHeight){ returntoTransform; } @Override publicvoidupdateDiskCacheKey(MessageDigestmessageDigest){ try{ messageDigest.update((context.getPackageName()+"RotateTransform").getBytes("utf-8")); }catch(Exceptione){ e.printStackTrace(); } } }); Glide.with(context).load(uri).apply(requestOptions).into(imageView); } }
4.结语
笔者使用这种方式实现了项目需求,但是由于本人接触音视频的相关内容比较少,全是在不断探索和学习中前进,如有不足之处请评论指正,谢谢。大家共同学习共同进步。
好了以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。