美文网首页Android技术知识Android开发经验谈Android开发
自定义View:实现RecyclerView的item添加悬浮层

自定义View:实现RecyclerView的item添加悬浮层

作者: 珠穆朗玛小王子 | 来源:发表于2018-11-15 14:12 被阅读30次

前言

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上,大家可以下载查看具体内容。

https://github.com/li504799868/VideoPlayRecyclerView

相关文章

网友评论

本文标题:自定义View:实现RecyclerView的item添加悬浮层

本文链接:https://www.haomeiwen.com/subject/tlryfqtx.html