仿抖音上下滑動(dòng)分頁(yè)視頻

轉(zhuǎn)自

http://www.reibang.com/p/53e4a1c0bd62

仿抖音上下滑動(dòng)分頁(yè)視頻

目錄介紹

01.先來(lái)看一下需求

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

2.1 使用ViewPager

2.2 使用RecyclerView

03.用ViewPager實(shí)現(xiàn)

3.1 自定義ViewPager

3.2 ViewPager和Fragment

3.3 修改滑動(dòng)距離翻頁(yè)

3.4 修改滑動(dòng)速度

04.用RecyclerView實(shí)現(xiàn)

4.1 自定義LayoutManager

4.2 添加滑動(dòng)監(jiān)聽(tīng)

4.3 監(jiān)聽(tīng)頁(yè)面是否滾動(dòng)

4.4 attach和Detached

05.優(yōu)化點(diǎn)詳談

5.1 ViewPager改變滑動(dòng)速率

5.2 PagerSnapHelper注意點(diǎn)

5.3 自定義LayoutManager注意點(diǎn)

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

5.5 視頻邏輯充分解藕

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

5.7 上拉很快翻頁(yè)黑屏

00.先看一下效果圖

01.先來(lái)看一下需求

項(xiàng)目中的視頻播放却桶,要求實(shí)現(xiàn)抖音那種豎直方向一次滑動(dòng)一頁(yè)的效果。滑動(dòng)要流暢不卡頓,并且手動(dòng)觸摸滑動(dòng)超過(guò)1/2的時(shí)候松開(kāi)可以滑動(dòng)下一頁(yè),沒(méi)有超過(guò)1/2返回原頁(yè)。

手指拖動(dòng)頁(yè)面滑動(dòng),只要沒(méi)有切換到其他的頁(yè)面,視頻都是在播放的漫试。切換了頁(yè)面,上一個(gè)視頻銷(xiāo)毀碘赖,該頁(yè)面則開(kāi)始初始化播放驾荣。

切換頁(yè)面的時(shí)候過(guò)渡效果要自然,避免出現(xiàn)閃屏普泡。具體的滑動(dòng)效果播掷,可以直接參考抖音……

開(kāi)源庫(kù)地址:github.com/yangchong21…

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

2.1 使用ViewPager

使用ViewPager實(shí)現(xiàn)豎直方法上下切換視頻分析

1.最近項(xiàng)目需求中有用到需要在ViewPager中播放視頻,就是豎直方法上下滑動(dòng)切換視頻撼班,視頻是網(wǎng)絡(luò)視頻叮趴,最開(kāi)始的實(shí)現(xiàn)思路是ViewPager中根據(jù)當(dāng)前item位置去初始化SurfaceView,同時(shí)銷(xiāo)毀時(shí)根據(jù)item的位置移除SurfaceView权烧。

2.上面那種方式確實(shí)是可以實(shí)現(xiàn)的眯亦,但是存在2個(gè)問(wèn)題伤溉,第一,MediaPlayer的生命周期不容易控制并且存在內(nèi)存泄漏問(wèn)題妻率。第二乱顾,連續(xù)三個(gè)item都是視頻時(shí),來(lái)回滑動(dòng)的過(guò)程中發(fā)現(xiàn)會(huì)出現(xiàn)上個(gè)視頻的最后一幀畫(huà)面的bug宫静。

3.未提升用戶體驗(yàn)走净,視頻播放器初始化完成前上面會(huì)覆蓋有該視頻的第一幀圖片,但是發(fā)現(xiàn)存在第一幀圖片與視頻第一幀信息不符的情況,后面會(huì)通過(guò)代碼給出解決方案孤里。

大概的實(shí)現(xiàn)思路是這樣

1.需要自定義一個(gè)豎直方向滑動(dòng)的ViewPager伏伯,注意這個(gè)特別重要。

2.一次滑動(dòng)一頁(yè)捌袜,建議采用ViewPager+FragmentStatePagerAdapter+Fragment方式來(lái)做说搅,后面會(huì)詳細(xì)說(shuō)。

3.在fragment中處理視頻的初始化虏等,播放和銷(xiāo)毀邏輯等邏輯弄唧。

4.由于一個(gè)頁(yè)面需要?jiǎng)?chuàng)建一個(gè)fragment,注意性能和滑動(dòng)流暢度這塊需要分析和探討霍衫。

不太建議使用ViewPager

1.ViewPager 自帶的滑動(dòng)效果完全滿足場(chǎng)景候引,而且支持Fragment和View等UI綁定,只要對(duì)布局和觸摸事件部分作一些修改敦跌,就可以把橫向的 ViewPager 改成豎向澄干。

2.但是沒(méi)有復(fù)用是個(gè)最致命的問(wèn)題。在onLayout方法中柠傍,所有子View會(huì)實(shí)例化并一字排開(kāi)在布局上麸俘。當(dāng)Item數(shù)量很大時(shí),將會(huì)是很大的性能浪費(fèi)携兵。

3.其次是可見(jiàn)性判斷的問(wèn)題。很多人會(huì)以為 Fragment 在 onResume 的時(shí)候就是可見(jiàn)的搂誉,而 ViewPager 中的 Fragment 就是個(gè)反例徐紧,尤其是多個(gè) ViewPager 嵌套時(shí),會(huì)同時(shí)有多個(gè)父 Fragment 多個(gè)子 Fragment 處于 onResume 的狀態(tài)炭懊,卻只有其中一個(gè)是可見(jiàn)的并级。除非放棄 ViewPager 的預(yù)加載機(jī)制。在頁(yè)面內(nèi)容曝光等重要的數(shù)據(jù)上報(bào)時(shí)侮腹,就需要判斷很多條件:onResumed 嘲碧、 setUserVisibleHint 、 setOnPageChangeListener 等父阻。

2.2 使用RecyclerView

使用RecyclerView實(shí)現(xiàn)樹(shù)枝方向上下切換視頻分析

1.首先RecyclerView它設(shè)置豎直方向滑動(dòng)是十分簡(jiǎn)單的愈涩,同時(shí)關(guān)于item的四級(jí)緩存也做好了處理望抽,而且滑動(dòng)的效果相比ViewPager要好一些。

2.滑動(dòng)事件處理比viewPager好履婉,即使你外層嵌套了下拉刷新上拉加載的布局煤篙,也不影響后期事件沖突處理,詳細(xì)可以看demo案例毁腿。

大概的實(shí)現(xiàn)思路是這樣

1.自定義一個(gè)LinearLayoutManager辑奈,重寫(xiě)onScrollStateChanged方法,注意是拿到滑動(dòng)狀態(tài)已烤。

2.一次滑動(dòng)切換一個(gè)頁(yè)面鸠窗,可以使用PagerSnapHelper來(lái)實(shí)現(xiàn),十分方便簡(jiǎn)單胯究。

3.在recyclerView對(duì)應(yīng)的adapter中稍计,在onCreateViewHolder初始化視頻操作,同時(shí)當(dāng)onViewRecycled時(shí)唐片,銷(xiāo)毀視頻資源丙猬。

4.添加自定義回調(diào)接口,在滾動(dòng)頁(yè)面和attch费韭,detach的時(shí)候茧球,定義初始化,頁(yè)面銷(xiāo)毀等方法星持,暴露給開(kāi)發(fā)者抢埋。

03.用ViewPager實(shí)現(xiàn)

3.1 自定義ViewPager

代碼如下所示,這里省略了不少的代碼督暂,具體可以看項(xiàng)目中的代碼揪垄。

/**

* <pre>

*? ? @author 楊充

*? ? blog? : https://github.com/yangchong211

*? ? time? : 2019/6/20

*? ? desc? : 自定義ViewPager,主要是處理邊界極端情況

*? ? revise:

* </pre>

*/publicclassVerticalViewPagerextendsViewPager{privatebooleanisVertical=false;privatelongmRecentTouchTime;publicVerticalViewPager(@NonNullContextcontext){super(context);}publicVerticalViewPager(@NonNullContextcontext,@NullableAttributeSetattrs){super(context,attrs);}privatevoidinit(){setPageTransformer(true,newHorizontalVerticalPageTransformer());setOverScrollMode(OVER_SCROLL_NEVER);}publicbooleanisVertical(){returnisVertical;}publicvoidsetVertical(booleanvertical){isVertical=vertical;init();}privateclassHorizontalVerticalPageTransformerimplementsPageTransformer{privatestaticfinalfloatMIN_SCALE=0.25f;@OverridepublicvoidtransformPage(@NonNullViewpage,floatposition){if(isVertical){if(position<-1){page.setAlpha(0);}elseif(position<=1){page.setAlpha(1);// Counteract the default slide transitionfloatxPosition=page.getWidth()*-position;page.setTranslationX(xPosition);//set Y position to swipe in from topfloatyPosition=position*page.getHeight();page.setTranslationY(yPosition);}else{page.setAlpha(0);}}else{intpageWidth=page.getWidth();if(position<-1){// [-Infinity,-1)// This page is way off-screen to the left.page.setAlpha(0);}elseif(position<=0){// [-1,0]// Use the default slide transition when moving to the left pagepage.setAlpha(1);page.setTranslationX(0);page.setScaleX(1);page.setScaleY(1);}elseif(position<=1){// (0,1]// Fade the page out.page.setAlpha(1-position);// Counteract the default slide transitionpage.setTranslationX(pageWidth*-position);page.setTranslationY(0);// Scale the page down (between MIN_SCALE and 1)floatscaleFactor=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軸的移動(dòng)距離

? ? * @param event 獲取事件類型的封裝類MotionEvent

? ? */privateMotionEventswapXY(MotionEventevent){//獲取寬高floatwidth=getWidth();floatheight=getHeight();//將Y軸的移動(dòng)距離轉(zhuǎn)變成X軸的移動(dòng)距離floatswappedX=(event.getY()/height)*width;//將X軸的移動(dòng)距離轉(zhuǎn)變成Y軸的移動(dòng)距離floatswappedY=(event.getX()/width)*height;//重設(shè)event的位置event.setLocation(swappedX,swappedY);returnevent;}@OverridepublicbooleanonInterceptTouchEvent(MotionEventev){mRecentTouchTime=System.currentTimeMillis();if(getCurrentItem()==0&&getChildCount()==0){returnfalse;}if(isVertical){booleanintercepted=super.onInterceptTouchEvent(swapXY(ev));swapXY(ev);// return touch coordinates to original reference frame for any child viewsreturnintercepted;}else{returnsuper.onInterceptTouchEvent(ev);}}@OverridepublicbooleanonTouchEvent(MotionEventev){if(getCurrentItem()==0&&getChildCount()==0){returnfalse;}if(isVertical){returnsuper.onTouchEvent(swapXY(ev));}else{returnsuper.onTouchEvent(ev);}}}

3.2 ViewPager和Fragment

采用了ViewPager+FragmentStatePagerAdapter+Fragment來(lái)處理逻翁。為何選擇使用FragmentStatePagerAdapter饥努,主要是因?yàn)槭褂?FragmentStatePagerAdapter更省內(nèi)存,但是銷(xiāo)毀后新建也是需要時(shí)間的八回。一般情況下酷愧,如果你是用于ViewPager展示數(shù)量特別多的條目時(shí),那么建議使用FragmentStatePagerAdapter缠诅。關(guān)于PagerAdapter的深度解析溶浴,可以我這篇文章:PagerAdapter深度解析和實(shí)踐優(yōu)化

在activity中的代碼如下所示

privatevoidinitViewPager(){List<Video>list=newArrayList<>();ArrayList<Fragment>fragments=newArrayList<>();for(inta=0;a<DataProvider.VideoPlayerList.length;a++){Video video=newVideo(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=newMyPagerAdapter(fragments,supportFragmentManager);vp.setAdapter(myPagerAdapter);}classMyPagerAdapterextends FragmentStatePagerAdapter{privateArrayList<Fragment>list;publicMyPagerAdapter(ArrayList<Fragment>list,FragmentManager fm){super(fm);this.list=list;}@OverridepublicFragmentgetItem(inti){returnlist.get(i);}@OverridepublicintgetCount(){returnlist!=null?list.size():0;}}

那么在fragment中如何處理呢?關(guān)于視頻播放器管引,這里可以看我封裝的庫(kù)士败,視頻lib

publicclassVideoFragmentextendsFragment{publicVideoPlayer videoPlayer;privateString url;privateint index;@OverridepublicvoidonStop(){super.onStop();VideoPlayerManager.instance().releaseVideoPlayer();}publicstaticFragmentnewInstant(String url){VideoFragment videoFragment=newVideoFragment();Bundle bundle=newBundle();bundle.putString("url",url);videoFragment.setArguments(bundle);returnvideoFragment;}@OverridepublicvoidonCreate(@Nullable Bundle savedInstanceState){super.onCreate(savedInstanceState);Bundle arguments=getArguments();if(arguments!=null){url=arguments.getString("url");}}@Nullable? ? @OverridepublicViewonCreateView(@NonNull LayoutInflater inflater,@Nullable ViewGroup container,@Nullable Bundle savedInstanceState){View view=inflater.inflate(R.layout.fragment_video,container,false);returnview;}@OverridepublicvoidonViewCreated(@NonNull View view,@Nullable Bundle savedInstanceState){super.onViewCreated(view,savedInstanceState);videoPlayer=view.findViewById(R.id.video_player);}@OverridepublicvoidonActivityCreated(@Nullable Bundle savedInstanceState){super.onActivityCreated(savedInstanceState);Log.d("初始化操作","------"+index++);VideoPlayerController controller=newVideoPlayerController(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 修改滑動(dòng)距離翻頁(yè)

需求要求必須手動(dòng)觸摸滑動(dòng)超過(guò)1/2的時(shí)候松開(kāi)可以滑動(dòng)下一頁(yè),沒(méi)有超過(guò)1/2返回原頁(yè)褥伴,首先肯定是重寫(xiě)viewpager谅将,只能從源碼下手漾狼。經(jīng)過(guò)分析,源碼滑動(dòng)的邏輯處理在此處戏自,truncator的屬性代表判斷的比例值邦投!

這個(gè)方法會(huì)在切頁(yè)的時(shí)候重定向Page,比如從第一個(gè)頁(yè)面滑動(dòng)擅笔,結(jié)果沒(méi)有滑動(dòng)到第二個(gè)頁(yè)面揩页,而是又返回到第一個(gè)頁(yè)面描融,那個(gè)這個(gè)page會(huì)有重定向的功能

privateintdetermineTargetPage(intcurrentPage,floatpageOffset,intvelocity,intdeltaX){inttargetPage;if(Math.abs(deltaX)>this.mFlingDistance&&Math.abs(velocity)>this.mMinimumVelocity){targetPage=velocity>0?currentPage:currentPage+1;}else{floattruncator=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));}returntargetPage;}

determineTargetPage這個(gè)方法就是計(jì)算接下來(lái)要滑到哪一頁(yè)。這個(gè)方法調(diào)用是在MotionEvent.ACTION_UP這個(gè)事件下,先說(shuō)下參數(shù)意思:

currentPage:當(dāng)前ViewPager顯示的頁(yè)面

pageOffset:用戶滑動(dòng)的頁(yè)面偏移量

velocity: 滑動(dòng)速率

deltaX: X方向移動(dòng)的距離

進(jìn)行debug調(diào)試之后凌彬,發(fā)現(xiàn)問(wèn)題就在0.4f和0.6f這個(gè)參數(shù)上煌往。分析得出:0.6f表示用戶滑動(dòng)能夠翻頁(yè)的偏移量颜武,所以不難理解妙痹,為啥要滑動(dòng)半屏或者以上了。

也可以修改Touch事件

控制ViewPager的Touch事件庐橙,這個(gè)基本是萬(wàn)能的假勿,畢竟是從根源上入手的。你可以在onTouchEvent和onInterceptTouchEvent中做邏輯的判斷态鳖。但是比較麻煩转培。

3.4 修改滑動(dòng)速度

使用viewPager進(jìn)行滑動(dòng)時(shí),如果通過(guò)手指滑動(dòng)來(lái)進(jìn)行的話浆竭,可以根據(jù)手指滑動(dòng)的距離來(lái)實(shí)現(xiàn)浸须,但是如果通過(guò)setCurrentItem函數(shù)來(lái)實(shí)現(xiàn)的話,則會(huì)發(fā)現(xiàn)直接閃過(guò)去的邦泄,會(huì)出現(xiàn)一下刷屏删窒。想要通過(guò)使用setCurrentItem函數(shù)來(lái)進(jìn)行viewpager的滑動(dòng),并且需要有過(guò)度滑動(dòng)的動(dòng)畫(huà)顺囊,那么肌索,該如何做呢?

具體可以分析setCurrentItem源碼的邏輯特碳,然后會(huì)看到scrollToItem方法诚亚,這個(gè)特別重要,主要是處理滾動(dòng)過(guò)程中的邏輯测萎。最主要關(guān)心的也是smoothScrollTo函數(shù)亡电,這個(gè)函數(shù)中届巩,可以看到具體執(zhí)行滑動(dòng)的其實(shí)就一句話硅瞧,就是mScroller.startScroll(sx,sy,dx,dy,duration),則可以看到恕汇,是mScroller這個(gè)對(duì)象進(jìn)行滑動(dòng)的腕唧。那么想要改變它的屬性或辖,則可以通過(guò)反射來(lái)實(shí)現(xiàn)。

代碼如下所示枣接,如果是手指觸摸滑動(dòng)颂暇,則可以加快一點(diǎn)滑動(dòng)速率,當(dāng)然滑動(dòng)持續(xù)時(shí)間你可以自己設(shè)置但惶。通過(guò)自己自定義滑動(dòng)的時(shí)間耳鸯,就可以控制滑動(dòng)的速度。

@TargetApi(Build.VERSION_CODES.KITKAT)publicvoidsetAnimationDuration(finalintduring){try{// viewPager平移動(dòng)畫(huà)事件Field mField=ViewPager.class.getDeclaredField("mScroller");mField.setAccessible(true);// 動(dòng)畫(huà)效果與ViewPager的一致Interpolator interpolator=newInterpolator(){@OverridepublicfloatgetInterpolation(floatt){t-=1.0f;returnt*t*t*t*t+1.0f;}};Scroller mScroller=newScroller(getContext(),interpolator){finalinttime=2000;@OverridepublicvoidstartScroll(intx,inty,intdx,intdy,intduration){// 如果手工滾動(dòng),則加速滾動(dòng)if(System.currentTimeMillis()-mRecentTouchTime>time){duration=during;}else{duration/=2;}super.startScroll(x,y,dx,dy,duration);}@OverridepublicvoidstartScroll(intx,inty,intdx,intdy){super.startScroll(x,y,dx,dy,during);}};mField.set(this,mScroller);}catch(NoSuchFieldException|IllegalAccessException|IllegalArgumentException e){e.printStackTrace();}}

04.用RecyclerView實(shí)現(xiàn)

4.1 自定義LayoutManager

自定義LayoutManager膀曾,并且繼承LinearLayoutManager县爬,這樣就得到一個(gè)可以水平排向或者豎向排向的布局策略。如果你接觸過(guò)SnapHelper應(yīng)該了解一下LinearSnapHelper和PagerSnapHelper這兩個(gè)子類類添谊,LinearSnapHelper可以實(shí)現(xiàn)讓列表的Item居中顯示的效果财喳,PagerSnapHelper就可以做到一次滾動(dòng)一個(gè)item顯示的效果。

重寫(xiě)onChildViewAttachedToWindow方法斩狱,在RecyclerView中耳高,當(dāng)Item添加進(jìn)來(lái)了調(diào)用這個(gè)方法。這個(gè)方法相當(dāng)于是把view添加到window時(shí)候調(diào)用的所踊,也就是說(shuō)它比draw方法先執(zhí)行泌枪,可以做一些初始化相關(guān)的操作。

/**

* 該方法必須調(diào)用

* @param recyclerView? ? ? ? ? ? ? ? ? ? ? ? ? recyclerView

*/@OverridepublicvoidonAttachedToWindow(RecyclerViewrecyclerView){if(recyclerView==null){thrownewIllegalArgumentException("The attach RecycleView must not null!!");}super.onAttachedToWindow(recyclerView);this.mRecyclerView=recyclerView;if(mPagerSnapHelper==null){init();}mPagerSnapHelper.attachToRecyclerView(mRecyclerView);mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);}

4.2 添加滑動(dòng)監(jiān)聽(tīng)

涉及到一次滑動(dòng)一頁(yè)視頻污筷,那么肯定會(huì)有視頻初始化和釋放的功能工闺。那么思考一下哪里來(lái)開(kāi)始播放視頻和在哪里釋放視頻?不要著急瓣蛀,要監(jiān)聽(tīng)滑動(dòng)到哪頁(yè)陆蟆,需要我們重寫(xiě)onScrollStateChanged()函數(shù),這里面有三種狀態(tài):SCROLL_STATE_IDLE(空閑)惋增,SCROLL_STATE_DRAGGING(拖動(dòng))叠殷,SCROLL_STATE_SETTLING(要移動(dòng)到最后位置時(shí))。

我們需要的就是RecyclerView停止時(shí)的狀態(tài)诈皿,我們就可以拿到這個(gè)View的Position林束,注意這里還有一個(gè)問(wèn)題,當(dāng)你通過(guò)這個(gè)position去拿Item會(huì)報(bào)錯(cuò)稽亏,這里涉及到RecyclerView的緩存機(jī)制壶冒,自己去腦補(bǔ)~~。打印Log,你會(huì)發(fā)現(xiàn)RecyclerView.getChildCount()一直為1或者會(huì)出現(xiàn)為2的情況截歉。來(lái)實(shí)現(xiàn)一個(gè)接口然后通過(guò)接口把狀態(tài)傳遞出去胖腾。

自定義監(jiān)聽(tīng)listener事件

publicinterfaceOnPagerListener{/**

? ? * 初始化完成

? ? */voidonInitComplete();/**

? ? * 釋放的監(jiān)聽(tīng)

? ? * @param isNext? ? ? ? ? ? ? ? ? ? 是否下一個(gè)

? ? * @param position? ? ? ? ? ? ? ? ? 索引

? ? */voidonPageRelease(booleanisNext,intposition);/***

? ? * 選中的監(jiān)聽(tīng)以及判斷是否滑動(dòng)到底部

? ? * @param position? ? ? ? ? ? ? ? ? 索引

? ? * @param isBottom? ? ? ? ? ? ? ? ? 是否到了底部

? ? */voidonPageSelected(intposition,booleanisBottom);}

獲取到RecyclerView空閑時(shí)選中的Item,重寫(xiě)LinearLayoutManager的onScrollStateChanged方法

/**

* 滑動(dòng)狀態(tài)的改變

* 緩慢拖拽-> SCROLL_STATE_DRAGGING

* 快速滾動(dòng)-> SCROLL_STATE_SETTLING

* 空閑狀態(tài)-> SCROLL_STATE_IDLE

* @param state? ? ? ? ? ? ? ? ? ? ? ? 狀態(tài)

*/@OverridepublicvoidonScrollStateChanged(int state){switch(state){caseRecyclerView.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;caseRecyclerView.SCROLL_STATE_DRAGGING:View viewDrag=mPagerSnapHelper.findSnapView(this);if(viewDrag!=null){int positionDrag=getPosition(viewDrag);}break;caseRecyclerView.SCROLL_STATE_SETTLING:View viewSettling=mPagerSnapHelper.findSnapView(this);if(viewSettling!=null){int positionSettling=getPosition(viewSettling);}break;default:break;}}

4.3 監(jiān)聽(tīng)頁(yè)面是否滾動(dòng)

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

/**

* 監(jiān)聽(tīng)豎直方向的相對(duì)偏移量

* @param dy? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? y軸滾動(dòng)值

* @param recycler? ? ? ? ? ? ? ? ? ? ? ? ? recycler

* @param state? ? ? ? ? ? ? ? ? ? ? ? ? ? state滾動(dòng)狀態(tài)

* @return? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? int值

*/@OverridepublicintscrollVerticallyBy(intdy,RecyclerView.Recyclerrecycler,RecyclerView.Statestate){if(getChildCount()==0||dy==0){return0;}this.mDrift=dy;returnsuper.scrollVerticallyBy(dy,recycler,state);}/**

* 監(jiān)聽(tīng)水平方向的相對(duì)偏移量

* @param dx? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? x軸滾動(dòng)值

* @param recycler? ? ? ? ? ? ? ? ? ? ? ? ? recycler

* @param state? ? ? ? ? ? ? ? ? ? ? ? ? ? state滾動(dòng)狀態(tài)

* @return? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? int值

*/@OverridepublicintscrollHorizontallyBy(intdx,RecyclerView.Recyclerrecycler,RecyclerView.Statestate){if(getChildCount()==0||dx==0){return0;}this.mDrift=dx;returnsuper.scrollHorizontallyBy(dx,recycler,state);}

4.4 attach和Detached

列表的選中監(jiān)聽(tīng)好了咸作,我們就看看什么時(shí)候釋放視頻的資源锨阿,第二步中的三種狀態(tài),去打印getChildCount()的日志记罚,你會(huì)發(fā)現(xiàn)getChildCount()在SCROLL_STATE_DRAGGING會(huì)為1墅诡,SCROLL_STATE_SETTLING為2,SCROLL_STATE_IDLE有時(shí)為1桐智,有時(shí)為2末早,還是RecyclerView的緩存機(jī)制O(∩∩)O,這里不會(huì)去贅述緩存機(jī)制说庭,要做的是要知道在什么時(shí)候去做釋放視頻的操作荐吉,還要分清是釋放上一頁(yè)還是下一頁(yè)。

privateRecyclerView.OnChildAttachStateChangeListenermChildAttachStateChangeListener=newRecyclerView.OnChildAttachStateChangeListener(){/**

? ? * 第一次進(jìn)入界面的監(jiān)聽(tīng)口渔,可以做初始化方面的操作

? ? * @param view? ? ? ? ? ? ? ? ? ? ? view

? ? */@OverridepublicvoidonChildViewAttachedToWindow(@NonNullViewview){if(mOnViewPagerListener!=null&&getChildCount()==1){mOnViewPagerListener.onInitComplete();}}/**

? ? * 頁(yè)面銷(xiāo)毀的時(shí)候調(diào)用該方法样屠,可以做銷(xiāo)毀方面的操作

? ? * @param view? ? ? ? ? ? ? ? ? ? ? view

? ? */@OverridepublicvoidonChildViewDetachedFromWindow(@NonNullViewview){if(mDrift>=0){if(mOnViewPagerListener!=null){mOnViewPagerListener.onPageRelease(true,getPosition(view));}}else{if(mOnViewPagerListener!=null){mOnViewPagerListener.onPageRelease(false,getPosition(view));}}}};

哪里添加該listener監(jiān)聽(tīng)事件,如下所示缺脉。這里注意需要在頁(yè)面銷(xiāo)毀的時(shí)候移除listener監(jiān)聽(tīng)事件痪欲。

/**

* attach到window窗口時(shí),該方法必須調(diào)用

* @param recyclerView? ? ? ? ? ? ? ? ? ? ? ? ? recyclerView

*/@OverridepublicvoidonAttachedToWindow(RecyclerViewrecyclerView){//這里省略部分代碼mRecyclerView.addOnChildAttachStateChangeListener(mChildAttachStateChangeListener);}/**

* 銷(xiāo)毀的時(shí)候調(diào)用該方法攻礼,需要移除監(jiān)聽(tīng)事件

* @param view? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? view

* @param recycler? ? ? ? ? ? ? ? ? ? ? ? ? ? ? recycler

*/@OverridepublicvoidonDetachedFromWindow(RecyclerViewview,RecyclerView.Recyclerrecycler){super.onDetachedFromWindow(view,recycler);if(mRecyclerView!=null){mRecyclerView.removeOnChildAttachStateChangeListener(mChildAttachStateChangeListener);}}

05.優(yōu)化點(diǎn)詳談

5.1 ViewPager改變滑動(dòng)速率

可以通過(guò)反射修改屬性业踢,注意,使用反射的時(shí)候礁扮,建議手動(dòng)try-catch知举,避免異常導(dǎo)致崩潰。代碼如下所示:

/**

* 修改滑動(dòng)靈敏度

* @param flingDistance? ? ? ? ? ? ? ? ? ? 滑動(dòng)慣性太伊,默認(rèn)是75

* @param minimumVelocity? ? ? ? ? ? ? ? ? 最小滑動(dòng)值雇锡,默認(rèn)是1200

*/publicvoidsetScrollFling(int flingDistance,int minimumVelocity){try{Field mFlingDistance=ViewPager.class.getDeclaredField("mFlingDistance");mFlingDistance.setAccessible(true);Object o=mFlingDistance.get(this);Log.d("setScrollFling",o.toString());//默認(rèn)值75mFlingDistance.set(this,flingDistance);Field mMinimumVelocity=ViewPager.class.getDeclaredField("mMinimumVelocity");mMinimumVelocity.setAccessible(true);Object o1=mMinimumVelocity.get(this);Log.d("setScrollFling",o1.toString());//默認(rèn)值1200mMinimumVelocity.set(this,minimumVelocity);}catch(Exception e){e.printStackTrace();}}

5.2 PagerSnapHelper注意點(diǎn)

好多時(shí)候會(huì)拋出一個(gè)異常"illegalstateexception an instance of onflinglistener already set".

看SnapHelper源碼attachToRecyclerView(xxx)方法時(shí),可以看到如果recyclerView不為null僚焦,則先destoryCallback()锰提,它作用在于取消之前的RecyclerView的監(jiān)聽(tīng)接口。然后通過(guò)setupCallbacks()設(shè)置監(jiān)聽(tīng)器芳悲,如果當(dāng)前RecyclerView已經(jīng)設(shè)置了OnFlingListener立肘,會(huì)拋出一個(gè)狀態(tài)異常。那么這個(gè)如何復(fù)現(xiàn)了名扛,很容易谅年,你初始化多次就可以看到這個(gè)bug。

建議手動(dòng)捕獲一下該異常肮韧,代碼設(shè)置如下所示融蹂。源碼中判斷了文黎,如果onFlingListener已經(jīng)存在的話,再次設(shè)置就直接拋出異常殿较,那么這里可以增強(qiáng)一下邏輯判斷,ok桩蓉,那么問(wèn)題便解決呢淋纲!

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

5.3 自定義LayoutManager注意點(diǎn)

網(wǎng)上有人已經(jīng)寫(xiě)了一篇自定義LayoutManager實(shí)現(xiàn)抖音的效果的博客业汰,我自己也很仔細(xì)看了這篇文章伙窃。不過(guò)我覺(jué)得有幾個(gè)注意要點(diǎn),因?yàn)橐玫骄€上app样漆,則一定要盡可能減少崩潰率……

通過(guò)SnapHelper調(diào)用findSnapView方法为障,得到的view,一定要增加非空判斷邏輯放祟,否則很容易造成崩潰鳍怨。

在監(jiān)聽(tīng)滾動(dòng)位移scrollVerticallyBy的時(shí)候,注意要增加判斷跪妥,就是getChildCount()如果為0時(shí)鞋喇,則需要返回0。

在onDetachedFromWindow調(diào)用的時(shí)候眉撵,可以把listener監(jiān)聽(tīng)事件給remove掉侦香。

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

從前臺(tái)切到后臺(tái),當(dāng)視頻正在播放或者正在緩沖時(shí)纽疟,調(diào)用方法可以設(shè)置暫停視頻罐韩。銷(xiāo)毀頁(yè)面,釋放污朽,內(nèi)部的播放器被釋放掉伴逸,同時(shí)如果在全屏、小窗口模式下都會(huì)退出膘壶。從后臺(tái)切換到前臺(tái)错蝴,當(dāng)視頻暫停時(shí)或者緩沖暫停時(shí),調(diào)用該方法重新開(kāi)啟視頻播放颓芭。具體視頻播放代碼設(shè)置如下顷锰,具體更加詳細(xì)內(nèi)容可以看我封裝的視頻播放器lib

@OverrideprotectedvoidonStop(){super.onStop();//從前臺(tái)切到后臺(tái),當(dāng)視頻正在播放或者正在緩沖時(shí)亡问,調(diào)用該方法暫停視頻VideoPlayerManager.instance().suspendVideoPlayer();}@OverrideprotectedvoidonDestroy(){super.onDestroy();//銷(xiāo)毀頁(yè)面官紫,釋放肛宋,內(nèi)部的播放器被釋放掉,同時(shí)如果在全屏束世、小窗口模式下都會(huì)退出VideoPlayerManager.instance().releaseVideoPlayer();}@OverridepublicvoidonBackPressed(){//處理返回鍵邏輯酝陈;如果是全屏,則退出全屏毁涉;如果是小窗口沉帮,則退出小窗口if(VideoPlayerManager.instance().onBackPressed()){return;}else{//銷(xiāo)毀頁(yè)面VideoPlayerManager.instance().releaseVideoPlayer();}super.onBackPressed();}@OverrideprotectedvoidonRestart(){super.onRestart();//從后臺(tái)切換到前臺(tái),當(dāng)視頻暫停時(shí)或者緩沖暫停時(shí)贫堰,調(diào)用該方法重新開(kāi)啟視頻播放VideoPlayerManager.instance().resumeVideoPlayer();}

5.5 視頻邏輯充分解藕

實(shí)際開(kāi)發(fā)中穆壕,翻頁(yè)肯定會(huì)涉及到視頻的初始化和銷(xiāo)毀的邏輯。首先要保證視頻只有唯一一個(gè)播放其屏,滑動(dòng)到分頁(yè)一半喇勋,總不可能讓兩個(gè)頁(yè)面都播放視頻吧,所以需要保證視頻VideoPlayer是一個(gè)單利對(duì)象偎行,這樣就可以保證唯一性呢川背!接著,不管是在recyclerView還是ViewPager中蛤袒,當(dāng)頁(yè)面處于不可見(jiàn)被銷(xiāo)毀或者view被回收的階段渗常,這個(gè)時(shí)候需要把視頻資源銷(xiāo)毀,盡量視頻播放功能封裝起來(lái)汗盘,然后在頁(yè)面不同狀態(tài)調(diào)用方法即可皱碘。

當(dāng)然,實(shí)際app中隐孽,視頻播放頁(yè)面癌椿,還有一些點(diǎn)贊,評(píng)論菱阵,分享踢俄,查看作者等等很多其他功能。那么這些都是要請(qǐng)求接口的晴及,還有滑動(dòng)分頁(yè)的功能都办,當(dāng)滑動(dòng)到最后某一頁(yè)時(shí)候拉取下一個(gè)視頻集合數(shù)據(jù)等業(yè)務(wù)邏輯。視頻播放功能這塊虑稼,因?yàn)楣δ鼙容^復(fù)雜琳钉,因此封裝一下比較好。盡量做到視頻功能解藕蛛倦!關(guān)于視頻封裝庫(kù)歌懒,可以看我之前寫(xiě)的一個(gè)庫(kù),視頻播放器溯壶。

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

如果是使用recyclerView實(shí)現(xiàn)滑動(dòng)翻頁(yè)效果及皂,那么為了提高使用體驗(yàn)效果甫男。則可以注意:1.在onBindViewHolder中不要做耗時(shí)操作,2.視頻滑動(dòng)翻頁(yè)的布局固定高度验烧,避免重復(fù)計(jì)算高度RecyclerView.setHasFixedSize(true)板驳,3.關(guān)于分頁(yè)拉取數(shù)據(jù)注意,建議一次拉下10條數(shù)據(jù)(這個(gè)也可以和服務(wù)端協(xié)定自定義數(shù)量)碍拆,而不要滑動(dòng)一頁(yè)加載下一頁(yè)的數(shù)據(jù)若治。

5.7 上拉很快翻頁(yè)黑屏

因?yàn)樵O(shè)置視頻的背景顏色為黑色,我看了好多播放器初始化的時(shí)候倔监,都是這樣的。因?yàn)樽詈?jiǎn)單的解決辦法菌仁,就是給它加個(gè)封面浩习,設(shè)置封面的背景即可。

作者:飛魚(yú)_9d08

鏈接:http://www.reibang.com/p/53e4a1c0bd62

來(lái)源:簡(jiǎn)書(shū)

著作權(quán)歸作者所有济丘。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)谱秽,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末摹迷,一起剝皮案震驚了整個(gè)濱河市疟赊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌峡碉,老刑警劉巖近哟,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鲫寄,居然都是意外死亡吉执,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)地来,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)戳玫,“玉大人,你說(shuō)我怎么就攤上這事未斑」舅蓿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵蜡秽,是天一觀的道長(zhǎng)府阀。 經(jīng)常有香客問(wèn)我,道長(zhǎng)芽突,這世上最難降的妖魔是什么肌似? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮诉瓦,結(jié)果婚禮上川队,老公的妹妹穿的比我還像新娘力细。我一直安慰自己,他們只是感情好固额,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布眠蚂。 她就那樣靜靜地躺著,像睡著了一般斗躏。 火紅的嫁衣襯著肌膚如雪逝慧。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天啄糙,我揣著相機(jī)與錄音笛臣,去河邊找鬼。 笑死隧饼,一個(gè)胖子當(dāng)著我的面吹牛沈堡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播燕雁,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼诞丽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了拐格?” 一聲冷哼從身側(cè)響起僧免,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捏浊,沒(méi)想到半個(gè)月后懂衩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡金踪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年勃痴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片热康。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沛申,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姐军,到底是詐尸還是另有隱情铁材,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布奕锌,位于F島的核電站著觉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏惊暴。R本人自食惡果不足惜饼丘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辽话。 院中可真熱鬧肄鸽,春花似錦卫病、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至逮诲,卻和暖如春帜平,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梅鹦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工裆甩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人齐唆。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓嗤栓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蝶念。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抛腕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355