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

2018-07-21  本文已影响5人  BooQin

视频的列表播放介绍

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

滑动手势处理

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

以上类中,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滑动事件中,还做了以下两件事:

    @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;
    }

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

上一篇下一篇

猜你喜欢

热点阅读