记录仿抖音的视频播放并缓存预加载视频的效果实现

语言: CN / TW / HK

theme: smartblue highlight: agate


持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

前言

之前的文章我们讲到了WX盆友圈动态列表的效果,九宫格控件的实现【传送门】。 并且讲到了发布动态中话题的处理【传送门】。 已经在动态列表中展示超长的文本与特殊文办的处理【传送门】

可以看到其实我们的动态列表并不是九宫格和一些图片文本的信息流展示,我们还包含和和WX盆友圈一样的视频功能。其中有一个快捷入口可以播放视频列表,类似抖音的效果。

device-2022-10-17-135840 00_00_00-00_00_30.gif

相信类似的效果大家都看的多了,网上也有很多的Demo了,这里记录一下我的想法和实现思路,仅供大家参考。

实现的几种思路

其实这种上下切换视频的效果,市面上大致分为几种思路:

1. 直接使用RecyclerView + SpanHelper。

Activity的布局就是一个RecyclerView, Adapter内部的Item的布局就是一个封面图和文本之类的,都没有播放器。

播放器是在 Activity 单独实例出来的一个,然后添加到Item的布局中,每次滚动到位置之后先尝试移除 VideoView 然后再加载VideoView。保证页面只有只有一个VideoView从而保证播放效果和内存优化。

核心伪代码如下: ```java

private void initRecyclerView() { mRecyclerView = findViewById(R.id.rv);

    mTikTokAdapter = new TikTokAdapter(mVideoList);
    ViewPagerLayoutManager layoutManager = new ViewPagerLayoutManager(this, OrientationHelper.VERTICAL);

    mRecyclerView.setLayoutManager(layoutManager);
    mRecyclerView.setAdapter(mTikTokAdapter);
    layoutManager.setOnViewPagerListener(new OnViewPagerListener() {
        @Override
        public void onInitComplete() {
            //自动播放第index条
            startPlay(mIndex);
        }

        @Override
        public void onPageRelease(boolean isNext, int position) {
            if (mCurPos == position) {
                mVideoView.release();
            }
        }

        @Override
        public void onPageSelected(int position, boolean isBottom) {
            if (mCurPos == position) return;
            startPlay(position);
        }
    });
}

private void startPlay(int position) {
    View itemView = mRecyclerView.getChildAt(0);
    TikTokAdapter.VideoHolder viewHolder = (TikTokAdapter.VideoHolder) itemView.getTag();
    mVideoView.release();
    Utils.removeViewFormParent(mVideoView);
    TiktokBean item = mVideoList.get(position);
    String playUrl = PreloadManager.getInstance(this).getPlayUrl(item.videoDownloadUrl);
    L.i("startPlay: " + "position: " + position + "  url: " + playUrl);
    mVideoView.setUrl(playUrl);
    mController.addControlComponent(viewHolder.mTikTokView, true);
    viewHolder.mPlayerContainer.addView(mVideoView, 0);
    mVideoView.start();
    mCurPos = position;
}

```

2. 自定义垂直的 ViewPager

github上面很多垂直的ViewPager自定义类,类似如下

```java public class VerticalViewPagerAdapter extends PagerAdapter { private FragmentManager fragmentManager; private FragmentTransaction mCurTransaction; private Fragment mCurrentPrimaryItem = null; private List urlList;

public void setUrlList(List<String> urlList) {
    this.urlList = urlList;
}


public VerticalViewPagerAdapter(FragmentManager fm) {
    this.fragmentManager = fm;
}

@Override
public int getCount() {
    return Integer.MAX_VALUE;
}

@Override
public Object instantiateItem(ViewGroup container, int position) {

    if (mCurTransaction == null) {
        mCurTransaction = fragmentManager.beginTransaction();
    }

    VideoFragment fragment = new VideoFragment();
    if (urlList != null && urlList.size() > 0) {
        Bundle bundle = new Bundle();
        if (position >= urlList.size()) {
            bundle.putString(VideoFragment.URL, urlList.get(position % urlList.size()));
        } else {
            bundle.putString(VideoFragment.URL, urlList.get(position));
        }
        fragment.setArguments(bundle);
    }


    mCurTransaction.add(container.getId(), fragment,
            makeFragmentName(container.getId(), position));
    fragment.setUserVisibleHint(false);

    return fragment;
}


@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = fragmentManager.beginTransaction();
    }
    mCurTransaction.detach((Fragment) object);
    mCurTransaction.remove((Fragment) object);
}

@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
    return ((Fragment) object).getView() == view;
}

private String makeFragmentName(int viewId, int position) {
    return "android:switcher:" + viewId + position;
}

@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment) object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);
        }
        if (fragment != null) {
            fragment.setMenuVisibility(true);
            fragment.setUserVisibleHint(true);
        }
        mCurrentPrimaryItem = fragment;
    }
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitNowAllowingStateLoss();
        mCurTransaction = null;
    }
}

} ```

使用的方式和RV的方式类似,只是如果用ViewPager的话,可以设置预加载的数量,这里设置的是前后4个,也是通过一个VideoView先移除,然后再添加到Item布局里面。

核心伪代码如下: ```java private void initVideoView() { mVideoView = new VideoView(this); mVideoView.setLooping(true);

    mVideoView.setScreenScaleType(VideoView.SCREEN_SCALE_CENTER_CROP);

    mController = new TikTokController(this);
    mVideoView.setVideoController(mController);
}

private void initViewPager() {
    mViewPager = findViewById(R.id.vvp);
    mViewPager.setOffscreenPageLimit(4);
    mTiktok2Adapter = new Tiktok2Adapter(mVideoList);
    mViewPager.setAdapter(mTiktok2Adapter);
    mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);

    mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {

        private int mCurItem;

        private boolean mIsReverseScroll;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels);
            if (position == mCurItem) {
                return;
            }
            mIsReverseScroll = position < mCurItem;
        }

        @Override
        public void onPageSelected(int position) {
            super.onPageSelected(position);
            if (position == mCurPos) return;
            startPlay(position);
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            super.onPageScrollStateChanged(state);
            if (state == VerticalViewPager.SCROLL_STATE_DRAGGING) {
                mCurItem = mViewPager.getCurrentItem();
            }

            if (state == VerticalViewPager.SCROLL_STATE_IDLE) {
                mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
            } else {
                mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
            }
        }
    });
}

private void startPlay(int position) {
    int count = mViewPager.getChildCount();
    for (int i = 0; i < count; i ++) {
        View itemView = mViewPager.getChildAt(i);
        Tiktok2Adapter.ViewHolder viewHolder = (Tiktok2Adapter.ViewHolder) itemView.getTag();
        if (viewHolder.mPosition == position) {
            mVideoView.release();
            Utils.removeViewFormParent(mVideoView);

            TiktokBean tiktokBean = mVideoList.get(position);
            String playUrl = mPreloadManager.getPlayUrl(tiktokBean.videoDownloadUrl);
            L.i("startPlay: " + "position: " + position + "  url: " + playUrl);
            mVideoView.setUrl(playUrl);
            mController.addControlComponent(viewHolder.mTikTokView, true);
            viewHolder.mPlayerContainer.addView(mVideoView, 0);
            mVideoView.start();
            mCurPos = position;
            break;
        }
    }
}

```

由于内部并非是RV实现,所以关于自定义的缓存池我们需要额外的处理一下,特别是如果加入了视频缓存方案。

```java public class TiktokAdapter extends PagerAdapter {

/**
 * View缓存池,从ViewPager中移除的item将会存到这里面,用来复用
 */
private List<View> mViewPool = new ArrayList<>();

private List<TiktokBean> mVideoBeans;

public TiktokAdapter(List<TiktokBean> videoBeans) {
    this.mVideoBeans = videoBeans;
}

@Override
public int getCount() {
    return mVideoBeans == null ? 0 : mVideoBeans.size();
}

@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
    return view == o;
}

@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    Context context = container.getContext();
    View view = null;
    if (mViewPool.size() > 0) {//取第一个进行复用
        view = mViewPool.get(0);
        mViewPool.remove(0);
    }

    ViewHolder viewHolder;
    if (view == null) {
        view = LayoutInflater.from(context).inflate(R.layout.item_tik_tok, container, false);
        viewHolder = new ViewHolder(view);
    } else {
        viewHolder = (ViewHolder) view.getTag();
    }

    TiktokBean item = mVideoBeans.get(position);
    //开始预加载
    PreloadManager.getInstance(context).addPreloadTask(item.videoDownloadUrl, position);

    Glide.with(context)
            .load(item.coverImgUrl)
            .placeholder(android.R.color.white)
            .into(viewHolder.mThumb);
    viewHolder.mTitle.setText(item.title);
    viewHolder.mTitle.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(context, "点击了标题", Toast.LENGTH_SHORT).show();
        }
    });
    viewHolder.mPosition = position;
    container.addView(view);
    return view;
}

@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    View itemView = (View) object;
    container.removeView(itemView);
    TiktokBean item = mVideoBeans.get(position);
    //取消预加载
    PreloadManager.getInstance(container.getContext()).removePreloadTask(item.videoDownloadUrl);
    //保存起来用来复用
    mViewPool.add(itemView);
}


public static class ViewHolder {

    public int mPosition;
    public TextView mTitle;//标题
    public ImageView mThumb;//封面图
    public TikTokView mTikTokView;
    public FrameLayout mPlayerContainer;

    ViewHolder(View itemView) {
        mTikTokView = itemView.findViewById(R.id.tiktok_View);
        mTitle = mTikTokView.findViewById(R.id.tv_title);
        mThumb = mTikTokView.findViewById(R.id.iv_thumb);
        mPlayerContainer = itemView.findViewById(R.id.container);
        itemView.setTag(this);
    }
}

} ```

3. ViewPagr2

既然我们可以用自定义的垂直ViewPager来实现,那么天然支持垂直滚动的ViewPager2当然可能实现了。

ViewPager2的机制是基于RV实现的,虽然使用方式和ViewPager的方式差不多,但是不需要我们自己实现缓存和复用了。

核心的伪代码如下: ```java private void initVideoView() { mVideoView = new VideoView(this); mVideoView.setLooping(true);

    mVideoView.setScreenScaleType(VideoView.SCREEN_SCALE_CENTER_CROP);

    mController = new TikTokController(this);
    mVideoView.setVideoController(mController);
}

private void initViewPager() {
    mViewPager = findViewById(R.id.vp2);
    mViewPager.setOffscreenPageLimit(4);
    mTiktok3Adapter = new Tiktok3Adapter(mVideoList);
    mViewPager.setAdapter(mTiktok3Adapter);
    mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
    mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {

        private int mCurItem;

        private boolean mIsReverseScroll;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels);
            if (position == mCurItem) {
                return;
            }
            mIsReverseScroll = position < mCurItem;
        }

        @Override
        public void onPageSelected(int position) {
            super.onPageSelected(position);
            if (position == mCurPos) return;
            mViewPager.post(new Runnable() {
                @Override
                public void run() {
                    startPlay(position);
                }
            });
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            super.onPageScrollStateChanged(state);
            if (state == VerticalViewPager.SCROLL_STATE_DRAGGING) {
                mCurItem = mViewPager.getCurrentItem();
            }
            if (state == ViewPager2.SCROLL_STATE_IDLE) {
                mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
            } else {
                mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
            }
        }
    });

    mViewPagerImpl = (RecyclerView) mViewPager.getChildAt(0);
}

private void startPlay(int position) {
    int count = mViewPagerImpl.getChildCount();
    for (int i = 0; i < count; i++) {
        View itemView = mViewPagerImpl.getChildAt(i);
        Tiktok3Adapter.ViewHolder viewHolder = (Tiktok3Adapter.ViewHolder) itemView.getTag();
        if (viewHolder.mPosition == position) {
            mVideoView.release();
            Utils.removeViewFormParent(mVideoView);
            TiktokBean tiktokBean = mVideoList.get(position);
            String playUrl = mPreloadManager.getPlayUrl(tiktokBean.videoDownloadUrl);
            L.i("startPlay: " + "position: " + position + "  url: " + playUrl);
            mVideoView.setUrl(playUrl);
            mController.addControlComponent(viewHolder.mTikTokView, true);
            viewHolder.mPlayerContainer.addView(mVideoView, 0);
            mVideoView.start();
            mCurPos = position;
            break;
        }
    }
}

```

其实三种方案各有利弊,都可以实现同样的效果,我使用的哪一种方案?

我使用的是ViewPager2方案,为什么?

ViewPager在部分设备上会出现滑动不跟手的情况,并且不方便实现RV那样加载的效果和预加载的效果。

而使用RV的话,虽然是可以像普通列表一样来实现这个视频滑动效果,但是感觉预加载的逻辑不太好精准的控制,只能在 onBindViewHolder 中开启预加载,在onViewDetachedFromWindow 中移除预加载。

如果是使用ViewPager2的话,由于内部也是RV实现,缓存什么的也不需要我们管,也能通过RecyclerView.Adapter中实现LoadMore的功能,实现预加载数据的逻辑,还能通过 setOffscreenPageLimit 来精准控制视频预加载的数量。

所以我选择的是ViewPager2方案。

加入视频缓存

视频的预加载与缓存效果,估计大家都是使用的开源方案 VideoCache 这种方案了。

它通过创建一个设备的本地代理服务 CacheService,在将视频资源的 url 交给播放器之前,先进行本地的一次转换,并将初始的url作为参数,拼接在本地代理的url上。如:http://127.0.0.1:8090/https://1234.mp4

当我们把到新的 url 并交给任意播放器后,播放器的加载都指向本地服务的新地址——即通过 Socket 连接建立的本地服务 CacheService,后者通过解析出请求中真正的 https://1234.mp4 地址,创建对应的下载任务,并从下载的文件缓存中,读取 buffer 返回给播放器。

我们无需关系视频文件是否已经下载,当文件已经下载,此时直接读取本地文件,将数据通过Socket不断传回给播放器。当文件没有下载,此时会新建一个本地文件,并开启远程下载任务,下载过程中,数据流不断涌入本地文件,本地文件大小、下载进度的变更都会响应式通知上层;除此之外,新的音视频流数据会通过Socket不断传回给播放器。

具体应用在我们方案中,就需要在ViewPager2设置 setOffscreenPageLimit 之后,比如预加载4个Item,那么我们就需要在 Adapter 的 onBindViewHolder 加入预加载资源,在 onViewDetachedFromWindow 移除预加载。

```java public class TiktokAdapter extends RecyclerView.Adapter {

/**
 * 数据源
 */
private List<TiktokBean> mVideoBeans;

public TiktokAdapter(List<TiktokBean> videoBeans) {
    this.mVideoBeans = videoBeans;
}

@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tik_tok, parent, false);
    return new ViewHolder(itemView);
}

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    Context context = holder.itemView.getContext();
    TiktokBean item = mVideoBeans.get(position);
    //开始预加载
    PreloadManager.getInstance(context).addPreloadTask(item.videoDownloadUrl, position);
    Glide.with(context)
            .load(item.coverImgUrl)
            .placeholder(android.R.color.white)
            .into(holder.mThumb);
    holder.mTitle.setText(item.title);
    holder.mPosition = position;
}

@Override
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
    super.onViewDetachedFromWindow(holder);
    TiktokBean item = mVideoBeans.get(holder.mPosition);
    //取消预加载
    PreloadManager.getInstance(holder.itemView.getContext()).removePreloadTask(item.videoDownloadUrl);
}

@Override
public int getItemCount() {
    return mVideoBeans != null ? mVideoBeans.size() : 0;
}

public static class ViewHolder extends RecyclerView.ViewHolder {

    public int mPosition;
    public TextView mTitle;//标题
    public ImageView mThumb;//封面图
    public TikTokView mTikTokView;
    public FrameLayout mPlayerContainer;

    ViewHolder(View itemView) {
        super(itemView);
        mTikTokView = itemView.findViewById(R.id.tiktok_View);
        mTitle = mTikTokView.findViewById(R.id.tv_title);
        mThumb = mTikTokView.findViewById(R.id.iv_thumb);
        mPlayerContainer = itemView.findViewById(R.id.container);
        itemView.setTag(this);
    }
}

} ```

VideoCache的问题与优化思路:

1.不支持直播流 2.不支持Seek 3.不支持优先级

比如5分钟的视频,我们缓存了前1分钟,此时Seek到第3分钟,那么此时还是会当场远程加载视频并不会写入缓存,下次再滑回这个视频,还是只缓存了1分钟的视频,再Seek到第3分钟面还是会当场远程加载视频数据。

主要是Seek对视频进行切片,视频碎片的问题,如果要解决还要考虑视频的拼接,直播流也是同样的逻辑。如果考虑这个就比较复杂。

性我们的系统限制是只能发MP4格式,并且我们的系统是不支持1分钟以上长视频,所以我们的视频都是不支持Seek的。就没有考虑这些问题。

而预加载的优先级,由于我们设置的是 setOffscreenPageLimit(4) ,预加载4个视频文件,此时按照道理应该是优先加载下一个视频,而不是第二个第三个视频。

不知道大家有没有更好的方案呢?

具体方案实现

这里我们ViewPager的Adapter使用的是BRVAH的方案,视频播放UI与控制使用的是 JzvdStd 。视频播放内核使用的是 ExoPlayer 。

初始化ViewPager2

```java /* * ViewPager2和内部的RV初始化和监听 / private void initPager() { mViewPager.setOffscreenPageLimit(4); mViewPager.setAdapter(mPresenter.mTiktokAdapter); mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);

    mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {

        private int mCurItem;

        private boolean mIsReverseScroll;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            super.onPageScrolled(position, positionOffset, positionOffsetPixels);
            if (position == mCurItem) {
                return;
            }
            mIsReverseScroll = position < mCurItem;
        }

        @Override
        public void onPageSelected(int position) {
            super.onPageSelected(position);
            if (position == mCurPos) return;

            mViewPager.post(() -> startPlay(position));
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            super.onPageScrollStateChanged(state);
            if (state == ViewPager2.SCROLL_STATE_DRAGGING) {
                mCurItem = mViewPager.getCurrentItem();
            }
            if (state == ViewPager2.SCROLL_STATE_IDLE) {
                mPreloadManager.resumePreload(mCurPos, mIsReverseScroll);
            } else {
                mPreloadManager.pausePreload(mCurPos, mIsReverseScroll);
            }
        }
    });

    //ViewPage2内部是通过RecyclerView去实现的,它位于ViewPager2的第0个位置
    mViewPagerImpl = (RecyclerView) mViewPager.getChildAt(0);

}

```

加载更多,以及Item的点击监听: ```java //内部Adapter的滑动监听,监听加载更多 mPresenter.mTiktokAdapter.setEnableLoadMore(false); mPresenter.mTiktokAdapter.setPreLoadNumber(2); mPresenter.mTiktokAdapter.setOnLoadMoreListener(() -> {

        //调用接口获取预加载的数据
        mPresenter.getNextVideos();

    }, mViewPagerImpl);


    //Item的点击事件
    mPresenter.mTiktokAdapter.setOnItemChildClickListener((adapter, view, position) -> {

    // 。。。
    });

}

//滑动之后开始播放视频的逻辑
private void startPlay(int position) {

    int count = mViewPagerImpl.getChildCount();
    for (int i = 0; i < count; i++) {
        //直接像TikTokActivity那样直接获取到索引的Tag拿到ViewHolder直接调用,这样可以省去遍历
        View itemView = mViewPagerImpl.getChildAt(i);
        TiktokAdapter.TiktokViewHolder viewHolder = (TiktokAdapter.TiktokViewHolder) itemView.getTag();
        if (viewHolder.mPosition == position) {
            Jzvd.releaseAllVideos();

            NewsFeedAdatperBean item = mPresenter.mDatas.get(position);
            ImageInfo videoUrlInfo = item.myNewsResources.get(1);

            mCurPlayVideoView = viewHolder.mVideoView;
            //使用预加载的缓存路径
            String proxyUrl = mPreloadManager.getPlayUrl(videoUrlInfo.bigImageUrl);
            YYLogUtils.w("视频播放proxyUrl:" + proxyUrl);
            mCurPlayVideoView.setUp(proxyUrl, "", Jzvd.SCREEN_NORMAL, JZMediaExo.class);

            //自定义的播放方法-强制性指定播放的裁剪模式为默认的模式
            mCurPlayVideoView.startVideo();

            YYLogUtils.w("视频播放:" + "startVideo");
            mCurPos = position;

            //长按事件-下载视频
            viewHolder.mVideoView.textureViewContainer.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {

                    showSavePopup(videoUrlInfo.bigImageUrl);
                    return false;
                }
            });

            break;
        }
    }
}

```

Adapter中由于是BRVAH实现的,反倒是简单一点,需要注意的是长视频与宽视频的裁剪。

```java public class TiktokAdapter extends BaseQuickAdapter {

private String mMyMemberId = "";
private final int mScreenHeight;
private final int mScreenWidth;

public TiktokAdapter(@Nullable List<NewsFeedAdatperBean> data) {
    super(R.layout.item_tiktok, data);

    mScreenHeight = ScreenUtils.getScreenHeight(CommUtils.getContext());
    mScreenWidth = ScreenUtils.getScreenWidth(CommUtils.getContext());

}

@Override
protected void convert(TiktokViewHolder helper, NewsFeedAdatperBean item) {
    ImageInfo thumImgInfo = item.myNewsResources.get(0);
    ImageInfo videoUrlInfo = item.myNewsResources.get(1);

    //开始预加载
    VideoCachePreloadManager.getInstance(mContext).addPreloadTask(videoUrlInfo.bigImageUrl, helper.getAdapterPosition());

    //设置封面
    int videoWidth = videoUrlInfo.getImageViewWidth();
    int videoHeight = videoUrlInfo.getImageViewHeight();

    //判断是横视频还是竖视频,根据宽高比例切换裁剪模式
    if (videoWidth >= videoHeight) {
        //横视频,宽度填满,高度按比例计算
        helper.mVideoView.thumbImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
    } else {
        //竖视频
        float videoRatio = (float) videoWidth / (float) videoHeight;
        float parentRatio = (float) mScreenWidth / (float) mScreenHeight;
        if (videoRatio > parentRatio) {
            //无需裁剪
            helper.mVideoView.thumbImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
        } else {
            //视频太高了-裁剪
            helper.mVideoView.thumbImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
        }
    }

    GlideImageEngine.get().imageLoad(helper.mVideoView.thumbImageView, thumImgInfo.thumbnailUrl);

    helper.mPosition = helper.getAdapterPosition();

    helper.addOnClickListener(R.id.iv_publisher_avatar, R.id.iv_member_follow, R.id.ll_likes_box, R.id.tv_comment_num, R.id.tv_share_num);

}


@Override
public void onViewDetachedFromWindow(@NonNull TiktokViewHolder holder) {
    super.onViewDetachedFromWindow(holder);
    NewsFeedAdatperBean item = mData.get(holder.mPosition);
    ImageInfo videoUrlInfo = item.myNewsResources.get(1);

    //取消预加载
    VideoCachePreloadManager.getInstance(mContext).removePreloadTask(videoUrlInfo.bigImageUrl);
}

public static class TiktokViewHolder extends BaseViewHolder {

    public int mPosition;
    public DouYinVideoView mVideoView;

    public TiktokViewHolder(View view) {
        super(view);

        view.setTag(this);

        mVideoView = view.findViewById(R.id.jz_video);
    }
}

} ```

结语

其实三种方案市面上都有实现的应用上线,都是可以做的,我个人只是觉得RV比较方便所以选择的ViewPager2,其实内部的控制都是基于现找到了RV再进行的操作。

如果后期扩展需要像抖音一样左右滑动进入详情页面,我们也可以使用嵌套ViewPager实现,或者DrawerLayout实现,又或者完全自定义View实现,都是可以的。

关于ViewPager2嵌套ViewPager的问题,之前的文章有讲到过【传送门】。由于我们的需求并没有做左右滑动进行详情的逻辑,所以我也并没有进行尝试。如果有坑欢迎小伙伴分享一下哦。

由于我们做的只是简单的版本,具体效果示例在文章开头已经贴出,如果你有更好的方案,或者优化的空间都也可以一起交流一下。如有错漏的地方还请指出,如果有疑问也可以在评论区大家一起讨论哦。

如果感觉本文对你有一点点的启发,还望你能 点赞 支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

「其他文章」