1. 程式人生 > >自定義View:實現RecyclerView的item新增懸浮層的效果

自定義View:實現RecyclerView的item新增懸浮層的效果

前言

20天后,終於良心發現更新部落格了,又到了年底,好多的事情都要收尾,今天分享一個RecyclerView的容器類,幫助大家實現新增Item的浮層的效果。

首先看一下效果圖:
在這裡插入圖片描述

有人會問我:老鐵,你實現的這個東西有個卵用?如果你沒看明白,我們再看一張非常熟悉的應用場景:
在這裡插入圖片描述

正文

記得2年前在創業公司的時候,正是短視訊火爆的高峰期,公司也做了一款二次元的短視訊app,很可惜還沒上線就被腰斬了。當時就要求做了這個效果,雖然實現了,但是實現的方案實在是太low了。今天也算是彌補了這個遺憾。

實現思路一

在每一個Item中放入一個VideoPlayer,但是缺點太多:

可控性差:控制播放哪一個位置的視訊,視訊的停止和播放等等,都需要寫大量的邏輯;
記憶體風險高:播放器還是很佔用記憶體的,一個頁面持有多個播放器,很容易導致記憶體洩露;
可維護性差:adapter中不可避免的需要插入播放相關的內容,耦合性強,程式碼臃腫,後期不易維護。

當然這個方案也有優點,就是不用考慮列表的滑動問題,因為播放器就在item裡面。

PS:不得不說我當時用的就是這個思路,現在回想一下實在是太low比了。

實現思路二

實現VideoPlayerController類,單例模式,封裝視訊播放的相關邏輯,需要播放哪一個視訊,新增到指定的item中,不播放移除播放器。

優點:

解耦:將adapter和播放邏輯進行解耦,增強維護性。
優化記憶體,一個頁面僅持有一個播放器。

缺點:

滑動問題:只能適用於滑動停止的時候播放,可擴充套件性差。
效能問題:新增和移除View,都會重新測量Parent,可能會出現卡頓問題。

這是我偶然想到的一個實現思路,僅僅具有參考意義,不推薦使用。

實現思路三(最終方案)

通過控制一個浮層的顯示,隱藏和滑動,覆蓋列表中播放位置的item。
優點:

解耦:adapter完全不用寫播放邏輯,因為已經被分離到懸浮的View中;
效能:一個列表僅持有一個播放器,也不會涉及到View的測量相關的問題。

缺點:

如果硬要說缺點的話,就是要對列表的滑動控制很精確,熟悉各種api和監聽器。

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

程式碼

我們為自定義View確命名為:FloatItemRecyclerView

我們的目的是擴充套件RecyclerView,所以FloatItemRecyclerView是一個包裝擴充套件類,什麼是包裝擴充套件類呢?例如比較有名氣的開源框架:PtrClassicFrameLayout,他實現的功能是下拉重新整理功能,只要把需要下拉重新整理的View放到裡面去,就實現了重新整理功能,不影響View本身的功能,把對架構的影響降到最低。

開發中,我們的通用架構中往往會使用一些開源的或自定義的RecyclerView,這種設計就會很棒,哪裡需要套哪裡,十分瀟灑。

所以FloatItemRecyclerView內部需要持有一個RecyclerView型別的物件,我們通過泛型可以新增任意型別的RecyclerView的子類。

public class FloatItemRecyclerView<V extends RecyclerView> extends FrameLayout {

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

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

	/**
     * 根據item設定是否顯示浮動的View
     */
    public interface FloatViewShowHook<V extends RecyclerView> {

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

        V initVideoPlayRecyclerView();
    }
}

通過指定FloatViewShowHook完成RecyclerView的新增和判斷RecyclerView的Item是否要顯示浮層。

然後需要新增OnScrollListener監聽RecyclerView的滑動狀態:

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) {
                    // 停止滑動
                    case 0:
                        View tempFirstChild = firstChild;
                        updateFloatScrollStopTranslateY();
                        // 如果firstChild沒有發生變化,回撥floatView滑動停止的監聽
                        if (tempFirstChild == firstChild) {
                            if (onFloatViewShowListener != null) {
                                onFloatViewShowListener.onScrollStopFloatView(floatView);
                            }
                        }
                        break;
                    // 開始滑動
                    case 1:
                        // 儲存第一個child
//                        getFirstChild();
                        updateFloatScrollStartTranslateY();
//                        showFloatView();
                        break;
                    // Fling
                    // 這裡有一個bug,如果手指在螢幕上快速滑動,但是手指並未離開,仍然有可能觸發Fling
                    // 所以這裡不對Fling狀態進行處理
//                    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) {
                    // 停止滑動
                    case 0:
                        updateFloatScrollStopTranslateY();
                        break;
                    // 開始滑動
                    case 1:
                        updateFloatScrollStartTranslateY();
                        break;
                    // Fling
                    case 2:
                        updateFloatScrollStartTranslateY();
                        if (onFloatViewShowListener != null) {
                            onFloatViewShowListener.onScrollFlingFloatView(floatView);
                        }

                        break;
                }
            }
        };

簡單的概括實現的邏輯:

  • 靜止狀態:遍歷RecyclerView的child,通過配置的Hook,判斷child是否需要顯示浮層,找到跳出迴圈,通過這個child的位置,更新浮層的位置。
  • 開始滑動:如果有顯示浮層的View,不停的重新整理浮層的位置,如果View已經劃出螢幕,重新找新的View。
  • 慣性滑動:註釋上已經寫的很清楚了,不做處理。

如何判斷child被滑出了螢幕呢?可以通過設定監聽addOnChildAttachStateChangeListener,判斷正在被移除的View是否是顯示浮層的View。

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

            }

            @Override
            public void onChildViewDetachedFromWindow(@NonNull View view) {
                if (view == firstChild && outScreen()) {
                    clearFirstChild();
                }
            }
        });

這裡還額外判斷了outScreen(),這是因為onChildViewDetachedFromWindow被回撥的時候,實際上還沒有被remove掉,所以會存在判斷的誤差,導致浮層會閃爍的問題。

我們還得增加一個OnLayoutChangeListener,當設定adapter和資料發生變化的時候會得到這個回撥,我們可以重新判斷具體哪一個Child要顯示浮層。

// 設定OnLayoutChangeListener監聽,會在設定adapter和adapter.notifyXXX的時候回撥
        // 所以我們要這裡做一些處理
        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;
                }
                // 資料已經重新整理,找到需要顯示懸浮的Item
                clearFirstChild();
                // 找到第一個child
                getFirstChild();
                updateFloatScrollStartTranslateY();
                showFloatView();
            }
        });

整體思路就是這麼簡單。實現一個這樣的效果,我們只需要一個300多行的類,下面貼出完整的程式碼:

/**
 * Created by li.zhipeng on 2018/10/10.
 * <p>
 * RecyclerView包裝類
 */
public class FloatItemRecyclerView<V extends RecyclerView> extends FrameLayout {

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

    /**
     * recyclerView
     */
    private V recyclerView;

    /**
     * 當前的滑動狀態
     */
    private int currentState = -1;

    private View firstChild = null;

    /**
     * 懸浮View的顯示狀態監聽器
     */
    private OnFloatViewShowListener onFloatViewShowListener;

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

    public FloatItemRecyclerView(@NonNull Context context) {
        this(context, null);
    }

    public FloatItemRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FloatItemRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 設定懸浮的View
     */
    public void setFloatView(View floatView) {
        this.floatView = floatView;
        if (floatView.getLayoutParams() == null) {
            floatView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        }
        addView(this.floatView);
        this.floatView.setVisibility(View.GONE);
    }

    /**
     * 必須設定FloatViewShowHook,完成View的初始化操作
     */
    public void setFloatViewShowHook(FloatViewShowHook<V> floatViewShowHook) {
        this.floatViewShowHook = floatViewShowHook;
        recyclerView = floatViewShowHook.initVideoPlayRecyclerView();
        addRecyclerView();
        // 移動到前臺
        if (floatView != null) {
            bringChildToFront(floatView);
            updateViewLayout(floatView, floatView.getLayoutParams());
        }
    }


    @SuppressWarnings("unchecked")
    private void addRecyclerView() {
        addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        // 設定滾動監聽
        initOnScrollListener();
        // 設定佈局監聽,當adapter資料發生改變的時候,需要做一些處理
        initOnLayoutChangedListener();
        // 監聽recyclerView的item滾動情況,判斷正在懸浮item是否已經移出了螢幕
        initOnChildAttachStateChangeListener();

    }

    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) {
                    // 停止滑動
                    case 0:
                        View tempFirstChild = firstChild;
                        updateFloatScrollStopTranslateY();
                        // 如果firstChild沒有發生變化,回撥floatView滑動停止的監聽
                        if (tempFirstChild == firstChild) {
                            if (onFloatViewShowListener != null) {
                                onFloatViewShowListener.onScrollStopFloatView(floatView);
                            }
                        }
                        break;
                    // 開始滑動
                    case 1:
                        // 儲存第一個child
//                        getFirstChild();
                        updateFloatScrollStartTranslateY();
//                        showFloatView();
                        break;
                    // Fling
                    // 這裡有一個bug,如果手指在螢幕上快速滑動,但是手指並未離開,仍然有可能觸發Fling
                    // 所以這裡不對Fling狀態進行處理
//                    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) {
                    // 停止滑動
                    case 0:
                        updateFloatScrollStopTranslateY();
                        break;
                    // 開始滑動
                    case 1:
                        updateFloatScrollStartTranslateY();
                        break;
                    // Fling
                    case 2:
                        updateFloatScrollStartTranslateY();
                        if (onFloatViewShowListener != null) {
                            onFloatViewShowListener.onScrollFlingFloatView(floatView);
                        }

                        break;
                }
            }
        };
        recyclerView.addOnScrollListener(myScrollerListener);
    }

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

            }

            @Override
            public void onChildViewDetachedFromWindow(@NonNull View view) {
                if (view == firstChild && outScreen()) {
                    clearFirstChild();
                }
            }
        });
    }

    private void initOnLayoutChangedListener() {
        // 設定OnLayoutChangeListener監聽,會在設定adapter和adapter.notifyXXX的時候回撥
        // 所以我們要這裡做一些處理
        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;
                }
                // 資料已經重新整理,找到需要顯示懸浮的Item
                clearFirstChild();
                // 找到第一個child
                getFirstChild();
                updateFloatScrollStartTranslateY();
                showFloatView();
            }
        });
    }

    /**
     * 手動計算應該播放視訊的child
     */
    public void findChildToPlay() {
        if (firstChild == null) {
            updateFloatScrollStopTranslateY();
            // 回撥顯示狀態的監聽器
            if (onFloatViewShowListener != null) {
                onFloatViewShowListener.onShowFloatView(floatView,
                        recyclerView.getChildAdapterPosition(firstChild));
            }
            return;
        }
        // 獲取fistChild在列表中的位置
        int position = recyclerView.getChildAdapterPosition(firstChild);
        // 判斷是否允許播放
        if (floatViewShowHook.needShowFloatView(firstChild, position)) {
            updateFloatScrollStartTranslateY();
            showFloatView();
            // 回撥顯示狀態的監聽器
            if (onFloatViewShowListener != null) {
                onFloatViewShowListener.onShowFloatView(floatView,
                        recyclerView.getChildAdapterPosition(firstChild));
            }
        } else {
            // 回撥隱藏狀態的監聽器
            if (onFloatViewShowListener != null) {
                onFloatViewShowListener.onHideFloatView(floatView);
            }
        }
    }

    /**
     * 判斷item是否已經畫出了螢幕
     */
    private boolean outScreen() {
        return recyclerView.getChildAdapterPosition(firstChild) != -1;
    }

    /**
     * 找到第一個要顯示懸浮item的
     */
    private void getFirstChild() {
        if (firstChild != null) {
            return;
        }
        int childPos = calculateShowFloatViewPosition();
        if (childPos != -1) {
            firstChild = recyclerView.getChildAt(childPos);
            // 回撥顯示狀態的監聽器
            if (onFloatViewShowListener != null) {
                onFloatViewShowListener.onShowFloatView(floatView,
                        recyclerView.getChildAdapterPosition(firstChild));
            }
        }
    }

    /**
     * 計算需要顯示floatView的位置
     */
    private int calculateShowFloatViewPosition() {
        // 如果沒有設定floatViewShowHook,預設返回第一個Child
        if (floatViewShowHook == null) {
            return 0;
        }
        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);
            // 判斷這個child是否需要顯示
            if (child != null && floatViewShowHook.needShowFloatView(child, firstVisiblePosition + i)) {
                return i;
            }
        }
        // -1 表示沒有需要顯示floatView的item
        return -1;
    }

    private void showFloatView() {
        if (firstChild != null) {
            floatView.post(new Runnable() {
                @Override
                public void run() {
                    floatView.setVisibility(View.VISIBLE);
                }
            });
        }
    }

    private void hideFloatView() {
        if (firstChild != null) {
            floatView.setVisibility(View.GONE);
        }
    }

    private void updateFloatScrollStartTranslateY() {
        if (firstChild != null) {
            int translateY = firstChild.getTop();
            floatView.setTranslationY(translateY);
            if (onFloatViewShowListener != null) {
                onFloatViewShowListener.onScrollFloatView(floatView);
            }
        }
    }

    private void updateFloatScrollStopTranslateY() {
        if (firstChild == null) {
            getFirstChild();
        }
        updateFloatScrollStartTranslateY();
        showFloatView();
    }

    public V getRecyclerView() {
        return recyclerView;
    }

    /**
     * 清除floatView依賴的item,並隱藏floatView
     */
    public void clearFirstChild() {
        hideFloatView();
        firstChild = null;
        // 回撥監聽器
        if (onFloatViewShowListener != null) {
            onFloatViewShowListener.onHideFloatView(floatView);
        }
    }

    public void setAdapter(RecyclerView.Adapter adapter) {
        recyclerView.setAdapter(adapter);
    }

    public void setOnFloatViewShowListener(OnFloatViewShowListener onFloatViewShowListener) {
        this.onFloatViewShowListener = onFloatViewShowListener;
    }

    /**
     * 顯示狀態的回撥監聽器
     */
    public interface OnFloatViewShowListener {

        /**
         * FloatView被顯示
         */
        void onShowFloatView(View floatView, int position);

        /**
         * FloatView被隱藏
         */
        void onHideFloatView(View floatView);

        /**
         * FloatView被移動
         */
        void onScrollFloatView(View floatView);

        /**
         * FloatView被處於Fling狀態
         */
        void onScrollFlingFloatView(View floatView);

        /**
         * FloatView由滾動變為靜止狀態
         */
        void onScrollStopFloatView(View floatView);

    }

    /**
     * 根據item設定是否顯示浮動的View
     */
    public interface FloatViewShowHook<V extends RecyclerView> {

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

        V initVideoPlayRecyclerView();

    }
}

Demo例項:

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());

效果就是第一張圖,這裡就不重複貼出來了。

總結

以上就是今天分享的內容,希望對大家今後的學習工作有所幫助。本來想釋出到jcenter上,不過似乎gradle 4.6和bintray外掛不相容,只能暫時上傳到github上,大家可以下載檢視具體內容。