项目中遇到一种需求,竖向滑动的列表中的item要有一种类型是可以左右滑动的横向列表item,我首先想到的就是外面的列表和里面的横向滑动的item都用recyclerview来实现,解决下滑动冲突应该就没问题,顺着思路就开始写代码,先开始两个列表都是直接用的原生的recyclerview,跑起来后竖向滑动很流畅,但横向的recyclerview滑动会不灵敏,从网上找了一种解决方案自定义外面的recyclerview,重写recyclerview的滑动拦截事件,让它只拦截竖向滑动的事件,下面我把自定义的recyclerview的代码贴出来:
外面的列表用到的自定义的总的recyclrview基类
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
/**
* Created by wjj on 2018/10/11.
* 总的recyclrview基类
*/
public class MyRecycleView extends RecyclerView{
private static final int INVALID_POINTER = -1;
private int mScrollPointerId = INVALID_POINTER;
private int mInitialTouchX, mInitialTouchY;
private int mTouchSlop;
public MyRecycleView(Context context) {
this(context, null);
}
public MyRecycleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyRecycleView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final ViewConfiguration vc = ViewConfiguration.get(getContext());
mTouchSlop = vc.getScaledTouchSlop();
}
@Override
public void setScrollingTouchSlop(int slopConstant) {
super.setScrollingTouchSlop(slopConstant);
final ViewConfiguration vc = ViewConfiguration.get(getContext());
switch (slopConstant) {
case TOUCH_SLOP_DEFAULT:
mTouchSlop = vc.getScaledTouchSlop();
break;
case TOUCH_SLOP_PAGING:
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(vc);
break;
default:
break;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
final int action = MotionEventCompat.getActionMasked(e);
final int actionIndex = MotionEventCompat.getActionIndex(e);
switch (action) {
case MotionEvent.ACTION_DOWN:
mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
mInitialTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = (int) (e.getY() + 0.5f);
return super.onInterceptTouchEvent(e);
case MotionEventCompat.ACTION_POINTER_DOWN:
mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
mInitialTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
mInitialTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
return super.onInterceptTouchEvent(e);
case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
if (index < 0) {
return false;
}
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
if (getScrollState() != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
final boolean canScrollVertically = getLayoutManager().canScrollVertically();
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy) || canScrollVertically)) {
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop && (Math.abs(dy) >= Math.abs(dx) || canScrollHorizontally)) {
startScroll = true;
}
return startScroll && super.onInterceptTouchEvent(e);
}
return super.onInterceptTouchEvent(e);
}
default:
return super.onInterceptTouchEvent(e);
}
}
}
竖向滑动列表用到的自定义的的recyclrview代码,注意这个才是真正用到的外面列表的自定义recyclrview代码
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
/**
* Created by wjj on 2018/10/11.
*解决recycrview嵌套横向recyclerview后,子的item recyclerview横向滑动不灵敏问题
*/
public class FeedRootRecyclerView extends MyRecycleView{
public FeedRootRecyclerView(Context context) {
this(context, null);
}
public FeedRootRecyclerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FeedRootRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
/* do nothing */
}
}
然后里面子的item的recyclerview就用原生的就行,不用自定义,这样运行起来后就没有滑动不灵敏的问题了。
下面再来记录下我遇到的外面列表滑动卡顿的问题,网上关于recyclerview滑动卡顿有一种方案是给recyclerview 添加OnScrollListener
然后判断列表滑动时停止加载图片,停止滑动后再加载网络图片,这种方案我试了下,效果感觉不是太佳
// 动态改变adapter中isScrolling的状态值,避免adapter在recyclerview正在滑动时进行网络,广告请求或者图片,视频加载
//查看源码可知State有三种状态:SCROLL_STATE_IDLE(静止)、SCROLL_STATE_DRAGGING(上升)、SCROLL_STATE_SETTLING(下落)
// 静止状态 滚动静止时才加载图片资源,极大提升流畅度
// if (newState==RecyclerView.SCROLL_STATE_IDLE){
// adapter?.isStopScrolling = true
// adapter?.notifyDataSetChanged() // notify调用后onBindViewHolder会响应调用
// }else{
// adapter?.isStopScrolling = false
// }
经过测试发现我这里出现的问题不是网络图片加载和recyclerview嵌套recyclerview滑动冲突造成的,而是竖向recyclerview的item类型多,然后布局比较复杂,特别是子的recyclerview的item,每次滑动复用都会重新创建适配器,特别耗时造成的,,优化的方案我用了两个:
1.简化每个itemview 的布局嵌套和不必要的绘制,对gpu的绘制压力大耗时是造成滑动卡和丢帧的原因,Android里面超过16毫秒眼睛就能明显感到卡顿,我检查我的代码,发现有个自定义的绘制阴影的方案特别耗时,因为每次复用都会计算宽高和绘制阴影
var sp = ShadowProperty()
.setShadowColor(0x40000000)
.setShadowDy(DipDpUtil.dip2px(activity, 0.5f))
.setShadowRadius(DipDpUtil.dip2px(activity, 3f))
.setShadowSide(ShadowProperty.ALL)
var sd = ShadowViewDrawable(sp, 0x77000000, DipDpUtil.dip2px(activity, 6f).toFloat(), DipDpUtil.dip2px(activity, 6f).toFloat())
ViewCompat.setBackground(itemHolder.itemV1, sd)
ViewCompat.setLayerType(itemHolder.itemV1, ViewCompat.LAYER_TYPE_SOFTWARE, null)
我把这个自定义控件代码注释掉后,滑动卡顿就会好转很多,然后得出一个结论,在列表中如果给控件加阴影,绘制会耗时,用图片做阴影滑动会非常流畅,当时ui就是不愿意切图,程序就用了自定义绘制阴影的控件,最后还是换成了用图片绘制阴影的方案。
2.如果子的itemview是recyclerview类型的时候,adapter不能重复的重建,要用好自定义复用策略
我贴下我的代码:
val linearManager = LinearLayoutManager(actiity)
holder.recycle_picfour.layoutManager = linearManager
linearManager.orientation = LinearLayoutManager.HORIZONTAL
var isVideo = position == 1
if (holder.recycle_picfour.adapter!=null){
(holder.recycle_picfour.adapter as HomeFraChildRecyclerAdapter).wallpaperBeanList = wallpaperBeanList
(holder.recycle_picfour.adapter as HomeFraChildRecyclerAdapter).itemWallpaperBean = itemWallpaperBean
(holder.recycle_picfour.adapter as HomeFraChildRecyclerAdapter).isVideo = isVideo
(holder.recycle_picfour.adapter as HomeFraChildRecyclerAdapter).notifyDataSetChanged()
}else{
var adapter = HomeFraChildRecyclerAdapter(actiity, isVideo, itemWallpaperBean)
holder.recycle_picfour.adapter = adapter
}
//之前方案,缓存recyclerview和adapter的复用的方案,存在数据会重复显示的问题,已用上面的adapter notifyDataSetChanged方法取代
// if (recyclerAdapterViewMap[position]!=null){
// if (position==1){
// var adapter = HomeFraChildRecyclerAdapter(actiity, isVideo, itemWallpaperBean)
// holder.recycle_picfour.adapter = adapter
// holder.recycle_picfour.setItemViewCacheSize(7)
// }else{
//// holder.recycle_picfour = recyclerViewMap!![position]!!
// recyclerAdapterViewMap!![position]!!.itemWallpaperBean = itemWallpaperBean
// holder.recycle_picfour.adapter = recyclerAdapterViewMap!![position]!!
// recyclerAdapterViewMap!![position]!!.notifyDataSetChanged()
// }
// }else{
// holder.recycle_picfour.setItemViewCacheSize(7)
// if (holder.recycle_picfour.adapter!=null){
// recyclerAdapterViewMap!![position]!!.itemWallpaperBean = itemWallpaperBean
// holder.recycle_picfour.adapter = recyclerAdapterViewMap!![position]!!
// recyclerAdapterViewMap!![position]!!.notifyDataSetChanged()
//// Toast.makeText(actiity,"holder.recycle_picfour.adapter!=null",Toast.LENGTH_SHORT).show()
// }else{
// var adapter = HomeFraChildRecyclerAdapter(actiity, isVideo, itemWallpaperBean)
// holder.recycle_picfour.adapter = adapter
////// Toast.makeText(actiity,"holder.recycle_picfour.adapter是新建的",Toast.LENGTH_SHORT).show()
// recyclerAdapterViewMap.put(position,adapter)
// }
//// recyclerViewMap.put(position,holder.recycle_picfour)
//
// }
这里的代码里面我注释掉的代码是我找到正确方法前尝试的方案,也记录下来作为怕坑的经验记录,便于后续温习,然后没有注释的代码是正确的复用方案,需要注意的是adapter里面的数据一定要动态手动赋值然后调用notifyDataSetChanged才会真正刷新recyclerview的itemview,否则会出现数据显示错乱的现象,这样无论竖向还是横向的recyclerview滑动就都会非常流畅了,我当时用了手机设置里面开发者选项中的gpu过度绘制呈现,蓝色的柱状图过高,超过绿色的线太多就说明滑动比较卡顿,柱状图越高越说明那一帧的绘制时间比较长。