IM 使用的图片视频加载预览

793 阅读4分钟
原文链接: xiaozhuanlan.com

现在很多直播软件都具备无限循环加载的功能还有就是群聊图片预览,这节就分析一下,这种功能的实现。

1.ViewPager的制作无限循环
2.RecylerView滑动,使它可以定在其中的完整的一格
3.自定义View和Layout达到滑动效果。

先给大家看看ViewPager的做法吧。其至少需要4个View来完成对象复用

ViewPager复用.png

ViewPager复用.png

private fun initView() {
        // 初始化复用的预览界面,4个足够满足滑动调度
        for (i in 0..3) {
            val previewView = MediaPreviewView(this)
            previewView.tag = i
            previewViews.add(previewView)
        }
       //自定义pagerAdapter
        mediaview_viewpager.adapter = object : PagerAdapter() {
            override fun instantiateItem(container: ViewGroup, position: Int): Any {
                Log.i(TAG, "View index = ${(position) % 4}, position = $position, current position = $currentPosition")
                val message = mediaItems[position]
                val previewView = previewViews[(position) %4]  //获取复用的view
                // 设置媒体数据
                previewView.setMediaItem(message)
                if (previewView.parent == null) {  //添加View
                    container.addView(previewView)
                }
                return previewView
            }
            //清空View中的内容,以便复用
            override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
                val previewView = previewViews[(position) % 5]
                previewView.clear()
            }

            override fun isViewFromObject(view: View, `object`: Any): Boolean {
                return view == `object`
            }
            //获取adapter的数据量
            override fun getCount() = mediaItems.size

            override fun getItemPosition(`object`: Any): Int {
                return if ((`object` as View).tag as Int == (currentPosition) % 5) {
                    POSITION_UNCHANGED
                } else {
                    POSITION_NONE
                }
            }
        }
        //添加滑动监听
        mediaview_viewpager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {}

            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}

            override fun onPageSelected(position: Int) {
                if (position != currentPosition) {
                    currentPosition = position
                    lastIndexId = mediaItems[position].indexId
                    if (position == 0) { //到第一个向前请求更多数据
                        getForwardMessages()
                    }
                    if (position == mediaItems.lastIndex) {  //到末尾向后请求更多数据
                        getBackwardMessages()
                    }
                }
            }
        })
    }

这里数据变更的时候,ViewPager会强制要求你刷新界面。如果你滑动的时候在网络获取数据地址,ViewPager强制刷新,你就发现突然黑屏了一下。。。这体验,真的非常差的。。。
Viewpager是没提供局部刷新的方法的,所以这个bug也无法修复,除非你重写整个Viewpager了,但是这样也不利于后续优化维护。
如果滑动是循环且数量是确定的,可以采取这种方案,但是需要动态更新数据,就不能采取这种方案了,体验很差。
直播滑动和在线图片轮播是需要动态加载,所以此方案不适用。

2.使用RecylerView的卡位的方案,这里非常简单,只需要PagerSnapHelper直接关联RecylerView,ReyclerView会直接完成View复用。而RecylerView拥有局部刷新的接口功能,所以不会出现闪屏的功能。

private fun initView() {
        mediaview_recycler.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
        mediaview_recycler.adapter = MediaViewAdapter()
        mediaview_recycler.isNestedScrollingEnabled = false
        //这里简单使用PagerSanpHepler关联到RecylverView就可以了
        PagerSnapHelper().apply {
            attachToRecyclerView(mediaview_recycler)
        }
    }

局部向前刷新

private fun getForwardMessages() {
          mediaItems.addAll(0, it)
          mediaview_recycler.adapter.notifyItemRangeInserted(0, it.size)
          mediaview_recycler.adapter.notifyItemRangeChanged(0, mediaItems.size)

    }

局部向后刷新

private fun getBackwardMessages() {
         if (it.isNotEmpty()) {
                 mediaItems.addAll(it)
                 mediaview_recycler.adapter.notifyItemInserted(mediaItems.size - it.size)
         }
    }

SnapHelper还有一个居中的效果的LinearSnapHelper
这里如果还需要判断视频播放时机,需要复写LayoutManager的方法。可以参考下面的文章
Android中模仿抖音的滑动RecycleView的实现

有这个PageRecylerView的开源库,但是反射了使用了RecylerView的mViewFlinger对象,如果上线google play是无法使用的。
github.com/ckrgithub/P…

3.自定义layout
这个开源是大神五年前写的,感觉就是ViewPager的原版。数据变更也需要强刷整个页面,所以会有闪屏的问题,需要改写刷新页面的方式为局部刷新。
github.com/castorflex/…

4.如果播放器存在进度条,那么recylerView就需要划分一个区域,触摸事件向下传递,复写拦截事件。

var canSmoothDownSide = false //true表示可以滑动,false表示不可以
    override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
        if (e?.action == MotionEvent.ACTION_MOVE && !canSmoothDownSide) {
            if (e.rawY > ScreenUtils.getScreenHeight(context) - ScreenUtils.dp2Px(context, 80f)) {
                return false
            }
        }
        return super.onInterceptTouchEvent(e)
    }

5.使用Exoplayer播放,ExoPlayerView,播放(true)和暂停(false)都是使用setPlayWhenReady,如果你使用了stopVideo来暂停,当recylerView滑动后再划回来会黑屏。一个ExoPlayerView,无法设置两次prepare MediaSource作为替换,不然会显示AudioFlinger could not create Task。

6.Exoplayer,如何视频太短(1~3秒)恢复过来会有黑屏的问题。视频有时候也会因为recylerView切换后无法卡在停止的那帧,无法播放。解决的方法是使用RecylerView onChildViewAttachedToWindow的时候,重新设置进度播放进度。

下面是需要注意的一些问题
1.Exoplayer在RecylerView不播放状态来回切换会发现,每次切换回来会发现视频快进回来的问题
2.Exoplayer在播放到最后,获取播放进度,会发现当前的播放位置,会比视频长度要长,所以判断的时候,需要判定大于等于视频长度而不是等于视频长度