仿抖音上下滑動分頁視訊
阿新 • • 發佈:2019-09-07
目錄介紹
- 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