1. 程式人生 > >仿抖音上下滑動分頁視訊

仿抖音上下滑動分頁視訊

目錄介紹

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

  • 因為設定視訊的背景顏色為黑色,我看了好多播放器初始化的時候,都是這樣的。因為最簡單的解決辦法,就是給它加個封面,設定封面的背景即可。

其他介紹

參考部落格

01.關於部落格彙總連結

02.關於我的部落格

滑動翻頁開源庫:https://github.com/yangchong211/YCScrollPager

視訊播放器:https://github.com/yangchong2