Realice el efecto de grabar la reproducción de video similar al vibrato y almacenar en caché el video precargado

¡Continúe creando y acelerando el crecimiento! Este es el día 12 de mi participación en el "Desafío de actualización de octubre del nuevo plan diario de Nuggets", haga clic para ver los detalles del evento

prefacio

En el artículo anterior, hablamos sobre el efecto de la lista dinámica del círculo de amigos de WX y la realización del [Portal] de control de Jiugongge . Y habló sobre el manejo de temas en las noticias de lanzamiento [Portal] . El procesamiento de texto superlargo y oficina de documentos especiales [Portal] se ha mostrado en la lista dinámica .

Se puede ver que nuestra lista dinámica no es una pantalla de flujo de información de Jiugongge y algunas imágenes y textos. También incluimos la misma función de video que WX Moments. Hay una entrada de acceso directo para reproducir la lista de videos, que es similar al efecto de Douyin.

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

Creo que todos han visto muchos efectos similares, y hay muchas demostraciones en Internet. Aquí están mis pensamientos e ideas de implementación, solo para su referencia.

Varias ideas para la realización.

De hecho, el efecto de subir y bajar videos se divide aproximadamente en varias ideas en el mercado:

1. Use RecyclerView + SpanHelper directamente.

El diseño de la Actividad es RecyclerView, y el diseño del Elemento dentro del Adaptador es una imagen de portada y texto, y no hay reproductor.

El reproductor es una instancia separada de la actividad y luego se agrega al diseño del elemento. Cada vez que se desplaza a la posición, primero intenta eliminar VideoView y luego cargar VideoView. Asegúrese de que solo haya un VideoView en la página para garantizar el efecto de reproducción y la optimización de la memoria.

El pseudocódigo central es el siguiente:


   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. Personaliza ViewPager vertical

Hay muchas clases personalizadas verticales de ViewPager en github, similares a las siguientes

public class VerticalViewPagerAdapter extends PagerAdapter {
    private FragmentManager fragmentManager;
    private FragmentTransaction mCurTransaction;
    private Fragment mCurrentPrimaryItem = null;
    private List<String> 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;
        }
    }
}
复制代码

El método de uso es similar al de RV, excepto que si se usa ViewPager, se puede configurar el número de precargas. Aquí, se configuran 4 antes y después. También se elimina a través de un VideoView y luego se agrega al diseño del elemento. .

El pseudocódigo central es el siguiente:

  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;
            }
        }
    }
复制代码

Dado que el interno no es una implementación de RV, debemos tratar con el grupo de caché personalizado, especialmente si se agrega la solución de caché de video.

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. VerPagr2

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

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

核心的伪代码如下:

 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,后者通过解析出请求中真正的 1234.mp4 地址,创建对应的下载任务,并从下载的文件缓存中,读取 buffer 返回给播放器。

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

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

public class TiktokAdapter extends RecyclerView.Adapter<Tiktok3Adapter.ViewHolder> {

    /**
     * 数据源
     */
    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个视频文件,此时按照道理应该是优先加载下一个视频,而不是第二个第三个视频。

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

具体方案实现

Aquí, el adaptador de nuestro ViewPager usa la solución BRVAH, y la interfaz de usuario y el control de reproducción de video usan JzvdStd. El kernel de reproducción de video usa ExoPlayer.

Inicializar ViewPager2

 /**
     * 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);

    }
复制代码

Cargue más, y el elemento de escucha de clics:

        //内部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;
            }
        }
    }   
复制代码

Dado que BRVAH implementa el Adaptador, es un poco más simple. Es necesario prestar atención al recorte de videos largos y anchos.

public class TiktokAdapter extends BaseQuickAdapter<NewsFeedAdatperBean, TiktokAdapter.TiktokViewHolder> {

    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);
        }
    }
}
复制代码

epílogo

De hecho, los tres esquemas tienen aplicaciones implementadas en el mercado, y todas se pueden hacer. Personalmente, solo creo que RV es más conveniente, así que elegí ViewPager2. De hecho, el control interno se basa en la operación después de encontrar el caravana

Si la expansión posterior necesita deslizarse hacia la izquierda y hacia la derecha como Douyin para ingresar a la página de detalles, también podemos usar ViewPager anidado para implementar, o DrawerLayout para implementar, o personalizar completamente View para implementar, todo es posible.

Con respecto al problema de ViewPager2 que anida ViewPager, el artículo anterior mencionó [Portal] . Como nuestras necesidades no tienen la lógica de deslizarse de izquierda a derecha por los detalles, tampoco lo probé. Si hay un pozo, compártelo con tus amigos.

Como lo que estamos haciendo es solo una versión simple, el ejemplo de efecto específico se ha publicado al principio del artículo. Si tiene una solución mejor o un espacio optimizado, también puede compartirlo con nosotros. Si hay algún error u omisión, indíquelo, y si tiene alguna pregunta, puede discutirla juntos en el área de comentarios.

Si sientes que este artículo te ha inspirado un poco, espero que puedas 点赞apoyarlo, tu apoyo es mi mayor motivación.

Ok, este es el final de este problema.

Supongo que te gusta

Origin juejin.im/post/7155381983814615048
Recomendado
Clasificación