乐趣区

仿抖音上下滑动分页视频

目录介绍

  • 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 上拉很快翻页黑屏

01. 先来看一下需求

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

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:
 */
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 方法先执行,可以做一些初始化相关的操作。

    /**
     * 该方法必须调用 
 */
@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
 */
@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 滚动状态 
 */
@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() {
        /**
         * 第一次进入界面的监听,可以做初始化方面的操作 
     */
    @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 窗口时,该方法必须调用 
 */
@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
 */
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 上拉很快翻页黑屏

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

其他介绍

参考博客

  • 自定义 LayoutManager 实现抖音的效果:https://www.jianshu.com/p/34a…
  • ViewPager 不为人知的秘密:https://www.jianshu.com/p/808…

01. 关于博客汇总链接

  • 1. 技术博客汇总
  • 2. 开源项目汇总
  • 3. 生活博客汇总
  • 4. 喜马拉雅音频汇总
  • 5. 其他汇总

02. 关于我的博客

  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/…
  • 简书:http://www.jianshu.com/u/b7b2…
  • csdn:http://my.csdn.net/m0_37700275
  • 喜马拉雅听书:http://www.ximalaya.com/zhubo…
  • 开源中国:https://my.oschina.net/zbj161…
  • 泡在网上的日子:http://www.jcodecraeer.com/me…
  • 邮箱:yangchong211@163.com
  • 阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
  • segmentfault 头条:https://segmentfault.com/u/xi…
  • 掘金:https://juejin.im/user/593943…

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

视频播放器:https://github.com/yangchong2…

退出移动版