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

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

前言

又到了年底,好多的事情都要收尾,今天分享一个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 initFloatItemRecyclerView();
    }
}

我们需要通过设置FloatViewShowHook完成的初始化工作:

initFloatItemRecyclerView:添加指定类型的RecyclerView,你需要自己设置LayoutManager和其他属性。

needShowFloatView:判断RecyclerView的某一个child是否需要显示浮层。如果你对RecyclerAdapter添加了Header或者Footer,别忘了对position做加减处理。你可以根据child 的位置或者通过position得到对应的数据,判断是否要显示浮层,例如图片和视频混合的列表,可以实现图片不添加浮层,而视频需要浮层播放的效果。

然后需要添加OnScrollListener监听RecyclerView的滑动状态:

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:
                        // 对正在显示的浮层的child做个副本,为了判断显示浮层的child是否发现了变化
                        View tempFirstChild = needFloatChild;
                        // 更新浮层的位置,覆盖child
                        updateFloatScrollStopTranslateY();
                        // 如果firstChild没有发生变化,回调floatView滑动停止的监听
                        if (tempFirstChild == needFloatChild) {
                            if (onFloatViewShowListener != null) {
                                onFloatViewShowListener.onScrollStopFloatView(floatView);
                            }
                        }
                        break;
                    // 开始滑动
                    case 1:
                        // 更新浮层的位置
                        updateFloatScrollStartTranslateY();
                        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);
    }

简单的概括实现的逻辑:

  • 静止状态:遍历RecyclerView的child,通过配置的Hook,判断child是否需要显示浮层,找到则跳出循环,通过这个child的位置,更新浮层的位置。
  • 开始滑动:如果有显示浮层的child,不停的刷新浮层的位置。
  • 惯性滑动:注释上已经写的很清楚了,不做处理。

对于child是否显示浮层的判断过程:

/**
 * 计算需要显示floatView的位置
 * 
 * @return 如果找到RecyclerView中对应的child,返回child的位置,否则发挥-1,表示没有要显示浮层的child
*/
 private int calculateShowFloatViewPosition() {
    // 如果没有设置floatViewShowHook,默认返回-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);
            // 判断这个child是否需要显示
            if (child != null && floatViewShowHook.needShowFloatView(child, firstVisiblePosition + i)) {
                return i;
            }
        }
        // -1 表示没有需要显示floatView的item
        return -1;
}

如何判断child被滑出了屏幕呢?可以通过设置监听addOnChildAttachStateChangeListener,判断正在被移除的View是否是显示浮层的View。

// 监听item的移除情况
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
      @Override
      public void onChildViewAttachedToWindow(@NonNull View view) {
      }

      @Override
      public void onChildViewDetachedFromWindow(@NonNull View view) {
          // 判断child是否被移除
          // 请注意:回调onChildViewDetachedFromWindow时并没有真正移除这个child
          // 所以这里增加一个判断:floatChildInScreen是否正在被adapter使用,防止浮层闪烁
          if (view == needFloatChild && floatChildInScreen()) {
              clearFirstChild();
          }
      }
 });
        
/**
 * 判断item是否正在显示内容
*/
private boolean floatChildInScreen() {
    return recyclerView.getChildAdapterPosition(needFloatChild) != -1;
}

这里还额外判断了floatChildInScreen(),这是因为经测试发现,在滚动的时候RecyclerView可能会执行onLayout,在onLayout时,又会把所有的child调用remove,然后回调onChildViewDetachedFromWindow,最终刷新adapter,从而导致浮层闪烁的问题。

通过查看源码发现,dispatchChildDetached负责分发onChildViewDetachedFromWindow,然后才真正移除child:

源码

所以我们可以增加判断:要被移除的正在显示浮层child,如果正在被adapter使用,我们不去隐藏显示浮层,这样就避免了浮层闪烁的问题。具体隐藏闪烁的原因还不清楚,可能跟我与PtrClassicFrameLayout一起使用有关。

我们还得增加一个OnLayoutChangeListener,当设置adapter和数据发生变化的时候会得到这个回调,我们可以重新判断具体哪一个Child要显示浮层。

// 设置OnLayoutChangeListener监听,会在设置adapter和adapter.notifyXXX的时候回调
// 所以我们要这里判断显示浮层的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;
          }
          // 数据已经刷新,找到需要显示悬浮的Item
          clearFirstChild();
          // 找到第一个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());

// 手动查询要显示浮层的child
recyclerView.findChildToPlay()

效果就是第一张图,这里就不重复贴出来了。

最后

突然想起在网上看到的一个段子:一个Android开发程序员,因为不会使用RecyclerView面试被拒了。

无论这个段子的是真是假,可见熟练使用RecyclerView已经变得非常重要。我们要开发一个列表,是选择ListView还是RecyclerView呢?简单说一下我的经验:

1、
开发通用架构推荐使用RecyclerView,否则你可能要维护多套不同样式的库。(列表,网格,瀑布流,自定义等,便于扩展)
2、
仅仅是开发一个列表,推荐使用RecyclerView,如果产品经理心情好,换成瀑布流怎么办。(便于维护)
3、
如果开发自定义View,且列表需要Header或Footer:如果和项目耦合性较强,且已经有扩展好的RecyclerView.Adapter,可以优先考虑使用RecyclerView;如果是想写开源库,ListView可以优先考虑,因为选择RecyclerView需要捆绑一个可以添加Header和Footer的Adapter,需要慎重考虑。

之后我会再做一个ListView的版本,方便大家使用。

以上就是今天分享的内容,希望对大家今后的学习工作有所帮助。本来想发布到jcenter上,不过似乎gradle 4.6和bintray插件不兼容,只能暂时上传到github上,大家可以下载查看具体内容。

https://github.com/li504799868/FloatItemRecyclerView

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页