記錄仿抖音的視頻播放並緩存預加載視頻的效果實現

語言: 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/http://1234.mp4

當我們把到新的 url 並交給任意播放器後,播放器的加載都指向本地服務的新地址——即通過 Socket 連接建立的本地服務 CacheService,後者通過解析出請求中真正的 http://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,這一期就此完結。

「其他文章」