記錄仿抖音的視訊播放並快取預載入視訊的效果實現

語言: 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,這一期就此完結。

「其他文章」