上一篇中给大家介绍了一下自定义View和ViewGroup的流程,并用FlowLayout这个例子给大家演示了一下自定义ViewGroup中onMeasure和onLayout的使用:https://blog.csdn.net/u013107751/article/details/81701606
今天就写个简单的小例子跟大家一起探讨一下自定义View中的事件处理,我们先来看一下要完成效果:
京东商品详情页大体就是进去的时候展示上面头部商品价格,下单预计送达时间,商品部分评价等等,然后是上滑到底部的时候如果在继续往下滑就滑进商品详情页.根据这个需求,想来想去现有的控件好像都没有能很好解决的,这个时候我们就要考虑使用自定义的ViewGroup了.通过自定义一个ViewGroup然后放入头部一个RecyclerView和一个底部商品详情介绍可以满足该功能:
public class DragSlideLayout extends ViewGroup {
public DragSlideLayout(Context context) {
this(context, null);
}
public DragSlideLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragSlideLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
}
定义一个DragSlideLayout继承ViewGroup,重写onMeasure,onLayout这两个方法是必须重写的,不然子View会显示不出来,onFinishInflate则是在XML加载完成的时候我们用来获取DragSlideLayout里面包含的子View的,onInterceptTouchEvent和onTouchEvent则是用来处理拖动的时候的事件处理的,至于对事件的分发机制还不太熟的童鞋可以先移步:https://mp.csdn.net/postedit/81667157 这边看一下ViewGroup的事件分发机制,这边不做过多的详解.
接下来我们先运用在XML中:
然后在MainActivity中填充一下RecyclerView的数据
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView footer = (RecyclerView) findViewById(R.id.footer);
RecyclerView header = (RecyclerView) findViewById(R.id.header);
footer.setLayoutManager(new LinearLayoutManager(this));
header.setLayoutManager(new LinearLayoutManager(this));
List list = new ArrayList();
for (int i = 0; i < 30; i++) {
list.add("我是商品"+i);
}
List list2 = new ArrayList();
for (int i = 0; i < 30; i++) {
list2.add("我是头部"+i);
}
footer.setAdapter(new MyRecycleAdapter(this,list) {
@Override
public void getItemView(MyRecycleViewHolder holder, int position, String item) {
holder.setText(R.id.tv_name,item);
}
@Override
public int getItemResource() {
return R.layout.item;
}
});
header.setAdapter(new MyRecycleAdapter(this,list2) {
@Override
public void getItemView(MyRecycleViewHolder holder, int position, String item) {
holder.setText(R.id.tv_name,item);
holder.getView(R.id.tv_name).setBackgroundColor(Color.YELLOW);
}
@Override
public int getItemResource() {
return R.layout.item;
}
});
}
MyRecycleAdapter只是自己在重写的一个RecyclerView的adapter为了简化代码,我相信大家都有这种控件,这边就不做过多的解释,好了,我们现在运行一下看一下效果
我们可以看到现在是白茫茫的一片,什么都没有,这是因为我们重写的onMeasure和onLayout都是空实现,现在我们来写一下这两个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
onMeasure很简单,让子View计算一下自己的宽高,然后ViewGroup自己计算一下自己的宽高,然后要onLayout,我们首先在
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 跟findviewbyId一样,初始化上下两个view
headerRecycler= getChildAt(0);
footRecycler= getChildAt(1);
}
然后onLayout:
if (changed){
if (headerRecycler.getTop() == 0) {
// 只在初始化的时候调用
// 一些参数作为全局变量保存起来
headerRecycler.layout(l, 0, r, b - t);
footRecycler.layout(l, 0, r, b - t);
viewHeight = headerRecycler.getMeasuredHeight();
footRecycler.offsetTopAndBottom(viewHeight);
} else {
// 如果已被初始化,这次onLayout只需要将之前的状态存入即可
headerRecycler.layout(l, headerRecycler.getTop(), r, headerRecycler.getBottom());
footRecycler.layout(l, footRecycler.getTop(), r, footRecycler.getBottom());
}
}
让两个子View上下摆放,然后我在来运行一下看看效果:
我们发现,只显示了头部的RecyclerView的数据,底部的拉不出来,这是为什么呢?别急,我们这不是还有两个重写的方法没实现的吗,我们现在来写一下onInterceptTouchEvent和onTouchEvent这两个方法
@Override
public boolean onTouchEvent(MotionEvent e) {
// 统一交给mDragHelper处理,由DragHelperCallback实现拖动效果
try {
mDragHelper.processTouchEvent(e);
} catch (Exception ex) {
ex.printStackTrace();
}
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (headerRecycler.getBottom() > 0 && headerRecycler.getTop() < 0) {
// view粘到顶部或底部,正在动画中的时候,不处理touch事件
return false;
}
boolean yScroll = gestureDetector.onTouchEvent(ev); //判断是否是竖直方向的滑动
boolean shouldIntercept = mDragHelper.shouldInterceptTouchEvent(ev);//是否拦截子View的触摸事件
int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// action_down时就让mDragHelper开始工作,否则有时候导致异常
mDragHelper.processTouchEvent(ev);
downTop = headerRecycler.getTop();
}
return shouldIntercept && yScroll;
}
这里引入两个类ViewDragHelper和GestureDetector,这两个又是什么东西呢?我们来看一下
ViewDragHelper 是收录在 v4 兼容包中一个工具类,它的目的是辅助自定义 ViewGroup。ViewDragHelper 针对 ViewGroup 中的拖拽和重新定位 views 操作时提供了一系列非常有用的方法和状态追踪.用这个类就不用我们自己在onTouchEvent实现拖拽的方法了
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
int childIndex = 1;
if (changedView == frameView2) {
childIndex = 2;
}
// 一个view位置改变,另一个view的位置要跟进
onViewPosChanged(childIndex, top);
}
// 决定了是否需要捕获这个 child,只有捕获了才能进行下面的拖拽行为
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 两个子View都需要跟踪,返回true
return true;
}
@Override
public int getViewVerticalDragRange(View child) {
// 这个用来控制拖拽过程中松手后,自动滑行的速度,暂时给一个随意的数值
return 1;
}
// 手指释放时的回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 滑动松开后,需要向上或者乡下粘到特定的位置
animTopOrBottom(releasedChild, yvel);
}
// 修整 child 垂直方向上的坐标,top 指 child 要移动到的坐标,dy 相对上次的偏移量
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
int finalTop = top;
if (child == frameView1) {
// 拖动的时第一个view
if (top > 0) {
// 不让第一个view往下拖,因为顶部会白板
finalTop = 0;
}
} else if (child == frameView2) {
// 拖动的时第二个view
if (top < 0) {
// 不让第二个view网上拖,因为底部会白板
finalTop = 0;
}
}
// finalTop代表的是理论上应该拖动到的位置。此处计算拖动的距离除以一个参数(3),是让滑动的速度变慢。数值越大,滑动的越慢
return child.getTop() + (finalTop - child.getTop()) / 3;
}
}
这就是ViewDragHelper的回调,主要就是用来处理手指触摸放手回弹等等.
GestureDetector:这也是自定义View中的一个神器,通常用来处理手指点击,滑动,长按等事件,我们这边只用到了onScroll方法用来判断手指的滑动方向
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx,
float dy) {
// 垂直滑动时dy>dx,才被认定是上下拖动
return Math.abs(dy) > Math.abs(dx);
}
处理完这波手势操作我们在来看一下效果;
咦!!!滑是滑得动了,可是好像跟我们要的效果不太一样啊,明明头部的RecyclerView的数据都还没展示完呢就滑到底部去了???这是为什么呢???
别急,这是因为我们现在默认把事件全部都交给父View处理了,内部的RecyclerView接收不到事件了,我们现在来改写一下RecyclerView的事件处理,让父View该拦截的时候在拦截,不该拦截的时候就不拦截了:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
downY = ev.getRawY();
needConsumeTouch = true; // 默认情况下,RecyclerView内部的滚动优先,默认情况下由该RecyclerView去消费touch事件
allowDragBottom = isAtBottom();
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
allowDragBottom = isAtBottom();
if (!needConsumeTouch) {
// 在最顶端且向上拉了,则这个touch事件交给父类去处理
getParent().requestDisallowInterceptTouchEvent(false);
return false;
} else if (allowDragBottom) {
// needConsumeTouch尚未被定性,此处给其定性
// 允许拖动到底部的下一页,而且又向上拖动了,就将touch事件交给父view
if (downY - ev.getRawY()> 2) {
// flag设置,由父类去消费
needConsumeTouch = false;
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
}
// 通知父view是否要处理touch事件
getParent().requestDisallowInterceptTouchEvent(needConsumeTouch);
return super.dispatchTouchEvent(ev);
}
/**
* 判断listView是否在顶部
*
* @return 是否在顶部
*/
private boolean isAtBottom() {
boolean resultValue = false;
int childNum = getChildCount();
if (childNum == 0) {
// 没有child,肯定在顶部
resultValue = true;
} else {
LayoutManager layoutManager = getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
int childCount = manager.getItemCount();
if (manager.findLastVisibleItemPosition() == childCount-1) {
// 根据第一个childView来判定是否在顶部
View lastView = manager.getChildAt(childNum -1);
if (Math.abs(lastView.getBottom() - getBottom()) < 2) {
resultValue = true;
}
}
}
}
return resultValue;
}
这是头部的RecyclerView,判断滑动到底部了然后又继续往上滑就让父布局处理事件,否则自己处理事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
downY = ev.getRawY();
needConsumeTouch = true; // 默认情况下,RecyclerView内部的滚动优先,默认情况下由该RecyclerView去消费touch事件
allowDragTop = isAtTop();
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
allowDragTop = isAtTop();
if (!needConsumeTouch) {
// 在最顶端且向上拉了,则这个touch事件交给父类去处理
getParent().requestDisallowInterceptTouchEvent(false);
return false;
} else if (allowDragTop) {
// needConsumeTouch尚未被定性,此处给其定性
// 允许拖动到底部的下一页,而且又向上拖动了,就将touch事件交给父view
if (ev.getRawY() - downY > 2) {
// flag设置,由父类去消费
needConsumeTouch = false;
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
}
// 通知父view是否要处理touch事件
getParent().requestDisallowInterceptTouchEvent(needConsumeTouch);
return super.dispatchTouchEvent(ev);
}
/**
* 判断listView是否在顶部
*
* @return 是否在顶部
*/
private boolean isAtTop() {
boolean resultValue = false;
int childNum = getChildCount();
if (childNum == 0) {
// 没有child,肯定在顶部
resultValue = true;
} else {
LayoutManager layoutManager = getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
if (manager.findFirstVisibleItemPosition() == 0) {
// 根据第一个childView来判定是否在顶部
View firstView = getChildAt(0);
if (Math.abs(firstView.getTop() - getTop()) < 2) {
resultValue = true;
}
}
}
}
return resultValue;
}
这是底部的RecyclerView,判断滑动到顶部了然后又继续往下滑就让父布局处理事件,否则自己处理事件,完整源码已上传至github:https://github.com/huang7855196/JDDragSlideLayout 欢迎大家指点