前言
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上,大家可以下载查看具体内容。
网友评论