上篇文章(https://www.jianshu.com/p/7c3f1359edbd)介绍了如何快速获取手机中的视频和图片,本篇文章将接着上篇继续阐述,如何实现视频及图片的预览。具体代码,请参考https://github.com/life2smile/PhotoAlbum.git 。
本篇文章主要分为三个部分,第一部分是阐述要实现的效果;第一部分是图片预览实现;第二部分是视频预览实现。
一、实现的效果
需求
1、预览页面是可滚动的,即支持在预览页面预览当前图片的同时,可以滑动预览下一张或前一张的图片或者视频。
2、预览页面既要支持图片预览也要支持视频预览。
3、用户在点击宫格中任一图片或者视频跳转预览页面的时候,预览页要保证正确展示该图片或视频,而不是都展示第一张图片或者视频。
方案
对于第一个问题,预览页面是可滑动的,大家自然而然的就想到了viewpager,确实是这样,本篇文章也是采用viewpager进行实现(ViewFipper也是个不错的选择)。最关键的是与之对应的adapter怎么选择。因为手机中的图片和视频有可能有上千张甚至更多,因此要重点考虑下内存的使用,避免因预览而占用过多的内存,以引起卡顿什么被系统kill。为此,这里采用了继承FragmentStatePagerAdapter的实现方案来实现。FragmentStatePagerAdapter在页面不可见的时候会及时回收对应的页面,以避免消耗大量内存。注意,采用FragmentStatePagerAdapter,其承载视图的页面类型显然要是Fragment。
对于第二个问题,预览页面既要支持图片预览,又要支持视频预览,显然二者的视图是不一样的,因此这里提供两种不同类型的Fragment,一个是ImgeFragment;一个是VideoFragment,从名字很容易看出:ImgeFragment用于图片预览,VideoFragment用于视频预览。
对于第三个问题,根据上篇文章,用户首先会看到宫格视图,然后随便点击某一个视图即可预览,因此跳转的预览页面的时候需要明确告诉预览页面,要预览的当前页面的position,以正确加载。
二、滑动实现
这里对于滑动的实现主要介绍下Adapter对应的实现,其他实现可参考git上的项目源码。
class PreviewAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager) {//这里注意我们自定义的适配器继承自FragmentStatePagerAdapter
private var mPreviewDataPathList: MutableList<PreviewData> = ArrayList()
constructor(fragmentManager: FragmentManager, list: MutableList<PreviewData>?) : this(fragmentManager) {
list?.let {
mPreviewDataPathList.addAll(list)
}
}
override fun getItem(position: Int): Fragment {
val mediaData: PreviewData = mPreviewDataPathList[position]
mediaData.takeIf {//根据预览的文件类型来展示相应类型的视图,这里是视频预览页面
it.isVideo()
}?.let {
return VideoFragment.newInstance(it)
}
mediaData.takeIf {//图片预览页面
it.isImage()
}?.let {
return ImageFragment.newInstance(it)
}
return Fragment()//默认返回一个空页面。这种情况理论不会发生。
}
override fun getCount(): Int {
return mPreviewDataPathList.size
}
}
从代码可以看出,适配器会根据不同的文件类型来加载不同的页面,进而完成可滑动并同时支持图片和视频预览的功能。
三、实现图片预览
图片预览的思路很简单,拿到图片文件路径之后,加载到ImageView控件中即可。
class ImageFragment : Fragment() {
private var mFilePath: String? = null
companion object {//kotlin伴随对象,这里面的方法可以理解为java中的静态方法
fun newInstance(previewData: PreviewData): ImageFragment {//fragment的标准实现,用于接收外部参数
val fragment: ImageFragment = ImageFragment()
val bundle = Bundle()
bundle.putString("filePath", previewData.filePath)//图片路径
fragment.arguments = bundle
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
mFilePath = it.getString("filePath")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val imageView = ImageView(context)//这里直接通过代码生成了一个ImageView,和在xml中写实现的效果一样。
val margin: Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics).toInt()
imageView.setPadding(margin, 0, margin, 0)
mFilePath?.let {
//这里对预览的图片宽高进行处理:以屏幕宽度为准进行图片裁剪
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(mFilePath, options)
val screenW = resources.displayMetrics.widthPixels
val screenH = resources.displayMetrics.heightPixels
val resizeW = if (options.outWidth > screenW) screenW else options.outWidth
val resizeH = if (options.outHeight > screenH) screenH else options.outHeight
mFilePath?.let {
imageView.setImageBitmap(ImageResizeUtil.resize(it, resizeW, resizeH))
}
}
return imageView
}
}
四、视频预览
要解决的问题
本demo采用的视频预览组件为VideoView,视频预览有几个难点要注意
1、VideoView本身没有提供预览第一帧的功能,因此展示的时候会黑屏
2、VideoView也没有提供可视化的播放和暂停icon,需要自己展示
3、VideoView在调用播放接口(start)到正式展示视频第一帧的过程,有一段时间差,这段时间也是黑屏的状态。
4、VideoView默认无法控制其尺寸,也就是videoview会依据其视频源的尺寸进行默认布局。这个需要处理
5、要考虑到纵向视频以及横向视频的预览播放问题。
6、在用户滑动切换的时候要及时停止
方案
针对问题1,我们只需要在videoview上覆盖一个ImageView作为视频第一帧即可,而ImageView的图片来源及时先前获取到的视频缩略图
针对问题2,只需要提供一个播放icon,监听其点击事件,调用播放接口即可
针对问题3,这个过程实际上有监听的api,通过监听准备状态再将视频第一帧隐藏即可,但是这个方法在某些机型上存在bug,即准备完成一次后就不在回调监听api,这个会在代码进行注释解释。
针对问题4和5,需要我们自定义VideoView,以精确控制控件的大小。
针对问题6,我们只需要监听fragment的显示状态即可
具体实现代码如下:
首先是VideoFragment代码:
class VideoFragment : Fragment() {
private var mFilePath: String? = null
private var mThumbnailPath: String? = null
private var mVideoView: ResizeVideoView? = null//针对问题4和5自定义VideoView,具体代码会在下面给到
private var mPlayImg: ImageView? = null
private var mFirstFrameImg: ImageView? = null
private var mPrepared: Boolean = false//标识是否已经准备过了一次,用于解决在问题3方案中某些机型存在bug
companion object {
fun newInstance(previewData: PreviewData): Fragment {
val fragment = VideoFragment()
val bundle = Bundle()
bundle.putString("filePath", previewData.filePath)
bundle.putString("thumbNailPath", previewData.thumbnailPath)
fragment.arguments = bundle
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
mFilePath = it.getString("filePath")
mThumbnailPath = it.getString("thumbNailPath")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_preview_video, container, false)
initView(view)
resizeVideo(view)
setListener()
return view;
}
private fun setListener() {
mPlayImg?.setOnClickListener {//播放按钮
mPlayImg?.visibility = View.GONE
if (mPrepared) {//如果已经准备过一次,直接隐藏预览视频帧即可
mFirstFrameImg?.visibility = View.GONE
}
mVideoView?.start()//播放视频,这个方法调用之后,到真正播放之前会有延时,期间会产生黑屏,因此需要监听setOnPreparedListener接口,见下面代码
}
mVideoView?.setOnPreparedListener {//视频播准备完毕后,会回调该接口
it.setOnInfoListener(MediaPlayer.OnInfoListener { mp, what, extra ->
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {//标识准备完毕
mPrepared = true
mFirstFrameImg?.visibility = View.GONE//隐藏第一帧,开始展示视频播放
}
return@OnInfoListener true
})
}
mVideoView?.setOnCompletionListener {
pause()//视频播放完毕后及时停止
}
}
private fun initView(view: View) {
mVideoView = view.findViewById(R.id.fragment_preview_video)
mFirstFrameImg = view.findViewById(R.id.fragment_preview_first_frame)
mPlayImg = view.findViewById(R.id.fragment_preview_play_img)
mThumbnailPath?.let {
mFirstFrameImg?.setImageURI(Uri.fromFile(File(it)))
}
mFilePath?.let {
mVideoView?.setVideoPath(mFilePath)
}
}
//重新布局VieoView
private fun resizeVideo(view: View) {
val sizeArr: IntArray = videoReSize()
//这里需要注意,考虑到父viewgroup可能会有padding,所以这里处理掉,以避免无法生效
mVideoView?.resizeVideoView(sizeArr[0] - (view.paddingLeft + view.paddingRight),
sizeArr[1] - (view.paddingTop + view.paddingBottom))
mFirstFrameImg?.let {
val layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
layoutParams.width = sizeArr[0]
layoutParams.height = sizeArr[1]
layoutParams.gravity = Gravity.CENTER
it.layoutParams = layoutParams
}
}
//监听fragment的显示状态,当不可见时需要停止视频播放
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (!isVisibleToUser) {
pause()
}
}
override fun onPause() {
super.onPause()
pause()
}
//这里主要是获取需要展示出来的VideoView的高度和宽度,默认已屏幕的高宽进行适配。
private fun videoReSize(): IntArray {
val res = IntArray(2) { 0 }
mThumbnailPath?.let {
val option: BitmapFactory.Options = BitmapFactory.Options()
option.inJustDecodeBounds = true
BitmapFactory.decodeFile(mThumbnailPath, option)
val screenW = resources.displayMetrics.widthPixels
val screenH = resources.displayMetrics.heightPixels
val originW = option.outWidth
val originH = option.outHeight
(originH > originW).let {
if (it) {
res[1] = maxOf(originH, screenH)
res[0] = res[1] * originW / originH
} else {
res[0] = maxOf(originW, screenW)
res[1] = res[0] * originH / originW
}
}
}
return res
}
private fun pause() {
mVideoView?.pause()
resetStatus()
}
private fun resetStatus() {
mPlayImg?.visibility = View.VISIBLE
mFirstFrameImg?.visibility = View.VISIBLE
}
}
ResizeVideoView代码:
class ResizeVideoView : VideoView {
private var mVideoViewWidth: Int = 0
private var mVideoViewHeight: Int = 0
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
//代码的重点在此,主要是根据外部传入的高度和宽度进行resize,这里默认处理的前提是要明确指定mode为EXACTLY,即在使用该空间的父viewgroup中明确指定宽高或者指定为match_parent。当然可以扩展支持wrap_content等特性。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSize = if (mVideoViewWidth == 0) MeasureSpec.getSize(widthMeasureSpec) else mVideoViewWidth
val heightSize = if (mVideoViewHeight == 0) MeasureSpec.getSize(heightMeasureSpec) else mVideoViewHeight
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthSize, heightSize)
}
}
fun resizeVideoView(width: Int, height: Int) {
mVideoViewWidth = width
mVideoViewHeight = height
invalidate()
}
}
五、The End
每一个看似很小的功能,想要实现的完美无缺都不容易,止于纸上谈兵,try it。
最后代码地址再粘贴下:https://github.com/life2smile/PhotoAlbum.git