仿抖音上下滑動分頁視頻

目錄介紹

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

00.先看一下效果圖

image

01.先來看一下需求

  • 項目中的視頻播放,要求實現抖音那種豎直方向一次滑動一頁的效果。滑動要流暢不卡頓,而且手動觸摸滑動超過1/2的時候鬆開能夠滑動下一頁,沒有超過1/2返回原頁。
  • 手指拖動頁面滑動,只要沒有切換到其餘的頁面,視頻都是在播放的。切換了頁面,上一個視頻銷燬,該頁面則開始初始化播放。
  • 切換頁面的時候過渡效果要天然,避免出現閃屏。具體的滑動效果,能夠直接參考抖音……
  • 開源庫地址:github.com/yangchong21…

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.關於個人博客

滑動翻頁開源庫:github.com/yangchong21…

視頻播放器:github.com/yangchong21…

相關文章
相關標籤/搜索