目錄介紹
- 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 { 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 上拉很快翻頁黑屏
- 因爲設置視頻的背景顏色爲黑色,我看了好多播放器初始化的時候,都是這樣的。因爲最簡單的解決辦法,就是給它加個封面,設置封面的背景即可。
其他介紹
參考博客
- 自定義LayoutManager實現抖音的效果:https://www.jianshu.com/p/34a0ef2d806d
- ViewPager不爲人知的祕密:https://www.jianshu.com/p/80891d0185f7
01.關於博客彙總鏈接
02.關於我的博客
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 簡書:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書:http://www.ximalaya.com/zhubo/71989305/
- 開源中國:https://my.oschina.net/zbj1618/blog
- 泡在網上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿里雲博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e