目录介绍
- 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:
* </pre>
*/
public class VerticalViewPager extends ViewPager {
代码语言:txt
复制 private boolean isVertical = false;
代码语言:txt
复制 private long mRecentTouchTime;
代码语言:txt
复制 public VerticalViewPager(@NonNull Context context) {
代码语言:txt
复制 super(context);
代码语言:txt
复制 }
代码语言:txt
复制 public VerticalViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
代码语言:txt
复制 super(context, attrs);
代码语言:txt
复制 }
代码语言:txt
复制 private void init() {
代码语言:txt
复制 setPageTransformer(true, new HorizontalVerticalPageTransformer());
代码语言:txt
复制 setOverScrollMode(OVER_SCROLL_NEVER);
代码语言:txt
复制 }
代码语言:txt
复制 public boolean isVertical() {
代码语言:txt
复制 return isVertical;
代码语言:txt
复制 }
代码语言:txt
复制 public void setVertical(boolean vertical) {
代码语言:txt
复制 isVertical = vertical;
代码语言:txt
复制 init();
代码语言:txt
复制 }
代码语言:txt
复制 private class HorizontalVerticalPageTransformer implements PageTransformer {
代码语言:txt
复制 private static final float MIN_SCALE = 0.25f;
代码语言:txt
复制 @Override
代码语言:txt
复制 public void transformPage(@NonNull View page, float position) {
代码语言:txt
复制 if (isVertical) {
代码语言:txt
复制 if (position < -1) {
代码语言:txt
复制 page.setAlpha(0);
代码语言:txt
复制 } else if (position <= 1) {
代码语言:txt
复制 page.setAlpha(1);
代码语言:txt
复制 // Counteract the default slide transition
代码语言:txt
复制 float xPosition = page.getWidth() * -position;
代码语言:txt
复制 page.setTranslationX(xPosition);
代码语言:txt
复制 //set Y position to swipe in from top
代码语言:txt
复制 float yPosition = position * page.getHeight();
代码语言:txt
复制 page.setTranslationY(yPosition);
代码语言:txt
复制 } else {
代码语言:txt
复制 page.setAlpha(0);
代码语言:txt
复制 }
代码语言:txt
复制 } else {
代码语言:txt
复制 int pageWidth = page.getWidth();
代码语言:txt
复制 if (position < -1) { // [-Infinity,-1)
代码语言:txt
复制 // This page is way off-screen to the left.
代码语言:txt
复制 page.setAlpha(0);
代码语言:txt
复制 } else if (position <= 0) { // [-1,0]
代码语言:txt
复制 // Use the default slide transition when moving to the left page
代码语言:txt
复制 page.setAlpha(1);
代码语言:txt
复制 page.setTranslationX(0);
代码语言:txt
复制 page.setScaleX(1);
代码语言:txt
复制 page.setScaleY(1);
代码语言:txt
复制 } else if (position <= 1) { // (0,1]
代码语言:txt
复制 // Fade the page out.
代码语言:txt
复制 page.setAlpha(1 - position);
代码语言:txt
复制 // Counteract the default slide transition
代码语言:txt
复制 page.setTranslationX(pageWidth * -position);
代码语言:txt
复制 page.setTranslationY(0);
代码语言:txt
复制 // Scale the page down (between MIN_SCALE and 1)
代码语言:txt
复制 float scaleFactor = MIN_SCALE (1 - MIN_SCALE) * (1 - Math.abs(position));
代码语言:txt
复制 page.setScaleX(scaleFactor);
代码语言:txt
复制 page.setScaleY(scaleFactor);
代码语言:txt
复制 } else { // (1, Infinity]
代码语言:txt
复制 // This page is way off-screen to the right.
代码语言:txt
复制 page.setAlpha(0);
代码语言:txt
复制 }
代码语言:txt
复制 }
代码语言:txt
复制 }
代码语言:txt
复制 }
代码语言:txt
复制 /**
代码语言:txt
复制 * 交换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;
}
代码语言:txt
复制 @Override
代码语言:txt
复制 public boolean onInterceptTouchEvent(MotionEvent ev) {
代码语言:txt
复制 mRecentTouchTime = System.currentTimeMillis();
代码语言:txt
复制 if (getCurrentItem() == 0 && getChildCount() == 0) {
代码语言:txt
复制 return false;
代码语言:txt
复制 }
代码语言:txt
复制 if (isVertical) {
代码语言:txt
复制 boolean intercepted = super.onInterceptTouchEvent(swapXY(ev));
代码语言:txt
复制 swapXY(ev);
代码语言:txt
复制 // return touch coordinates to original reference frame for any child views
代码语言:txt
复制 return intercepted;
代码语言:txt
复制 } else {
代码语言:txt
复制 return super.onInterceptTouchEvent(ev);
代码语言:txt
复制 }
代码语言:txt
复制 }
代码语言:txt
复制 @Override
代码语言:txt
复制 public boolean onTouchEvent(MotionEvent ev) {
代码语言:txt
复制 if (getCurrentItem() == 0 && getChildCount() == 0) {
代码语言:txt
复制 return false;
代码语言:txt
复制 }
代码语言:txt
复制 if (isVertical) {
代码语言:txt
复制 return super.onTouchEvent(swapXY(ev));
代码语言:txt
复制 } else {
代码语言:txt
复制 return super.onTouchEvent(ev);
代码语言:txt
复制 }
代码语言:txt
复制 }
代码语言:txt
复制}
代码语言:txt
复制```
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);
}
代码语言:txt
复制class MyPagerAdapter extends FragmentStatePagerAdapter{
代码语言:txt
复制 private ArrayList<Fragment> list;
代码语言:txt
复制 public MyPagerAdapter(ArrayList<Fragment> list , FragmentManager fm){
代码语言:txt
复制 super(fm);
代码语言:txt
复制 this.list = list;
代码语言:txt
复制 }
代码语言:txt
复制 @Override
代码语言:txt
复制 public Fragment getItem(int i) {
代码语言:txt
复制 return list.get(i);
代码语言:txt
复制 }
代码语言:txt
复制 @Override
代码语言:txt
复制 public int getCount() {
代码语言:txt
复制 return list!=null ? list.size() : 0;
代码语言:txt
复制 }
代码语言:txt
复制}
代码语言:txt
复制```
- 那么在fragment中如何处理呢?关于视频播放器,这里可以看我封装的库,视频libpublic class VideoFragment extends Fragment{
代码语言:txt
复制 public VideoPlayer videoPlayer;
代码语言:txt
复制 private String url;
代码语言:txt
复制 private int index;
代码语言:txt
复制 @Override
代码语言:txt
复制 public void onStop() {
代码语言:txt
复制 super.onStop();
代码语言:txt
复制 VideoPlayerManager.instance().releaseVideoPlayer();
代码语言:txt
复制 }
代码语言:txt
复制 public static Fragment newInstant(String url){
代码语言:txt
复制 VideoFragment videoFragment = new VideoFragment();
代码语言:txt
复制 Bundle bundle = new Bundle();
代码语言:txt
复制 bundle.putString("url",url);
代码语言:txt
复制 videoFragment.setArguments(bundle);
代码语言:txt
复制 return videoFragment;
代码语言:txt
复制 }
代码语言:txt
复制 @Override
代码语言:txt
复制 public void onCreate(@Nullable Bundle savedInstanceState) {
代码语言:txt
复制 super.onCreate(savedInstanceState);
代码语言:txt
复制 Bundle arguments = getArguments();
代码语言:txt
复制 if (arguments != null) {
代码语言:txt
复制 url = arguments.getString("url");
代码语言:txt
复制 }
代码语言:txt
复制 }
代码语言:txt
复制 @Nullable
代码语言:txt
复制 @Override
代码语言:txt
复制 public View onCreateView(@NonNull LayoutInflater inflater,
代码语言:txt
复制 @Nullable ViewGroup container,
代码语言:txt
复制 @Nullable Bundle savedInstanceState) {
代码语言:txt
复制 View view = inflater.inflate(R.layout.fragment_video, container, false);
代码语言:txt
复制 return view;
代码语言:txt
复制 }
代码语言:txt
复制 @Override
代码语言:txt
复制 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
代码语言:txt
复制 super.onViewCreated(view, savedInstanceState);
代码语言:txt
复制 videoPlayer = view.findViewById(R.id.video_player);
代码语言:txt
复制 }
代码语言:txt
复制 @Override
代码语言:txt
复制 public void onActivityCreated(@Nullable Bundle savedInstanceState) {
代码语言:txt
复制 super.onActivityCreated(savedInstanceState);
代码语言:txt
复制 Log.d("初始化操作","------" index );
代码语言:txt
复制 VideoPlayerController controller = new VideoPlayerController(getActivity());
代码语言:txt
复制 videoPlayer.setUp(url,null);
代码语言:txt
复制 videoPlayer.setPlayerType(ConstantKeys.IjkPlayerType.TYPE_IJK);
代码语言:txt
复制 videoPlayer.setController(controller);
代码语言:txt
复制 ImageUtils.loadImgByPicasso(getActivity(),"",
代码语言:txt
复制 R.drawable.image_default,controller.imageView());
代码语言:txt
复制 }
代码语言:txt
复制}
代码语言:txt
复制```
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);
}
代码语言:txt
复制 if (this.mItems.size() > 0) {
代码语言:txt
复制 ViewPager.ItemInfo firstItem = (ViewPager.ItemInfo)this.mItems.get(0);
代码语言:txt
复制 ViewPager.ItemInfo lastItem = (ViewPager.ItemInfo)this.mItems.get(this.mItems.size() - 1);
代码语言:txt
复制 targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
代码语言:txt
复制 }
代码语言:txt
复制 return targetPage;
代码语言:txt
复制}
代码语言:txt
复制```
代码语言:txt
复制- 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);
}
代码语言:txt
复制 @Override
代码语言:txt
复制 public void startScroll(int x, int y, int dx, int dy) {
代码语言:txt
复制 super.startScroll(x, y, dx, dy,during);
代码语言:txt
复制 }
代码语言:txt
复制 };
代码语言:txt
复制 mField.set(this, mScroller);
代码语言:txt
复制 } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
代码语言:txt
复制 e.printStackTrace();
代码语言:txt
复制 }
代码语言:txt
复制}
代码语言:txt
复制```
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 {
代码语言:txt
复制 /**
代码语言:txt
复制 * 初始化完成
*/
void onInitComplete();
代码语言:txt
复制 /**
代码语言:txt
复制 * 释放的监听
* @param isNext 是否下一个
* @param position 索引
*/
void onPageRelease(boolean isNext,int position);
代码语言:txt
复制 /***
代码语言:txt
复制 * 选中的监听以及判断是否滑动到底部
* @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);
}
代码语言:txt
复制/**
代码语言:txt
复制 * 监听水平方向的相对偏移量
* @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();
}
}
代码语言:txt
复制 /**
代码语言:txt
复制 * 页面销毁的时候调用该方法,可以做销毁方面的操作
* @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);
}
代码语言:txt
复制/**
代码语言:txt
复制 * 销毁的时候调用该方法,需要移除监听事件
* @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);
代码语言:txt
复制 Field mMinimumVelocity = ViewPager.class.getDeclaredField("mMinimumVelocity");
代码语言:txt
复制 mMinimumVelocity.setAccessible(true);
代码语言:txt
复制 Object o1 = mMinimumVelocity.get(this);
代码语言:txt
复制 Log.d("setScrollFling",o1.toString());
代码语言:txt
复制 //默认值1200
代码语言:txt
复制 mMinimumVelocity.set(this,minimumVelocity);
代码语言:txt
复制 } catch (Exception e){
代码语言:txt
复制 e.printStackTrace();
代码语言:txt
复制 }
代码语言:txt
复制}
代码语言:txt
复制```
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();
}
代码语言:txt
复制@Override
代码语言:txt
复制protected void onDestroy() {
代码语言:txt
复制 super.onDestroy();
代码语言:txt
复制 //销毁页面,释放,内部的播放器被释放掉,同时如果在全屏、小窗口模式下都会退出
代码语言:txt
复制 VideoPlayerManager.instance().releaseVideoPlayer();
代码语言:txt
复制}
代码语言:txt
复制@Override
代码语言:txt
复制public void onBackPressed() {
代码语言:txt
复制 //处理返回键逻辑;如果是全屏,则退出全屏;如果是小窗口,则退出小窗口
代码语言:txt
复制 if (VideoPlayerManager.instance().onBackPressed()){
代码语言:txt
复制 return;
代码语言:txt
复制 }else {
代码语言:txt
复制 //销毁页面
代码语言:txt
复制 VideoPlayerManager.instance().releaseVideoPlayer();
代码语言:txt
复制 }
代码语言:txt
复制 super.onBackPressed();
代码语言:txt
复制}
代码语言:txt
复制@Override
代码语言:txt
复制protected void onRestart() {
代码语言:txt
复制 super.onRestart();
代码语言:txt
复制 //从后台切换到前台,当视频暂停时或者缓冲暂停时,调用该方法重新开启视频播放
代码语言:txt
复制 VideoPlayerManager.instance().resumeVideoPlayer();
代码语言:txt
复制}
代码语言:txt
复制```
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/34a0ef2d806d
- ViewPager不为人知的秘密:https://www.jianshu.com/p/80891d0185f7
滑动翻页开源库:https://github.com/yangchong211/YCScrollPager
视频播放器:https://github.com/yangchong211/YCVideoPlayer