仿抖音上下滑动分页视频

目录介绍

  • 01.先来看一下需求
  • 02.有几种实现方式
    • 2.1 使用ViewPager
    • 2.2 使用RecyclerView
  • 03.用ViewPager实现
    • 3.1 自定义ViewPager
    • 3.2 ViewPager和Fragment
    • 3.3 修改滑动距离翻页
    • 3.4 修改滑动速度
  • 04.用RecyclerView实现
    • 4.1 自定义LayoutManager
    • 4.2 添加滑动监听
    • 4.3 监听页面是否滚动
    • 4.4 attach和Detached
  • 05.优化点详谈
    • 5.1 ViewPager改变滑动速率
    • 5.2 PagerSnapHelper注意点
    • 5.3 自定义LayoutManager注意点
    • 5.4 视频播放逻辑优化
    • 5.5 视频逻辑充分解藕
    • 5.6 翻页卡顿优化分析
    • 5.7 上拉很快翻页黑屏

00.先看一下效果图

image

01.先来看一下需求

  • 项目中的视频播放,要求实现抖音那种竖直方向一次滑动一页的效果。滑动要流畅不卡顿,而且手动触摸滑动超过1/2的时候松开能够滑动下一页,没有超过1/2返回原页。
  • 手指拖动页面滑动,只要没有切换到其余的页面,视频都是在播放的。切换了页面,上一个视频销毁,该页面则开始初始化播放。
  • 切换页面的时候过渡效果要天然,避免出现闪屏。具体的滑动效果,能够直接参考抖音……
  • 开源库地址:github.com/yangchong21…

02.有几种实现方式

2.1 使用ViewPager

  • 使用ViewPager实现竖直方法上下切换视频分析
    • 1.最近项目需求中有用到须要在ViewPager中播放视频,就是竖直方法上下滑动切换视频,视频是网络视频,最开始的实现思路是ViewPager中根据当前item位置去初始化SurfaceView,同时销毁时根据item的位置移除SurfaceView。
    • 2.上面那种方式确实是能够实现的,可是存在2个问题,第一,MediaPlayer的生命周期不容易控制而且存在内存泄漏问题。第二,连续三个item都是视频时,来回滑动的过程当中发现会出现上个视频的最后一帧画面的bug。
    • 3.未提高用户体验,视频播放器初始化完成前上面会覆盖有该视频的第一帧图片,可是发现存在第一帧图片与视频第一帧信息不符的状况,后面会经过代码给出解决方案。
  • 大概的实现思路是这样
    • 1.须要自定义一个竖直方向滑动的ViewPager,注意这个特别重要。
    • 2.一次滑动一页,建议采用ViewPager+FragmentStatePagerAdapter+Fragment方式来作,后面会详细说。
    • 3.在fragment中处理视频的初始化,播放和销毁逻辑等逻辑。
    • 4.因为一个页面须要建立一个fragment,注意性能和滑动流畅度这块须要分析和探讨。
  • 不太建议使用ViewPager
    • 1.ViewPager 自带的滑动效果彻底知足场景,并且支持Fragment和View等UI绑定,只要对布局和触摸事件部分做一些修改,就能够把横向的 ViewPager 改为竖向。
    • 2.可是没有复用是个最致命的问题。在onLayout方法中,全部子View会实例化并一字排开在布局上。当Item数量很大时,将会是很大的性能浪费。
    • 3.其次是可见性判断的问题。不少人会觉得 Fragment 在 onResume 的时候就是可见的,而 ViewPager 中的 Fragment 就是个反例,尤为是多个 ViewPager 嵌套时,会同时有多个父 Fragment 多个子 Fragment 处于 onResume 的状态,却只有其中一个是可见的。除非放弃 ViewPager 的预加载机制。在页面内容曝光等重要的数据上报时,就须要判断不少条件:onResumed 、 setUserVisibleHint 、 setOnPageChangeListener 等。

2.2 使用RecyclerView

  • 使用RecyclerView实现树枝方向上下切换视频分析
    • 1.首先RecyclerView它设置竖直方向滑动是十分简单的,同时关于item的四级缓存也作好了处理,并且滑动的效果相比ViewPager要好一些。
    • 2.滑动事件处理比viewPager好,即便你外层嵌套了下拉刷新上拉加载的布局,也不影响后期事件冲突处理,详细能够看demo案例。
  • 大概的实现思路是这样
    • 1.自定义一个LinearLayoutManager,重写onScrollStateChanged方法,注意是拿到滑动状态。
    • 2.一次滑动切换一个页面,可使用PagerSnapHelper来实现,十分方便简单。
    • 3.在recyclerView对应的adapter中,在onCreateViewHolder初始化视频操做,同时当onViewRecycled时,销毁视频资源。
    • 4.添加自定义回调接口,在滚动页面和attch,detach的时候,定义初始化,页面销毁等方法,暴露给开发者。

03.用ViewPager实现

3.1 自定义ViewPager

  • 代码以下所示,这里省略了很多的代码,具体能够看项目中的代码。
    /** * <pre> * @author 杨充 * blog : https://github.com/yangchong211 * time : 2019/6/20 * desc : 自定义ViewPager,主要是处理边界极端状况 * revise: * </pre> */
    public class VerticalViewPager extends ViewPager {
    
        private boolean isVertical = false;
        private long mRecentTouchTime;
    
        public VerticalViewPager(@NonNull Context context) {
            super(context);
        }
    
        public VerticalViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        private void init() {
            setPageTransformer(true, new HorizontalVerticalPageTransformer());
            setOverScrollMode(OVER_SCROLL_NEVER);
        }
    
        public boolean isVertical() {
            return isVertical;
        }
    
        public void setVertical(boolean vertical) {
            isVertical = vertical;
            init();
        }
    
        private class HorizontalVerticalPageTransformer implements PageTransformer {
    
            private static final float MIN_SCALE = 0.25f;
    
            @Override
            public void transformPage(@NonNull View page, float position) {
                if (isVertical) {
                    if (position < -1) {
                        page.setAlpha(0);
                    } else if (position <= 1) {
                        page.setAlpha(1);
                        // Counteract the default slide transition
                        float xPosition = page.getWidth() * -position;
                        page.setTranslationX(xPosition);
                        //set Y position to swipe in from top
                        float yPosition = position * page.getHeight();
                        page.setTranslationY(yPosition);
                    } else {
                        page.setAlpha(0);
                    }
                } else {
                    int pageWidth = page.getWidth();
                    if (position < -1) { // [-Infinity,-1)
                        // This page is way off-screen to the left.
                        page.setAlpha(0);
                    } else if (position <= 0) { // [-1,0]
                        // Use the default slide transition when moving to the left page
                        page.setAlpha(1);
                        page.setTranslationX(0);
                        page.setScaleX(1);
                        page.setScaleY(1);
                    } else if (position <= 1) { // (0,1]
                        // Fade the page out.
                        page.setAlpha(1 - position);
                        // Counteract the default slide transition
                        page.setTranslationX(pageWidth * -position);
                        page.setTranslationY(0);
                        // Scale the page down (between MIN_SCALE and 1)
                        float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));
                        page.setScaleX(scaleFactor);
                        page.setScaleY(scaleFactor);
                    } else { // (1,+Infinity]
                        // This page is way off-screen to the right.
                        page.setAlpha(0);
                    }
                }
            }
        }
    
        /** * 交换x轴和y轴的移动距离 * @param event 获取事件类型的封装类MotionEvent */
        private MotionEvent swapXY(MotionEvent event) {
            //获取宽高
            float width = getWidth();
            float height = getHeight();
            //将Y轴的移动距离转变成X轴的移动距离
            float swappedX = (event.getY() / height) * width;
            //将X轴的移动距离转变成Y轴的移动距离
            float swappedY = (event.getX() / width) * height;
            //重设event的位置
            event.setLocation(swappedX, swappedY);
            return event;
        }
    
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            mRecentTouchTime = System.currentTimeMillis();
            if (getCurrentItem() == 0 && getChildCount() == 0) {
                return false;
            }
            if (isVertical) {
                boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
                swapXY(ev);
                // return touch coordinates to original reference frame for any child views
                return intercepted;
            } else {
                return super.onInterceptTouchEvent(ev);
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (getCurrentItem() == 0 && getChildCount() == 0) {
                return false;
            }
            if (isVertical) {
                return super.onTouchEvent(swapXY(ev));
            } else {
                return super.onTouchEvent(ev);
            }
        }
    }
    复制代码

3.2 ViewPager和Fragment

  • 采用了ViewPager+FragmentStatePagerAdapter+Fragment来处理。为什么选择使用FragmentStatePagerAdapter,主要是由于使用 FragmentStatePagerAdapter更省内存,可是销毁后新建也是须要时间的。通常状况下,若是你是用于ViewPager展现数量特别多的条目时,那么建议使用FragmentStatePagerAdapter。关于PagerAdapter的深度解析,能够我这篇文章:PagerAdapter深度解析和实践优化
  • 在activity中的代码以下所示
    private void initViewPager() {
        List<Video> list = new ArrayList<>();
        ArrayList<Fragment> fragments = new ArrayList<>();
        for (int a = 0; a< DataProvider.VideoPlayerList.length ; a++){
            Video video = new Video(DataProvider.VideoPlayerTitle[a],
                    10,"",DataProvider.VideoPlayerList[a]);
            list.add(video);
            fragments.add(VideoFragment.newInstant(DataProvider.VideoPlayerList[a]));
        }
        vp.setOffscreenPageLimit(1);
        vp.setCurrentItem(0);
        vp.setOrientation(DirectionalViewPager.VERTICAL);
        FragmentManager supportFragmentManager = getSupportFragmentManager();
        MyPagerAdapter myPagerAdapter = new MyPagerAdapter(fragments, supportFragmentManager);
        vp.setAdapter(myPagerAdapter);
    }
    
    
    class MyPagerAdapter extends FragmentStatePagerAdapter{
    
        private ArrayList<Fragment> list;
    
        public MyPagerAdapter(ArrayList<Fragment> list , FragmentManager fm){
            super(fm);
            this.list = list;
        }
    
        @Override
        public Fragment getItem(int i) {
            return list.get(i);
        }
    
        @Override
        public int getCount() {
            return list!=null ? list.size() : 0;
        }
    }
    复制代码
  • 那么在fragment中如何处理呢?关于视频播放器,这里能够看我封装的库,视频lib
    public class VideoFragment extends  Fragment{
    
        public VideoPlayer videoPlayer;
        private String url;
        private int index;
    
        @Override
        public void onStop() {
            super.onStop();
            VideoPlayerManager.instance().releaseVideoPlayer();
        }
    
        public static Fragment newInstant(String url){
            VideoFragment videoFragment = new VideoFragment();
            Bundle bundle = new Bundle();
            bundle.putString("url",url);
            videoFragment.setArguments(bundle);
            return videoFragment;
        }
    
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Bundle arguments = getArguments();
            if (arguments != null) {
                url = arguments.getString("url");
            }
        }
    
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater,
                                 @Nullable ViewGroup container,
                                 @Nullable Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_video, container, false);
            return view;
        }
    
        @Override
        public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            videoPlayer = view.findViewById(R.id.video_player);
        }
    
        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            Log.d("初始化操做","------"+index++);
            VideoPlayerController controller = new VideoPlayerController(getActivity());
            videoPlayer.setUp(url,null);
            videoPlayer.setPlayerType(ConstantKeys.IjkPlayerType.TYPE_IJK);
            videoPlayer.setController(controller);
            ImageUtils.loadImgByPicasso(getActivity(),"",
                    R.drawable.image_default,controller.imageView());
        }
    }
    复制代码

3.3 修改滑动距离翻页

  • 需求要求必须手动触摸滑动超过1/2的时候松开能够滑动下一页,没有超过1/2返回原页,首先确定是重写viewpager,只能从源码下手。通过分析,源码滑动的逻辑处理在此处,truncator的属性表明判断的比例值!
    • 这个方法会在切页的时候重定向Page,好比从第一个页面滑动,结果没有滑动到第二个页面,而是又返回到第一个页面,那个这个page会有重定向的功能
    private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
        int targetPage;
        if (Math.abs(deltaX) > this.mFlingDistance && Math.abs(velocity) > this.mMinimumVelocity) {
            targetPage = velocity > 0 ? currentPage : currentPage + 1;
        } else {
            float truncator = currentPage >= this.mCurItem ? 0.4F : 0.6F;
            targetPage = currentPage + (int)(pageOffset + truncator);
        }
    
        if (this.mItems.size() > 0) {
            ViewPager.ItemInfo firstItem = (ViewPager.ItemInfo)this.mItems.get(0);
            ViewPager.ItemInfo lastItem = (ViewPager.ItemInfo)this.mItems.get(this.mItems.size() - 1);
            targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
        }
    
        return targetPage;
    }
    复制代码
    • determineTargetPage这个方法就是计算接下来要滑到哪一页。这个方法调用是在MotionEvent.ACTION_UP这个事件下,先说下参数意思:
      • currentPage:当前ViewPager显示的页面
      • pageOffset:用户滑动的页面偏移量
      • velocity: 滑动速率
      • deltaX: X方向移动的距离
    • 进行debug调试以后,发现问题就在0.4f和0.6f这个参数上。分析得出:0.6f表示用户滑动可以翻页的偏移量,因此不难理解,为啥要滑动半屏或者以上了。
  • 也能够修改Touch事件
    • 控制ViewPager的Touch事件,这个基本是万能的,毕竟是从根源上入手的。你能够在onTouchEvent和onInterceptTouchEvent中作逻辑的判断。可是比较麻烦。

3.4 修改滑动速度

  • 使用viewPager进行滑动时,若是经过手指滑动来进行的话,能够根据手指滑动的距离来实现,可是若是经过setCurrentItem函数来实现的话,则会发现直接闪过去的,会出现一下刷屏。想要经过使用setCurrentItem函数来进行viewpager的滑动,而且须要有过分滑动的动画,那么,该如何作呢?
  • 具体能够分析setCurrentItem源码的逻辑,而后会看到scrollToItem方法,这个特别重要,主要是处理滚动过程当中的逻辑。最主要关心的也是smoothScrollTo函数,这个函数中,能够看到具体执行滑动的其实就一句话,就是mScroller.startScroll(sx,sy,dx,dy,duration),则能够看到,是mScroller这个对象进行滑动的。那么想要改变它的属性,则能够经过反射来实现。
  • 代码以下所示,若是是手指触摸滑动,则能够加快一点滑动速率,固然滑动持续时间你能够本身设置。经过本身自定义滑动的时间,就能够控制滑动的速度。
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public void setAnimationDuration(final int during){
        try {
            // viewPager平移动画事件
            Field mField = ViewPager.class.getDeclaredField("mScroller");
            mField.setAccessible(true);
            // 动画效果与ViewPager的一致
            Interpolator interpolator = new Interpolator() {
                @Override
                public float getInterpolation(float t) {
                    t -= 1.0f;
                    return t * t * t * t * t + 1.0f;
                }
            };
            Scroller mScroller = new Scroller(getContext(),interpolator){
                final int time = 2000;
                @Override
                public void startScroll(int x, int y, int dx, int dy, int duration) {
                    // 若是手工滚动,则加速滚动
                    if (System.currentTimeMillis() - mRecentTouchTime > time) {
                        duration = during;
                    } else {
                        duration /= 2;
                    }
                    super.startScroll(x, y, dx, dy, duration);
                }
    
                @Override
                public void startScroll(int x, int y, int dx, int dy) {
                    super.startScroll(x, y, dx, dy,during);
                }
            };
            mField.set(this, mScroller);
        } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
            e.printStackTrace();
        }
    }
    复制代码

04.用RecyclerView实现

4.1 自定义LayoutManager

  • 自定义LayoutManager,而且继承LinearLayoutManager,这样就获得一个能够水平排向或者竖向排向的布局策略。若是你接触过SnapHelper应该了解一下LinearSnapHelper和PagerSnapHelper这两个子类类,LinearSnapHelper能够实现让列表的Item居中显示的效果,PagerSnapHelper就能够作到一次滚动一个item显示的效果。
  • 重写onChildViewAttachedToWindow方法,在RecyclerView中,当Item添加进来了调用这个方法。这个方法至关因而把view添加到window时候调用的,也就是说它比draw方法先执行,能够作一些初始化相关的操做。
    /** * 该方法必须调用 * @param recyclerView recyclerView */
    @Override
    public void onAttachedToWindow(RecyclerView recyclerView) {
        if (recyclerView == null) {
            throw new IllegalArgumentException("The attach RecycleView must not null!!");
        }
        super.onAttachedToWindow(recyclerView);
        this.mRecyclerView = recyclerView;
        if (mPagerSnapHelper==null){
            init();
        }
        mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
        mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);
    }
    复制代码

4.2 添加滑动监听

  • 涉及到一次滑动一页视频,那么确定会有视频初始化和释放的功能。那么思考一下哪里来开始播放视频和在哪里释放视频?不要着急,要监听滑动到哪页,须要咱们重写onScrollStateChanged()函数,这里面有三种状态:SCROLL_STATE_IDLE(空闲),SCROLL_STATE_DRAGGING(拖动),SCROLL_STATE_SETTLING(要移动到最后位置时)。
  • 咱们须要的就是RecyclerView中止时的状态,咱们就能够拿到这个View的Position,注意这里还有一个问题,当你经过这个position去拿Item会报错,这里涉及到RecyclerView的缓存机制,本身去脑补~~。打印Log,你会发现RecyclerView.getChildCount()一直为1或者会出现为2的状况。来实现一个接口而后经过接口把状态传递出去。
  • 自定义监听listener事件
    public interface OnPagerListener {
    
        /** * 初始化完成 */
        void onInitComplete();
    
        /** * 释放的监听 * @param isNext 是否下一个 * @param position 索引 */
        void onPageRelease(boolean isNext,int position);
    
        /*** * 选中的监听以及判断是否滑动到底部 * @param position 索引 * @param isBottom 是否到了底部 */
        void onPageSelected(int position,boolean isBottom);
    }
    复制代码
  • 获取到RecyclerView空闲时选中的Item,重写LinearLayoutManager的onScrollStateChanged方法
    /**
     * 滑动状态的改变
     * 缓慢拖拽-> SCROLL_STATE_DRAGGING
     * 快速滚动-> SCROLL_STATE_SETTLING
     * 空闲状态-> SCROLL_STATE_IDLE
     * @param state                         状态
     */
    @Override
    public void onScrollStateChanged(int state) {
        switch (state) {
            case RecyclerView.SCROLL_STATE_IDLE:
                View viewIdle = mPagerSnapHelper.findSnapView(this);
                int positionIdle = 0;
                if (viewIdle != null) {
                    positionIdle = getPosition(viewIdle);
                }
                if (mOnViewPagerListener != null && getChildCount() == 1) {
                    mOnViewPagerListener.onPageSelected(positionIdle,
                            positionIdle == getItemCount() - 1);
                }
                break;
            case RecyclerView.SCROLL_STATE_DRAGGING:
                View viewDrag = mPagerSnapHelper.findSnapView(this);
                if (viewDrag != null) {
                    int positionDrag = getPosition(viewDrag);
                }
                break;
            case RecyclerView.SCROLL_STATE_SETTLING:
                View viewSettling = mPagerSnapHelper.findSnapView(this);
                if (viewSettling != null) {
                    int positionSettling = getPosition(viewSettling);
                }
                break;
            default:
                break;
        }
    }
    复制代码

4.3 监听页面是否滚动

  • 这里有两个方法scrollHorizontallyBy()和scrollVerticallyBy()能够拿到滑动偏移量,能够判断滑动方向。
    /**
     * 监听竖直方向的相对偏移量
     * @param dy                                y轴滚动值
     * @param recycler                          recycler
     * @param state                             state滚动状态
     * @return                                  int值
     */
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        this.mDrift = dy;
        return super.scrollVerticallyBy(dy, recycler, state);
    }
    
    
    /**
     * 监听水平方向的相对偏移量
     * @param dx                                x轴滚动值
     * @param recycler                          recycler
     * @param state                             state滚动状态
     * @return                                  int值
     */
    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dx == 0) {
            return 0;
        }
        this.mDrift = dx;
        return super.scrollHorizontallyBy(dx, recycler, state);
    }
    复制代码

4.4 attach和Detached

  • 列表的选中监听好了,咱们就看看何时释放视频的资源,第二步中的三种状态,去打印getChildCount()的日志,你会发现getChildCount()在SCROLL_STATE_DRAGGING会为1,SCROLL_STATE_SETTLING为2,SCROLL_STATE_IDLE有时为1,有时为2,仍是RecyclerView的缓存机制O(∩∩)O,这里不会去赘述缓存机制,要作的是要知道在何时去作释放视频的操做,还要分清是释放上一页仍是下一页。
    private RecyclerView.OnChildAttachStateChangeListener mChildAttachStateChangeListener =
            new RecyclerView.OnChildAttachStateChangeListener() {
        /**
         * 第一次进入界面的监听,能够作初始化方面的操做
         * @param view                      view
         */
        @Override
        public void onChildViewAttachedToWindow(@NonNull View view) {
            if (mOnViewPagerListener != null && getChildCount() == 1) {
                mOnViewPagerListener.onInitComplete();
            }
        }
    
        /**
         * 页面销毁的时候调用该方法,能够作销毁方面的操做
         * @param view                      view
         */
        @Override
        public void onChildViewDetachedFromWindow(@NonNull View view) {
            if (mDrift >= 0){
                if (mOnViewPagerListener != null) {
                    mOnViewPagerListener.onPageRelease(true , getPosition(view));
                }
            }else {
                if (mOnViewPagerListener != null) {
                    mOnViewPagerListener.onPageRelease(false , getPosition(view));
                }
            }
        }
    };
    复制代码
  • 哪里添加该listener监听事件,以下所示。这里注意须要在页面销毁的时候移除listener监听事件。
    /**
     * attach到window窗口时,该方法必须调用
     * @param recyclerView                          recyclerView
     */
    @Override
    public void onAttachedToWindow(RecyclerView recyclerView) {
        //这里省略部分代码
        mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);
    }
    
    /**
     * 销毁的时候调用该方法,须要移除监听事件
     * @param view                                  view
     * @param recycler                              recycler
     */
    @Override
    public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
        super.onDetachedFromWindow(view, recycler);
        if (mRecyclerView!=null){
            mRecyclerView.removeOnChildAttachStateChangeListener(mChildAttachStateChangeListener);
        }
    }
    复制代码

05.优化点详谈

5.1 ViewPager改变滑动速率

  • 能够经过反射修改属性,注意,使用反射的时候,建议手动try-catch,避免异常致使崩溃。代码以下所示:
    /** * 修改滑动灵敏度 * @param flingDistance 滑动惯性,默认是75 * @param minimumVelocity 最小滑动值,默认是1200 */
    public void setScrollFling(int flingDistance , int minimumVelocity){
        try {
            Field mFlingDistance = ViewPager.class.getDeclaredField("mFlingDistance");
            mFlingDistance.setAccessible(true);
            Object o = mFlingDistance.get(this);
            Log.d("setScrollFling",o.toString());
            //默认值75
            mFlingDistance.set(this, flingDistance);
    
            Field mMinimumVelocity = ViewPager.class.getDeclaredField("mMinimumVelocity");
            mMinimumVelocity.setAccessible(true);
            Object o1 = mMinimumVelocity.get(this);
            Log.d("setScrollFling",o1.toString());
            //默认值1200
            mMinimumVelocity.set(this,minimumVelocity);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    复制代码

5.2 PagerSnapHelper注意点

  • 好多时候会抛出一个异常"illegalstateexception an instance of onflinglistener already set".
  • 看SnapHelper源码attachToRecyclerView(xxx)方法时,能够看到若是recyclerView不为null,则先destoryCallback(),它做用在于取消以前的RecyclerView的监听接口。而后经过setupCallbacks()设置监听器,若是当前RecyclerView已经设置了OnFlingListener,会抛出一个状态异常。那么这个如何复现了,很容易,你初始化屡次就能够看到这个bug。
  • 建议手动捕获一下该异常,代码设置以下所示。源码中判断了,若是onFlingListener已经存在的话,再次设置就直接抛出异常,那么这里能够加强一下逻辑判断,ok,那么问题便解决呢!
    try {
        //attachToRecyclerView源码上的方法可能会抛出IllegalStateException异常,这里手动捕获一下
        RecyclerView.OnFlingListener onFlingListener = mRecyclerView.getOnFlingListener();
        //源码中判断了,若是onFlingListener已经存在的话,再次设置就直接抛出异常,那么这里能够判断一下
        if (onFlingListener==null){
            mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
        }
    } catch (IllegalStateException e){
        e.printStackTrace();
    }
    复制代码

5.3 自定义LayoutManager注意点

  • 网上有人已经写了一篇自定义LayoutManager实现抖音的效果的博客,我本身也很仔细看了这篇文章。不过我以为有几个注意要点,由于要用到线上app,则必定要尽量减小崩溃率……
  • 经过SnapHelper调用findSnapView方法,获得的view,必定要增长非空判断逻辑,不然很容易形成崩溃。
  • 在监听滚动位移scrollVerticallyBy的时候,注意要增长判断,就是getChildCount()若是为0时,则须要返回0。
  • 在onDetachedFromWindow调用的时候,能够把listener监听事件给remove掉。

5.4 视频播放逻辑优化

  • 从前台切到后台,当视频正在播放或者正在缓冲时,调用方法能够设置暂停视频。销毁页面,释放,内部的播放器被释放掉,同时若是在全屏、小窗口模式下都会退出。从后台切换到前台,当视频暂停时或者缓冲暂停时,调用该方法从新开启视频播放。具体视频播放代码设置以下,具体更加详细内容能够看我封装的视频播放器lib
    @Override
    protected void onStop() {
        super.onStop();
        //从前台切到后台,当视频正在播放或者正在缓冲时,调用该方法暂停视频
        VideoPlayerManager.instance().suspendVideoPlayer();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //销毁页面,释放,内部的播放器被释放掉,同时若是在全屏、小窗口模式下都会退出
        VideoPlayerManager.instance().releaseVideoPlayer();
    }
    
    @Override
    public void onBackPressed() {
        //处理返回键逻辑;若是是全屏,则退出全屏;若是是小窗口,则退出小窗口
        if (VideoPlayerManager.instance().onBackPressed()){
            return;
        }else {
            //销毁页面
            VideoPlayerManager.instance().releaseVideoPlayer();
        }
        super.onBackPressed();
    }
    
    @Override
    protected void onRestart() {
        super.onRestart();
        //从后台切换到前台,当视频暂停时或者缓冲暂停时,调用该方法从新开启视频播放
        VideoPlayerManager.instance().resumeVideoPlayer();
    }
    复制代码

5.5 视频逻辑充分解藕

  • 实际开发中,翻页确定会涉及到视频的初始化和销毁的逻辑。首先要保证视频只有惟一一个播放,滑动到分页一半,总不可能让两个页面都播放视频吧,因此须要保证视频VideoPlayer是一个单利对象,这样就能够保证惟一性呢!接着,无论是在recyclerView仍是ViewPager中,当页面处于不可见被销毁或者view被回收的阶段,这个时候须要把视频资源销毁,尽可能视频播放功能封装起来,而后在页面不一样状态调用方法便可。
  • 固然,实际app中,视频播放页面,还有一些点赞,评论,分享,查看做者等等不少其余功能。那么这些都是要请求接口的,还有滑动分页的功能,当滑动到最后某一页时候拉取下一个视频集合数据等业务逻辑。视频播放功能这块,由于功能比较复杂,所以封装一下比较好。尽可能作到视频功能解藕!关于视频封装库,能够看我以前写的一个库,视频播放器

5.6 翻页卡顿优化分析

  • 若是是使用recyclerView实现滑动翻页效果,那么为了提升使用体验效果。则能够注意:1.在onBindViewHolder中不要作耗时操做,2.视频滑动翻页的布局固定高度,避免重复计算高度RecyclerView.setHasFixedSize(true),3.关于分页拉取数据注意,建议一次拉下10条数据(这个也能够和服务端协定自定义数量),而不要滑动一页加载下一页的数据。

5.7 上拉很快翻页黑屏

  • 由于设置视频的背景颜色为黑色,我看了好多播放器初始化的时候,都是这样的。由于最简单的解决办法,就是给它加个封面,设置封面的背景便可。

其余介绍

参考博客

01.关于博客汇总连接

02.关于个人博客

滑动翻页开源库:github.com/yangchong21…

视频播放器:github.com/yangchong21…

相关文章
相关标签/搜索