美文网首页
秀品中视频播放模块的解析(上)

秀品中视频播放模块的解析(上)

作者: BooQin | 来源:发表于2018-07-21 20:58 被阅读5次

    视频的列表播放介绍

    在社交类App中,对视频的播放日渐流行,Facebook, Instagram,微博等都已支持在显示列表中直接播放视频。本文将以秀品项目源码来介绍列表播放视频的实现,希望通过该文章可以理解其实现原理以及如何使用该功能模块。
      实现列表中播放视频,主要需要解决一下问题:

    • 用户的手势处理,滑动列表时根据计算更新视频的状态。
    • 视频的状态管理和控制。

    滑动手势处理

    列表的显示实现,使用RecyclerView来完成的,主要的类有:

    • 自定义的RecyclerView,ViewPostRecyclerView
    • 自定义Adapter,AdapterPlayer
    • 自定义ViewHolder,VHolderPlayer
    • 视频和RecyclerView的连接控制类,VideoRecyclerViewAutoControlAttacher

    以上类中,AdapterPlayer只是简单的添加了ViewHolder以及在第一次加载itemView的相应操作,VHolderPlayer对视频相关的View做了封装。滑动手势的响应主要交由VideoRecyclerViewAutoControlAttacher类(以下简称Attacher类)来处理,Attacher类作为RecyclerView和视频类的连接类,控制的视频播放,暂停以及回收的时机,以及对RecyclerView滑动手势的监听和处理来进行最终的决策,由于第一次RecyclerView在加载ItemViews时并不伴随着手势,不能触发视频的播放,所以Adapter提供了一个接口OnFirstHolderBindListener,由Attacher实现:

        @Override
        public void onFirstHolderBind() {
            if (mTimer != null) {
                mTimer.cancel();
                mTimer.purge();
            }
            if (mIsAutoPlayEnabled) {
                mTimer = new Timer();
                mTimer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        //涉及到界面相关更新,通过handler指定延时时间触发播放
                        mViewHandler.obtainMessage().sendToTarget();
                    }
                }, FIRST_CHECK_DELAY);
            }
        }
    

    在视频相关的操作中需要Wifi网络条件下才被允许,网络状态变化后需要更新视频的播放状态,Attraher通过事件总线的方式来进行操作。

    接下来重点看一下手势的监听处理。

    Attracher类通过一个attch方法来持有RecyclerView对象,在该方法中,对成员变量进行了一些初始化,比如确定ItemView的header高,回收操作的大小边界,以及最重要的滑动手势监听的实现:

        public void attach(final RecyclerView loadMoreRecyclerView) {
            if (loadMoreRecyclerView != null && loadMoreRecyclerView.getLayoutManager() != null && loadMoreRecyclerView.getAdapter() != null) {
                ……
                mVideoHeight = ScreenUtil.getScreenWidth(context);
                //ItemView中不只是视频,视频上的宽度即为headerHeight,此处为52dp
                mHeaderHeight = resources.getDimensionPixelSize(R.dimen.post_holder_header_height);
                //上滑回收的最大值,当显示的部分小于该大小时回收,此处为43dp
                mReleaseUpLimit = resources.getDimensionPixelSize(R.dimen.post_holder_action_container_height);
                //下滑回收的最大值
                mReleaseDownLimit = (int) (resources.getDimensionPixelSize(R.dimen.post_holder_header_height) / 3f * 2);
    
                //该类用与滑动方向的判定,在Sroll事件中被调用
                final ScrollDirectionDetector scrollDirectionDetector = new ScrollDirectionDetector(new ScrollDirectionDetector.OnDetectScrollListener() {
                    @Override
                    public void onScrollDirectionChanged(int scrollDirection) {
                        mScrollDirection = scrollDirection;
                    }
                });
    
                //RecyclerView添加滑动事件监听
                loadMoreRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                    @Override
                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                        // 滑动停止时根据当前位置激活视频播放
                        ……
                    }
    
                    @Override
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        // 滑动过程中一旦触发停止播放的条件则立即执行
                        ……
                    }
                });
                return;
            }
            throw new IllegalArgumentException("必须先设置 Adapter 与 LayoutManager 后才能调用 attach()");
        }
    

    scrollDirectionDetector对象会在onScrolled中被调用,即在滑动过程中来更新动作状态,实现方法可以看一下该类下的onDetectedListScroll方法,其原理是根据当前ItemView位置和上一次保存的位置比较,如果相同就获取View的top值进行比较,不同则直接与上一次位置比较确定是上滑还是下,最后保存这次位置和top值:

        public void onDetectedListScroll(RecyclerView recyclerView, int firstVisibleItem) {
            //移到判断内?
            View view = recyclerView.getChildAt(0);
            int top = (view == null) ? 0 : view.getTop();
            //如果第一个可见的Item与上一次一致,就判断其View的Top值,最后确定是上滑还是下滑
            if (firstVisibleItem == mOldFirstVisibleItem) {
                if (top > mOldTop) {
                    onScrollDown(); //更新状态,回调OnDetectScrollListener接口的方法
                } else if (top < mOldTop) {
                    onScrollUp();
                }
            } else {
                //不同的item直接判断上下滑
                if (firstVisibleItem < mOldFirstVisibleItem) {
                    onScrollDown();
                } else {
                    onScrollUp();
                }
            }
     
            mOldTop = top;
            mOldFirstVisibleItem = firstVisibleItem;
        }
    

    在onScrolled滑动事件中,还做了以下两件事:

    • 通过计算获取需要停止播放的videoToStop(ViewHolder)
    • 确定需要播放的ViewHolder所在位置值
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            // 滑动过程中一旦触发停止播放的条件则立即执行
            // mRecyclerViewHeaderCount = loadMoreRecyclerView.getHeaderCount();
            mRecyclerViewHeaderCount = 0;
    
            //获取第一个和最后一个可见Item的位置值,不包含头部
            int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition() - mRecyclerViewHeaderCount;
            int lastVisiblePos = mLayoutManager.findLastVisibleItemPosition() - mRecyclerViewHeaderCount;
            //检测滑动方向,更新mScrollDirection
            scrollDirectionDetector.onDetectedListScroll(recyclerView, firstVisiblePos);
            if (mScrollDirection == ScrollDirectionDetector.UP) {
                //根据位置获取要停止的ViewHolder
                VHolderPlayer videoToStop = findDeactivateHolderWhenUp(firstVisiblePos);
                if (videoToStop != null) {
                    //停止
                    videoToStop.deactivate();
                    PlayerApp.getAppInstance().getPlayerPool().removeCompleteNoAutoPlay(videoToStop.getPlayerKey());
                    //如果ViewHolder位置满足回收的大小,直接释放资源
                    if (videoToStop.itemView.getHeight() - mRect4UpDeactivate.top < mReleaseUpLimit) {
                        //释放资源
                        videoToStop.release();
                    }
                }
                //获取需要播放的ViewHolder位置
                int posToActivate = findActivateHolderWhenUp(lastVisiblePos);
                if (posToActivate != -1) {
                    //更新成员变量,在停止滑动时根据该值播放视频
                    mPositionToActivate = posToActivate;
                }
            } else if (mScrollDirection == ScrollDirectionDetector.DOWN) {
                VHolderPlayer videoToStop = findDeactivateHolderWhenDown(lastVisiblePos);
                if (videoToStop != null) {
                    videoToStop.deactivate();
                    PlayerApp.getAppInstance().getPlayerPool().removeCompleteNoAutoPlay(videoToStop.getPlayerKey());
                    if (mRect4DownDeactivate.bottom < mReleaseDownLimit) {
                        videoToStop.release();
                    }
                }
                int posToActivate = findActivateHolderWhenDown(firstVisiblePos);
                if (posToActivate != -1) {
                    mPositionToActivate = posToActivate;
                }
            }
        }
    

    那么如何获取需要停止的ViewHolder以及要播放的位置呢?其两者的思路是一致的,大致的原理是通过判断ItemView中的视频区域(TextureView)的可见范围高度大小和其滑动的分享来做处理的。最大的问题是如何获取可见区域,幸运的是Android中View类提供了一个getLocalVisibleRect(Rect r)方法,此方法可以通过传入一个Rect对象,来获取当前显示区域的宽高大小,得到的四个点lift,top,right,bottom的值是以该view的左上角为参考点的值,可以通过下图进行理解。

    rect

    关于实际的操作,可以通过下图来理解整个RecyclerView在滑动过程中的计算操作:


    recyclerview-video

    以上滑为例,在onScrolled中会执行获取停止ViewHodler的findDeactivateHolderWhenUp方法和获取需要播放位置的findActivateHolderWhenUp方法,其两者的方法步骤类似,通过getLocalVisiable方法获取显示的区域,然后计算得到TextureView的区域与一个设定好的基准值做比较(图中的x和y值),在该代码中,基准值为屏幕宽度的三分之一,当被隐藏的TextureView超过该值,进行播放或暂停。代码如下:

        /**
         * 列表向上移动时 - 尝试找出需要停止播放的 ViewHolder
         *
         * @param firstVisiblePos 列表首个可见的 item 的位置
         * @return 需要停止播放的 ViewHolder,如果没有找到返回 null
         */
        private VHolderPlayer findDeactivateHolderWhenUp(int firstVisiblePos) {
            //获取当前ViewHolder的位置
            int currentCalcPos = firstVisiblePos + mRecyclerViewHeaderCount;
            if (currentCalcPos > mRecyclerViewHeaderCount - 1) {
                //在RecyclerView中获取ViewHolder
                RecyclerView.ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(currentCalcPos);
                if (viewHolder instanceof VHolderPlayer) {
                    VHolderPlayer currentVideoHolder = (VHolderPlayer) viewHolder;
                    //获取显示View的Rect,即该View显示相对于自己完整的Rect的位置区域
                    currentVideoHolder.itemView.getLocalVisibleRect(mRect4UpDeactivate);
                    //TextureView区域是否大于基准值
                    if (mRect4UpDeactivate.top - mHeaderHeight > mVideoHeight / 3f) {
                        return currentVideoHolder;
                    }
                }
            }
            return null;
        }
     
        private int findActivateHolderWhenUp(int lastVisiblePos) {
            int currentCalcPos = lastVisiblePos + mRecyclerViewHeaderCount;
            RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(currentCalcPos);
            if (holder != null && holder instanceof VHolderPlayer) {
                VHolderPlayer currentVideoHolder = (VHolderPlayer) holder;
                currentVideoHolder.itemView.getLocalVisibleRect(mRect4UpActivate);
                if (mRect4UpActivate.bottom - mHeaderHeight > mVideoHeight / 3f) {
                    return currentCalcPos;
                }
            }
            return -1;
        }
    

    而onScrollStateChanged就简单的多了,该方法会在滑动结束的时候触发,通过获取在onScrolled里得到位置mPositionToActivate,从RecyclerView中读取需要更新的ViewHolder,并开始播放,更新当前活动状态的key。代码如下:

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            // 滑动停止时根据当前位置激活视频播放
            if (newState != mScrollState && newState == RecyclerView.SCROLL_STATE_IDLE && mPositionToActivate >= 0) {
                //获取要播放的ViewHolder
                RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(mPositionToActivate);
                if (holder != null && holder instanceof VHolderPlayer) {
                    if (mIsAutoPlayEnabled) {
                        VHolderPlayer video = (VHolderPlayer) holder;
                        if (video.isNeedPlayNow()) {
                            //如果需要自动播放,开始播放
                            video.activate();
                            //更新key
                            mCurrentActiveKey = video.getPlayerKey();
                        }
                    }
                    //重置位置
                    mPositionToActivate = -1;
                }
            }
            //保存状态
            mScrollState = newState;
        }
    

    以上为整个滑动事件的解析。

    相关文章

      网友评论

          本文标题:秀品中视频播放模块的解析(上)

          本文链接:https://www.haomeiwen.com/subject/zurkmftx.html