仿抖音上下滑動分頁視頻

仿抖音上下滑動分頁視頻

目錄介紹

  • 01.先來看一下需求
  • 02.有幾種實現(xiàn)方式
    • 2.1 使用ViewPager
    • 2.2 使用RecyclerView
  • 03.用ViewPager實現(xiàn)
    • 3.1 自定義ViewPager
    • 3.2 ViewPager和Fragment
    • 3.3 修改滑動距離翻頁
    • 3.4 修改滑動速度
  • 04.用RecyclerView實現(xiàn)
    • 4.1 自定義LayoutManager
    • 4.2 添加滑動監(jiān)聽
    • 4.3 監(jiān)聽頁面是否滾動
    • 4.4 attach和Detached
  • 05.優(yōu)化點詳談
    • 5.1 ViewPager改變滑動速率
    • 5.2 PagerSnapHelper注意點
    • 5.3 自定義LayoutManager注意點
    • 5.4 視頻播放邏輯優(yōu)化
    • 5.5 視頻邏輯充分解藕
    • 5.6 翻頁卡頓優(yōu)化分析
    • 5.7 上拉很快翻頁黑屏

00.先看一下效果圖

01.先來看一下需求

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

02.有幾種實現(xiàn)方式

2.1 使用ViewPager

  • 使用ViewPager實現(xiàn)豎直方法上下切換視頻分析
    • 1.最近項目需求中有用到需要在ViewPager中播放視頻,就是豎直方法上下滑動切換視頻,視頻是網(wǎng)絡視頻,最開始的實現(xiàn)思路是ViewPager中根據(jù)當前item位置去初始化SurfaceView,同時銷毀時根據(jù)item的位置移除SurfaceView。
    • 2.上面那種方式確實是可以實現(xiàn)的,但是存在2個問題,第一,MediaPlayer的生命周期不容易控制并且存在內存泄漏問題。第二,連續(xù)三個item都是視頻時,來回滑動的過程中發(fā)現(xiàn)會出現(xiàn)上個視頻的最后一幀畫面的bug。
    • 3.未提升用戶體驗,視頻播放器初始化完成前上面會覆蓋有該視頻的第一幀圖片,但是發(fā)現(xiàn)存在第一幀圖片與視頻第一幀信息不符的情況,后面會通過代碼給出解決方案。
  • 大概的實現(xiàn)思路是這樣
    • 1.需要自定義一個豎直方向滑動的ViewPager,注意這個特別重要。
    • 2.一次滑動一頁,建議采用ViewPager+FragmentStatePagerAdapter+Fragment方式來做,后面會詳細說。
    • 3.在fragment中處理視頻的初始化,播放和銷毀邏輯等邏輯。
    • 4.由于一個頁面需要創(chuàng)建一個fragment,注意性能和滑動流暢度這塊需要分析和探討。
  • 不太建議使用ViewPager
    • 1.ViewPager 自帶的滑動效果完全滿足場景,而且支持Fragment和View等UI綁定,只要對布局和觸摸事件部分作一些修改,就可以把橫向的 ViewPager 改成豎向。
    • 2.但是沒有復用是個最致命的問題。在onLayout方法中,所有子View會實例化并一字排開在布局上。當Item數(shù)量很大時,將會是很大的性能浪費。
    • 3.其次是可見性判斷的問題。很多人會以為 Fragment 在 onResume 的時候就是可見的,而 ViewPager 中的 Fragment 就是個反例,尤其是多個 ViewPager 嵌套時,會同時有多個父 Fragment 多個子 Fragment 處于 onResume 的狀態(tài),卻只有其中一個是可見的。除非放棄 ViewPager 的預加載機制。在頁面內容曝光等重要的數(shù)據(jù)上報時,就需要判斷很多條件:onResumed 、 setUserVisibleHint 、 setOnPageChangeListener 等。

2.2 使用RecyclerView

  • 使用RecyclerView實現(xiàn)樹枝方向上下切換視頻分析
    • 1.首先RecyclerView它設置豎直方向滑動是十分簡單的,同時關于item的四級緩存也做好了處理,而且滑動的效果相比ViewPager要好一些。
    • 2.滑動事件處理比viewPager好,即使你外層嵌套了下拉刷新上拉加載的布局,也不影響后期事件沖突處理,詳細可以看demo案例。
  • 大概的實現(xiàn)思路是這樣
    • 1.自定義一個LinearLayoutManager,重寫onScrollStateChanged方法,注意是拿到滑動狀態(tài)。
    • 2.一次滑動切換一個頁面,可以使用PagerSnapHelper來實現(xiàn),十分方便簡單。
    • 3.在recyclerView對應的adapter中,在onCreateViewHolder初始化視頻操作,同時當onViewRecycled時,銷毀視頻資源。
    • 4.添加自定義回調接口,在滾動頁面和attch,detach的時候,定義初始化,頁面銷毀等方法,暴露給開發(fā)者。

03.用ViewPager實現(xiàn)

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展示數(shù)量特別多的條目時,那么建議使用FragmentStatePagerAdapter。關于PagerAdapter的深度解析,可以我這篇文章:PagerAdapter深度解析和實踐優(yōu)化

  • 在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,只能從源碼下手。經(jīng)過分析,源碼滑動的邏輯處理在此處,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這個事件下,先說下參數(shù)意思:
      • currentPage:當前ViewPager顯示的頁面
      • pageOffset:用戶滑動的頁面偏移量
      • velocity: 滑動速率
      • deltaX: X方向移動的距離
    • 進行debug調試之后,發(fā)現(xiàn)問題就在0.4f和0.6f這個參數(shù)上。分析得出:0.6f表示用戶滑動能夠翻頁的偏移量,所以不難理解,為啥要滑動半屏或者以上了。
  • 也可以修改Touch事件

    • 控制ViewPager的Touch事件,這個基本是萬能的,畢竟是從根源上入手的。你可以在onTouchEvent和onInterceptTouchEvent中做邏輯的判斷。但是比較麻煩。

3.4 修改滑動速度

  • 使用viewPager進行滑動時,如果通過手指滑動來進行的話,可以根據(jù)手指滑動的距離來實現(xiàn),但是如果通過setCurrentItem函數(shù)來實現(xiàn)的話,則會發(fā)現(xiàn)直接閃過去的,會出現(xiàn)一下刷屏。想要通過使用setCurrentItem函數(shù)來進行viewpager的滑動,并且需要有過度滑動的動畫,那么,該如何做呢?

  • 具體可以分析setCurrentItem源碼的邏輯,然后會看到scrollToItem方法,這個特別重要,主要是處理滾動過程中的邏輯。最主要關心的也是smoothScrollTo函數(shù),這個函數(shù)中,可以看到具體執(zhí)行滑動的其實就一句話,就是mScroller.startScroll(sx,sy,dx,dy,duration),則可以看到,是mScroller這個對象進行滑動的。那么想要改變它的屬性,則可以通過反射來實現(xiàn)。

  • 代碼如下所示,如果是手指觸摸滑動,則可以加快一點滑動速率,當然滑動持續(xù)時間你可以自己設置。通過自己自定義滑動的時間,就可以控制滑動的速度。

    @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實現(xiàn)

4.1 自定義LayoutManager

  • 自定義LayoutManager,并且繼承LinearLayoutManager,這樣就得到一個可以水平排向或者豎向排向的布局策略。如果你接觸過SnapHelper應該了解一下LinearSnapHelper和PagerSnapHelper這兩個子類類,LinearSnapHelper可以實現(xiàn)讓列表的Item居中顯示的效果,PagerSnapHelper就可以做到一次滾動一個item顯示的效果。

  • 重寫onChildViewAttachedToWindow方法,在RecyclerView中,當Item添加進來了調用這個方法。這個方法相當于是把view添加到window時候調用的,也就是說它比draw方法先執(zhí)行,可以做一些初始化相關的操作。

    /**
     * 該方法必須調用
     * @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 添加滑動監(jiān)聽

  • 涉及到一次滑動一頁視頻,那么肯定會有視頻初始化和釋放的功能。那么思考一下哪里來開始播放視頻和在哪里釋放視頻?不要著急,要監(jiān)聽滑動到哪頁,需要我們重寫onScrollStateChanged()函數(shù),這里面有三種狀態(tài):SCROLL_STATE_IDLE(空閑),SCROLL_STATE_DRAGGING(拖動),SCROLL_STATE_SETTLING(要移動到最后位置時)。

  • 我們需要的就是RecyclerView停止時的狀態(tài),我們就可以拿到這個View的Position,注意這里還有一個問題,當你通過這個position去拿Item會報錯,這里涉及到RecyclerView的緩存機制,自己去腦補~~。打印Log,你會發(fā)現(xiàn)RecyclerView.getChildCount()一直為1或者會出現(xiàn)為2的情況。來實現(xiàn)一個接口然后通過接口把狀態(tài)傳遞出去。

  • 自定義監(jiān)聽listener事件

    public interface OnPagerListener {
    
        /**
         * 初始化完成
         */
        void onInitComplete();
    
        /**
         * 釋放的監(jiān)聽
         * @param isNext                    是否下一個
         * @param position                  索引
         */
        void onPageRelease(boolean isNext,int position);
    
        /***
         * 選中的監(jiān)聽以及判斷是否滑動到底部
         * @param position                  索引
         * @param isBottom                  是否到了底部
         */
        void onPageSelected(int position,boolean isBottom);
    }
    
    
  • 獲取到RecyclerView空閑時選中的Item,重寫LinearLayoutManager的onScrollStateChanged方法

    /**
     * 滑動狀態(tài)的改變
     * 緩慢拖拽-> SCROLL_STATE_DRAGGING
     * 快速滾動-> SCROLL_STATE_SETTLING
     * 空閑狀態(tài)-> SCROLL_STATE_IDLE
     * @param state                         狀態(tài)
     */
    @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 監(jiān)聽頁面是否滾動

  • 這里有兩個方法scrollHorizontallyBy()和scrollVerticallyBy()可以拿到滑動偏移量,可以判斷滑動方向。

    /**
     * 監(jiān)聽豎直方向的相對偏移量
     * @param dy                                y軸滾動值
     * @param recycler                          recycler
     * @param state                             state滾動狀態(tài)
     * @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);
    }
    
    /**
     * 監(jiān)聽水平方向的相對偏移量
     * @param dx                                x軸滾動值
     * @param recycler                          recycler
     * @param state                             state滾動狀態(tài)
     * @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

  • 列表的選中監(jiān)聽好了,我們就看看什么時候釋放視頻的資源,第二步中的三種狀態(tài),去打印getChildCount()的日志,你會發(fā)現(xiàn)getChildCount()在SCROLL_STATE_DRAGGING會為1,SCROLL_STATE_SETTLING為2,SCROLL_STATE_IDLE有時為1,有時為2,還是RecyclerView的緩存機制O(∩∩)O,這里不會去贅述緩存機制,要做的是要知道在什么時候去做釋放視頻的操作,還要分清是釋放上一頁還是下一頁。

    private RecyclerView.OnChildAttachStateChangeListener mChildAttachStateChangeListener =
            new RecyclerView.OnChildAttachStateChangeListener() {
        /**
         * 第一次進入界面的監(jiān)聽,可以做初始化方面的操作
         * @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監(jiān)聽事件,如下所示。這里注意需要在頁面銷毀的時候移除listener監(jiān)聽事件。

    /**
     * attach到window窗口時,該方法必須調用
     * @param recyclerView                          recyclerView
     */
    @Override
    public void onAttachedToWindow(RecyclerView recyclerView) {
        //這里省略部分代碼
        mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);
    }
    
    /**
     * 銷毀的時候調用該方法,需要移除監(jiān)聽事件
     * @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.優(yōu)化點詳談

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的監(jiān)聽接口。然后通過setupCallbacks()設置監(jiān)聽器,如果當前RecyclerView已經(jīng)設置了OnFlingListener,會拋出一個狀態(tài)異常。那么這個如何復現(xiàn)了,很容易,你初始化多次就可以看到這個bug。

  • 建議手動捕獲一下該異常,代碼設置如下所示。源碼中判斷了,如果onFlingListener已經(jīng)存在的話,再次設置就直接拋出異常,那么這里可以增強一下邏輯判斷,ok,那么問題便解決呢!

    try {
        //attachToRecyclerView源碼上的方法可能會拋出IllegalStateException異常,這里手動捕獲一下
        RecyclerView.OnFlingListener onFlingListener = mRecyclerView.getOnFlingListener();
        //源碼中判斷了,如果onFlingListener已經(jīng)存在的話,再次設置就直接拋出異常,那么這里可以判斷一下
        if (onFlingListener==null){
            mPagerSnapHelper.attachToRecyclerView(mRecyclerView);
        }
    } catch (IllegalStateException e){
        e.printStackTrace();
    }
    
    

5.3 自定義LayoutManager注意點

  • 網(wǎng)上有人已經(jīng)寫了一篇自定義LayoutManager實現(xiàn)抖音的效果的博客,我自己也很仔細看了這篇文章。不過我覺得有幾個注意要點,因為要用到線上app,則一定要盡可能減少崩潰率……
  • 通過SnapHelper調用findSnapView方法,得到的view,一定要增加非空判斷邏輯,否則很容易造成崩潰。
  • 在監(jiān)聽滾動位移scrollVerticallyBy的時候,注意要增加判斷,就是getChildCount()如果為0時,則需要返回0。
  • 在onDetachedFromWindow調用的時候,可以把listener監(jiān)聽事件給remove掉。

5.4 視頻播放邏輯優(yōu)化

  • 從前臺切到后臺,當視頻正在播放或者正在緩沖時,調用方法可以設置暫停視頻。銷毀頁面,釋放,內部的播放器被釋放掉,同時如果在全屏、小窗口模式下都會退出。從后臺切換到前臺,當視頻暫停時或者緩沖暫停時,調用該方法重新開啟視頻播放。具體視頻播放代碼設置如下,具體更加詳細內容可以看我封裝的視頻播放器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 視頻邏輯充分解藕

  • 實際開發(fā)中,翻頁肯定會涉及到視頻的初始化和銷毀的邏輯。首先要保證視頻只有唯一一個播放,滑動到分頁一半,總不可能讓兩個頁面都播放視頻吧,所以需要保證視頻VideoPlayer是一個單利對象,這樣就可以保證唯一性呢!接著,不管是在recyclerView還是ViewPager中,當頁面處于不可見被銷毀或者view被回收的階段,這個時候需要把視頻資源銷毀,盡量視頻播放功能封裝起來,然后在頁面不同狀態(tài)調用方法即可。
  • 當然,實際app中,視頻播放頁面,還有一些點贊,評論,分享,查看作者等等很多其他功能。那么這些都是要請求接口的,還有滑動分頁的功能,當滑動到最后某一頁時候拉取下一個視頻集合數(shù)據(jù)等業(yè)務邏輯。視頻播放功能這塊,因為功能比較復雜,因此封裝一下比較好。盡量做到視頻功能解藕!關于視頻封裝庫,可以看我之前寫的一個庫,視頻播放器。

5.6 翻頁卡頓優(yōu)化分析

  • 如果是使用recyclerView實現(xiàn)滑動翻頁效果,那么為了提高使用體驗效果。則可以注意:1.在onBindViewHolder中不要做耗時操作,2.視頻滑動翻頁的布局固定高度,避免重復計算高度RecyclerView.setHasFixedSize(true),3.關于分頁拉取數(shù)據(jù)注意,建議一次拉下10條數(shù)據(jù)(這個也可以和服務端協(xié)定自定義數(shù)量),而不要滑動一頁加載下一頁的數(shù)據(jù)。

5.7 上拉很快翻頁黑屏

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

作者:飛魚_9d08
鏈接:http://www.itdecent.cn/p/53e4a1c0bd62

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容