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

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

前言

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

首先看一下效果圖:


在這里插入圖片描述

有人會問我:老鐵加叁,你實(shí)現(xiàn)的這個(gè)東西有個(gè)卵用?如果你沒看明白蔗怠,我們再看一張非常熟悉的應(yīng)用場景:


在這里插入圖片描述

正文

記得2年前在創(chuàng)業(yè)公司的時(shí)候墩弯,正是短視頻火爆的高峰期,公司也做了一款二次元的短視頻app寞射,很可惜還沒上線就被腰斬了渔工。當(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è)頁面持有多個(gè)播放器,很容易導(dǎo)致內(nèi)存泄露履腋;
可維護(hù)性差:adapter中不可避免的需要插入播放相關(guān)的內(nèi)容珊燎,耦合性強(qiáng),代碼臃腫遵湖,后期不易維護(hù)悔政。

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

PS:不得不說我當(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è)頁面僅持有一個(gè)播放器二鳄。

缺點(diǎn):

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

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

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

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

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

缺點(diǎn):

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

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

代碼

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

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

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

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

所以FloatItemRecyclerView內(nèi)部需要持有一個(gè)RecyclerView類型的對象,我們通過泛型可以添加任意類型的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();
    }
}

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

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

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

然后需要添加OnScrollListener監(jiān)聽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:
                        // 對正在顯示的浮層的child做個(gè)副本浸遗,為了判斷顯示浮層的child是否發(fā)現(xiàn)了變化
                        View tempFirstChild = needFloatChild;
                        // 更新浮層的位置猫胁,覆蓋child
                        updateFloatScrollStopTranslateY();
                        // 如果firstChild沒有發(fā)生變化,回調(diào)floatView滑動(dòng)停止的監(jiān)聽
                        if (tempFirstChild == needFloatChild) {
                            if (onFloatViewShowListener != null) {
                                onFloatViewShowListener.onScrollStopFloatView(floatView);
                            }
                        }
                        break;
                    // 開始滑動(dòng)
                    case 1:
                        // 更新浮層的位置
                        updateFloatScrollStartTranslateY();
                        break;
                    // Fling
                    // 這里有一個(gè)bug跛锌,如果手指在屏幕上快速滑動(dòng)弃秆,但是手指并未離開,仍然有可能觸發(fā)Fling
                    // 所以這里不對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;
                    // 開始滑動(dòng)
                    case 1:
                        updateFloatScrollStartTranslateY();
                        break;
                    // Fling
                    case 2:
                        updateFloatScrollStartTranslateY();
                        if (onFloatViewShowListener != null) {
                            onFloatViewShowListener.onScrollFlingFloatView(floatView);
                        }
                        break;
                }
            }
        };
        recyclerView.addOnScrollListener(myScrollerListener);
    }

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

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

對于child是否顯示浮層的判斷過程:

/**
 * 計(jì)算需要顯示floatView的位置
 * 
 * @return 如果找到RecyclerView中對應(yīng)的child们拙,返回child的位置,否則發(fā)揮-1阁吝,表示沒有要顯示浮層的child
*/
 private int calculateShowFloatViewPosition() {
    // 如果沒有設(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 表示沒有需要顯示floatView的item
        return -1;
}

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

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

      @Override
      public void onChildViewDetachedFromWindow(@NonNull View view) {
          // 判斷child是否被移除
          // 請注意:回調(diào)onChildViewDetachedFromWindow時(shí)并沒有真正移除這個(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)測試發(fā)現(xiàn)埂奈,在滾動(dòng)的時(shí)候RecyclerView可能會執(zhí)行onLayout迄损,在onLayout時(shí),又會把所有的child調(diào)用remove账磺,然后回調(diào)onChildViewDetachedFromWindow芹敌,最終刷新adapter,從而導(dǎo)致浮層閃爍的問題垮抗。

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

源碼

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

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

// 設(shè)置OnLayoutChangeListener監(jiān)聽挺狰,會在設(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();
      }
});

整體思路就是這么簡單,如果你需要這樣的效果她渴,你只需要添加如下代碼:

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ù)貼出來了。

最后

突然想起在網(wǎng)上看到的一個(gè)段子:一個(gè)Android開發(fā)程序員趁耗,因?yàn)椴粫褂肦ecyclerView面試被拒了沉唠。

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

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

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

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

https://github.com/li504799868/FloatItemRecyclerView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市镊绪,隨后出現(xiàn)的幾起案子匀伏,更是在濱河造成了極大的恐慌,老刑警劉巖蝴韭,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件够颠,死亡現(xiàn)場離奇詭異,居然都是意外死亡榄鉴,警方通過查閱死者的電腦和手機(jī)履磨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庆尘,“玉大人剃诅,你說我怎么就攤上這事∈患桑” “怎么了矛辕?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我如筛,道長堡牡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任杨刨,我火速辦了婚禮晤柄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妖胀。我一直安慰自己芥颈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布赚抡。 她就那樣靜靜地躺著爬坑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涂臣。 梳的紋絲不亂的頭發(fā)上盾计,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機(jī)與錄音赁遗,去河邊找鬼署辉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岩四,可吹牛的內(nèi)容都是我干的哭尝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼剖煌,長吁一口氣:“原來是場噩夢啊……” “哼材鹦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起耕姊,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤桶唐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后茉兰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尤泽,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年邦邦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了安吁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡燃辖,死狀恐怖鬼店,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情黔龟,我是刑警寧澤妇智,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布滥玷,位于F島的核電站,受9級特大地震影響巍棱,放射性物質(zhì)發(fā)生泄漏惑畴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一航徙、第九天 我趴在偏房一處隱蔽的房頂上張望如贷。 院中可真熱鬧,春花似錦到踏、人聲如沸杠袱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽楣富。三九已至,卻和暖如春伴榔,著一層夾襖步出監(jiān)牢的瞬間纹蝴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工踪少, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留塘安,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓秉馏,卻偏偏與公主長得像耙旦,于是被迫代替她去往敵國和親脱羡。 傳聞我的和親對象是個(gè)殘疾皇子萝究,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,167評論 25 707
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料锉罐? 從這篇文章中你...
    hw1212閱讀 12,730評論 2 59
  • 【Android 控件 RecyclerView】 概述 RecyclerView是什么 從Android 5.0...
    Rtia閱讀 307,532評論 27 439
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 32,940評論 6 472
  • 家電行業(yè)的老大沒落帆竹,已經(jīng)沒有盈利能力,即使有補(bǔ)貼也不行脓规!任何一個(gè)行業(yè)都不會是一直會發(fā)展下去栽连,經(jīng)歷了這么多年,我們已...
    娛樂1閱讀 115評論 0 0