自定義View:實(shí)現(xiàn)RecyclerView的item添加懸浮層的效果

*本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布

前言

又到了年底,好多的事情都要收尾,今天分享一個(gè)RecyclerView的包裝擴(kuò)展類,幫助大家實(shí)現(xiàn)添加Item的浮層的效果。

首先看一下效果圖:


在這里插入圖片描述

有人會(huì)問(wèn)我:老鐵,你實(shí)現(xiàn)的這個(gè)東西有個(gè)卵用?如果你沒(méi)看明白,我們?cè)倏匆粡埛浅J煜さ膽?yīng)用場(chǎng)景:


在這里插入圖片描述

正文

記得2年前在創(chuàng)業(yè)公司的時(shí)候,正是短視頻火爆的高峰期,公司也做了一款二次元的短視頻app,很可惜還沒(méi)上線就被腰斬了。當(dāng)時(shí)就要求做了這個(gè)效果,雖然實(shí)現(xiàn)了,但是實(shí)現(xiàn)的方案實(shí)在是太low了。今天也是彌補(bǔ)了這個(gè)遺憾。

實(shí)現(xiàn)思路一

在每一個(gè)Item中放入一個(gè)VideoPlayer,但是缺點(diǎn)太多:

可控性差:控制播放哪一個(gè)位置的視頻,視頻的停止和播放等等,都需要寫大量的邏輯;
內(nèi)存風(fēng)險(xiǎn)高:播放器還是很占用內(nèi)存的,一個(gè)頁(yè)面持有多個(gè)播放器,很容易導(dǎo)致內(nèi)存泄露;
可維護(hù)性差:adapter中不可避免的需要插入播放相關(guān)的內(nèi)容,耦合性強(qiáng),代碼臃腫,后期不易維護(hù)。

當(dāng)然這個(gè)方案也有優(yōu)點(diǎn),就是不用考慮列表的滑動(dòng)問(wèn)題,因?yàn)椴シ牌骶驮趇tem里面。

PS:不得不說(shuō)我當(dāng)時(shí)用的就是這個(gè)思路,現(xiàn)在回想一下實(shí)在是太low比了。

實(shí)現(xiàn)思路二

實(shí)現(xiàn)VideoPlayerController類,單例模式,封裝視頻播放的相關(guān)邏輯,需要播放哪一個(gè)視頻,添加到指定的item中,不播放移除播放器。

優(yōu)點(diǎn):

解耦:將adapter和播放邏輯進(jìn)行解耦,增強(qiáng)維護(hù)性。
優(yōu)化內(nèi)存,一個(gè)頁(yè)面僅持有一個(gè)播放器。

缺點(diǎn):

滑動(dòng)問(wèn)題:只能適用于滑動(dòng)停止的時(shí)候播放,可擴(kuò)展性差。
性能問(wèn)題:添加和移除View,都會(huì)重新測(cè)量Parent,可能會(huì)出現(xiàn)卡頓問(wèn)題。

這是我偶然想到的一個(gè)實(shí)現(xiàn)思路,僅僅具有參考意義,不推薦使用。

實(shí)現(xiàn)思路三(最終方案)

通過(guò)控制一個(gè)浮層的顯示,隱藏和滑動(dòng),覆蓋列表中播放位置的item。
優(yōu)點(diǎn):

解耦:adapter完全不用寫播放邏輯,因?yàn)橐呀?jīng)被分離到懸浮的View中;
性能:一個(gè)列表僅持有一個(gè)播放器,也不會(huì)涉及到View的測(cè)量相關(guān)的問(wèn)題。

缺點(diǎn):

如果硬要說(shuō)缺點(diǎn)的話,就是要對(duì)列表的滑動(dòng)控制很精確,熟悉各種api和監(jiān)聽(tīng)器。

這也是我最終確定的方案,也是目前想到的最完美的方案。

代碼

我們?yōu)樽远xView確命名為:FloatItemRecyclerView。

我們的目的是擴(kuò)展RecyclerView,所以FloatItemRecyclerView的定位是一個(gè)包裝擴(kuò)展類,什么是包裝擴(kuò)展類呢?

例如比較有名氣的開(kāi)源框架:PtrClassicFrameLayout,他實(shí)現(xiàn)的功能是下拉刷新功能,只要把需要下拉刷新的View放到里面去,就實(shí)現(xiàn)了刷新功能,不影響View本身的功能,把對(duì)架構(gòu)的影響降到最低。

開(kāi)發(fā)中,我們的通用架構(gòu)中往往會(huì)使用一些開(kāi)源的或自定義的RecyclerView,這種設(shè)計(jì)就會(huì)很棒,哪里需要套哪里,十分瀟灑。

所以FloatItemRecyclerView內(nèi)部需要持有一個(gè)RecyclerView類型的對(duì)象,我們通過(guò)泛型可以添加任意類型的RecyclerView的子類。

public class FloatItemRecyclerView<V extends RecyclerView> extends FrameLayout {

    /**
     * 要懸浮的View
     */
    private View floatView;

    /**
     * recyclerView
     */
    private V recyclerView;
    
    /**
     * 控制每一個(gè)item是否要顯示floatView
     */
    private FloatViewShowHook<V> floatViewShowHook;

    /**
     * 根據(jù)item設(shè)置是否顯示浮動(dòng)的View
     */
    public interface FloatViewShowHook<V extends RecyclerView> {

        /**
         * 當(dāng)前item是否要顯示floatView
         *
         * @param child    itemView
         * @param position 在列表中的位置
         */
        boolean needShowFloatView(View child, int position);

        V initFloatItemRecyclerView();
    }
}

我們需要通過(guò)設(shè)置FloatViewShowHook完成的初始化工作:

initFloatItemRecyclerView:添加指定類型的RecyclerView,你需要自己設(shè)置LayoutManager和其他屬性。

needShowFloatView:判斷RecyclerView的某一個(gè)child是否需要顯示浮層。如果你對(duì)RecyclerAdapter添加了Header或者Footer,別忘了對(duì)position做加減處理。你可以根據(jù)child 的位置或者通過(guò)position得到對(duì)應(yīng)的數(shù)據(jù),判斷是否要顯示浮層,例如圖片和視頻混合的列表,可以實(shí)現(xiàn)圖片不添加浮層,而視頻需要浮層播放的效果。

然后需要添加OnScrollListener監(jiān)聽(tīng)RecyclerView的滑動(dòng)狀態(tài):

private void initOnScrollListener() {
        RecyclerView.OnScrollListener myScrollerListener = new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (floatView == null) {
                    return;
                }
                currentState = newState;
                switch (newState) {
                    // 停止滑動(dòng)
                    case 0:
                        // 對(duì)正在顯示的浮層的child做個(gè)副本,為了判斷顯示浮層的child是否發(fā)現(xiàn)了變化
                        View tempFirstChild = needFloatChild;
                        // 更新浮層的位置,覆蓋child
                        updateFloatScrollStopTranslateY();
                        // 如果firstChild沒(méi)有發(fā)生變化,回調(diào)floatView滑動(dòng)停止的監(jiān)聽(tīng)
                        if (tempFirstChild == needFloatChild) {
                            if (onFloatViewShowListener != null) {
                                onFloatViewShowListener.onScrollStopFloatView(floatView);
                            }
                        }
                        break;
                    // 開(kāi)始滑動(dòng)
                    case 1:
                        // 更新浮層的位置
                        updateFloatScrollStartTranslateY();
                        break;
                    // Fling
                    // 這里有一個(gè)bug,如果手指在屏幕上快速滑動(dòng),但是手指并未離開(kāi),仍然有可能觸發(fā)Fling
                    // 所以這里不對(duì)Fling狀態(tài)進(jìn)行處理
//                    case 2:
//                        hideFloatView();
//                        break;
                }
            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (floatView == null) {
                    return;
                }
                switch (currentState) {
                    // 停止滑動(dòng)
                    case 0:
                        updateFloatScrollStopTranslateY();
                        break;
                    // 開(kāi)始滑動(dòng)
                    case 1:
                        updateFloatScrollStartTranslateY();
                        break;
                    // Fling
                    case 2:
                        updateFloatScrollStartTranslateY();
                        if (onFloatViewShowListener != null) {
                            onFloatViewShowListener.onScrollFlingFloatView(floatView);
                        }
                        break;
                }
            }
        };
        recyclerView.addOnScrollListener(myScrollerListener);
    }

簡(jiǎn)單的概括實(shí)現(xiàn)的邏輯:

  • 靜止?fàn)顟B(tài):遍歷RecyclerView的child,通過(guò)配置的Hook,判斷child是否需要顯示浮層,找到則跳出循環(huán),通過(guò)這個(gè)child的位置,更新浮層的位置。
  • 開(kāi)始滑動(dòng):如果有顯示浮層的child,不停的刷新浮層的位置。
  • 慣性滑動(dòng):注釋上已經(jīng)寫的很清楚了,不做處理。

對(duì)于child是否顯示浮層的判斷過(guò)程:

/**
 * 計(jì)算需要顯示floatView的位置
 * 
 * @return 如果找到RecyclerView中對(duì)應(yīng)的child,返回child的位置,否則發(fā)揮-1,表示沒(méi)有要顯示浮層的child
*/
 private int calculateShowFloatViewPosition() {
    // 如果沒(méi)有設(shè)置floatViewShowHook,默認(rèn)返回-1
        if (floatViewShowHook == null) {
            return -1;
        }
        int firstVisiblePosition;
        if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
            firstVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
        } else {
            throw new IllegalArgumentException("only support LinearLayoutManager!!!");
        }
        int childCount = recyclerView.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = recyclerView.getChildAt(i);
            // 判斷這個(gè)child是否需要顯示
            if (child != null && floatViewShowHook.needShowFloatView(child, firstVisiblePosition + i)) {
                return i;
            }
        }
        // -1 表示沒(méi)有需要顯示floatView的item
        return -1;
}

如何判斷child被滑出了屏幕呢?可以通過(guò)設(shè)置監(jiān)聽(tīng)addOnChildAttachStateChangeListener,判斷正在被移除的View是否是顯示浮層的View。

// 監(jiān)聽(tīng)item的移除情況
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
      @Override
      public void onChildViewAttachedToWindow(@NonNull View view) {
      }

      @Override
      public void onChildViewDetachedFromWindow(@NonNull View view) {
          // 判斷child是否被移除
          // 請(qǐng)注意:回調(diào)onChildViewDetachedFromWindow時(shí)并沒(méi)有真正移除這個(gè)child
          // 所以這里增加一個(gè)判斷:floatChildInScreen是否正在被adapter使用,防止浮層閃爍
          if (view == needFloatChild && floatChildInScreen()) {
              clearFirstChild();
          }
      }
 });
        
/**
 * 判斷item是否正在顯示內(nèi)容
*/
private boolean floatChildInScreen() {
    return recyclerView.getChildAdapterPosition(needFloatChild) != -1;
}

這里還額外判斷了floatChildInScreen(),這是因?yàn)榻?jīng)測(cè)試發(fā)現(xiàn),在滾動(dòng)的時(shí)候RecyclerView可能會(huì)執(zhí)行onLayout,在onLayout時(shí),又會(huì)把所有的child調(diào)用remove,然后回調(diào)onChildViewDetachedFromWindow,最終刷新adapter,從而導(dǎo)致浮層閃爍的問(wèn)題。

通過(guò)查看源碼發(fā)現(xiàn),dispatchChildDetached負(fù)責(zé)分發(fā)onChildViewDetachedFromWindow,然后才真正移除child:

源碼

所以我們可以增加判斷:要被移除的正在顯示浮層child,如果正在被adapter使用,我們不去隱藏顯示浮層,這樣就避免了浮層閃爍的問(wèn)題。具體隱藏閃爍的原因還不清楚,可能跟我與PtrClassicFrameLayout一起使用有關(guān)。

我們還得增加一個(gè)OnLayoutChangeListener,當(dāng)設(shè)置adapter和數(shù)據(jù)發(fā)生變化的時(shí)候會(huì)得到這個(gè)回調(diào),我們可以重新判斷具體哪一個(gè)Child要顯示浮層。

// 設(shè)置OnLayoutChangeListener監(jiān)聽(tīng),會(huì)在設(shè)置adapter和adapter.notifyXXX的時(shí)候回調(diào)
// 所以我們要這里判斷顯示浮層的child
recyclerView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
      @Override
      public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
          if (recyclerView.getAdapter() == null) {
              return;
          }
          // 數(shù)據(jù)已經(jīng)刷新,找到需要顯示懸浮的Item
          clearFirstChild();
          // 找到第一個(gè)child
          getFirstChild();
          updateFloatScrollStartTranslateY();
          showFloatView();
      }
});

整體思路就是這么簡(jiǎn)單,如果你需要這樣的效果,你只需要添加如下代碼:

FloatItemRecyclerView<RecyclerView> recyclerView = findViewById(R.id.recycler_view);
recyclerView.setFloatViewShowHook(this);
recyclerView.setFloatView(getLayoutInflater().inflate(R.layout.float_view, (ViewGroup) getWindow().getDecorView(), false));
recyclerView.setOnFloatViewShowListener(this);
recyclerView.setAdapter(new MyAdapter());

// 手動(dòng)查詢要顯示浮層的child
recyclerView.findChildToPlay()

效果就是第一張圖,這里就不重復(fù)貼出來(lái)了。

最后

突然想起在網(wǎng)上看到的一個(gè)段子:一個(gè)Android開(kāi)發(fā)程序員,因?yàn)椴粫?huì)使用RecyclerView面試被拒了。

無(wú)論這個(gè)段子的是真是假,可見(jiàn)熟練使用RecyclerView已經(jīng)變得非常重要。我們要開(kāi)發(fā)一個(gè)列表,是選擇ListView還是RecyclerView呢?簡(jiǎn)單說(shuō)一下我的經(jīng)驗(yàn):

1、
開(kāi)發(fā)通用架構(gòu)推薦使用RecyclerView,否則你可能要維護(hù)多套不同樣式的庫(kù)。(列表,網(wǎng)格,瀑布流,自定義等,便于擴(kuò)展)
2、
僅僅是開(kāi)發(fā)一個(gè)列表,推薦使用RecyclerView,如果產(chǎn)品經(jīng)理心情好,換成瀑布流怎么辦。(便于維護(hù))
3、
如果開(kāi)發(fā)自定義View,且列表需要Header或Footer:如果和項(xiàng)目耦合性較強(qiáng),且已經(jīng)有擴(kuò)展好的RecyclerView.Adapter,可以優(yōu)先考慮使用RecyclerView;如果是想寫開(kāi)源庫(kù),ListView可以優(yōu)先考慮,因?yàn)檫x擇RecyclerView需要捆綁一個(gè)可以添加Header和Footer的Adapter,需要慎重考慮。

之后我會(huì)再做一個(gè)ListView的版本,方便大家使用。

以上就是今天分享的內(nèi)容,希望對(duì)大家今后的學(xué)習(xí)工作有所幫助。本來(lái)想發(fā)布到j(luò)center上,不過(guò)似乎gradle 4.6和bintray插件不兼容,只能暫時(shí)上傳到github上,大家可以下載查看具體內(nèi)容。

https://github.com/li504799868/FloatItemRecyclerView

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

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,141評(píng)論 25 708
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 14,039評(píng)論 2 59
  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 從Android 5.0...
    Rtia閱讀 308,464評(píng)論 27 440
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 33,169評(píng)論 6 472
  • 家電行業(yè)的老大沒(méi)落,已經(jīng)沒(méi)有盈利能力,即使有補(bǔ)貼也不行!任何一個(gè)行業(yè)都不會(huì)是一直會(huì)發(fā)展下去,經(jīng)歷了這么多年,我們已...
    娛樂(lè)1閱讀 157評(píng)論 0 0

友情鏈接更多精彩內(nèi)容