主目录见:Android高级进阶知识(这是总目录索引)
上一篇《从场景到源码分析事件分发》已经很全面地分析了事件的分发流程,如果会了这个流程,那么这个例子应该也是没有问题的,当然前面View绘制的内容也应该学完。废话不多说,直接上图:
如果想要这个源码可以点击[我想要源码]进行下载,可以用来了解整理的思想。
一.目标
又到了实例的时间了,这个例子算是比较综合了,今天我们的目标又是什么呢?
1.复习《View和ViewGroup的绘制原理源码分析》的知识;
2.复习《从场景到源码分析事件分发》中事件拦截的知识;
3.同时了解VelocityTracker和Scroller类的使用。
二.实例分析
1.基础使用
1)在xml中使用
<?xml version="1.0" encoding="utf-8"?>
<com.lenovohit.swipemenu.SwipeMenu xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="60dp">
<include android:id="@+id/item_content" layout="@layout/item_content"/>
<include android:id="@+id/item_menu" layout="@layout/item_menu"/>
</com.lenovohit.swipemenu.SwipeMenu>
首先第一个include包含的是内容布局,第二个include包含的是删除菜单布局。
2)设置监听
SwipeMenu slideLayout = (SwipeMenu) convertView;
slideLayout.setOnStatusChangedListener(new SwipeStatusChangedListener());
然后在Adapter中设置SwipeMenu监听事件来监听点击onDown,打开onOpen,关闭onClose事件。
2.SwipeMenu 实现分析
因为ListView部分很容易,我们都是老司机,没必要再去讲这么基础的东西了,当然你也可以用在RecyclerView中,这都是没问题的。
我们先来看这个自定义控件SwipeMenu 的构造函数:
public SwipeMenu(@NonNull Context context) {
super(context);
init();
}
public SwipeMenu(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public SwipeMenu(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
我们知道这都是老套路了,我们直接看我们init()方法做了些啥?
private void init(){
mScroller = new Scroller(getContext());
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
}
我们看到这里面初始化了Scroller对象和获取到最小滑动距离。想要了解Scroller可以参考这篇[Android Scroller完全解析,关于Scroller你所需知道的一切 ],知识不是特别难。接着在view全部加载完我们就可以获取SwipeMenu中的子视图了:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mMenu = getChildAt(1);
}
这里获取第一个子视图即内容部分视图,第二个子视图即删除菜单部分视图。然后我们就来测量一下SwipeMenu的子视图了:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
measureChildren(widthMeasureSpec,heightMeasureSpec);
int width = mContent.getMeasuredWidth() + mMenu.getMeasuredWidth();
int height = Math.max(mContent.getMeasuredHeight(),mMenu.getMeasuredHeight());
setMeasuredDimension(MeasureSpec.EXACTLY == widthMode ? widthMeasureSpec : width,
MeasureSpec.EXACTLY == heightMode ? heightMeasureSpec : height);
}
我们这个地方使用了ViewGroup的measureChildren来测量子视图,这个方法是ViewGroup提供的,里面会根据父类的Spec和子类的padding和LayoutParams中的宽高来共同测量子视图。最后我们根据父类给的mode来进行宽高测量。这样我们的子视图就测量完成了,我们在测量完获取子视图的宽高:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mContentWidth = mContent.getMeasuredWidth();
mMenuWidth = mMenu.getMeasuredWidth();
mSwipeMenuWidth = getMeasuredWidth();
}
这个方法是在onMeasure之后执行的,其实这个在View的size发生改变后都会执行。测量完毕,又有了各个视图的宽高了,我们就可以进行布局了:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mMenu.layout(mContentWidth,0,mContentWidth + mMenuWidth,getMeasuredHeight());
}
因为我们是FrameLayout,所以内容布局已经占满父控件了,我们只要将删除菜单放在内容布局右边即可。到这里我们的控件实际已经写完了。但是没有事件响应,只能看看,为了让他功能丰富起来,我们就给他添加事件吧。
3.事件拦截
我们要做侧滑肯定是要在适当的时机来决定拦截不拦截事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true;
}
if (mIStatusChangedListener != null){
mIStatusChangedListener.onDown(this);
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;
//判断如果x方向滑动大于y方向滑动,则拦截(防止内容里面有控件点击事件消耗了此事件)
if (Math.abs(deltaX) > Math.abs(deltaY)){
intercepted = true;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastX = x;
mLastY = y;
mLastInterceptX = x;
mLastInterceptY = y;
return intercepted;
}
我们看到这是很典型的拦截事件,我们里面监听了DOWN,MOVE,UP事件。首先我们看到DOWN事件里面:
intercepted = false;
if (!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true;
}
if (mIStatusChangedListener != null){
mIStatusChangedListener.onDown(this);
}
这几句代码是先判断mScroller是否还在滑动,如果还在滑动则停止,并且拦截这个事件,然后告诉监听器回调我已经开始点击了。然后我们看到MOVE事件里面:
int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;
//判断如果x方向滑动大于y方向滑动,则拦截(防止内容里面有控件点击事件消耗了此事件)
if (Math.abs(deltaX) > Math.abs(deltaY)){
intercepted = true;
}
这几句代码比较重要,我们看到这个地方有个判断x方向滑动大于y方向滑动我们就拦截这个事件,为什么要拦截呢?因为如果我们没有拦截事件,有可能这个滑动事件被子视图消费了,我们的SwipeMenu就不能消费这个滑动事件了。所以我们拦截完这个事件交给我自己来处理:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
int scrollerX = getScrollX() - deltaX;
//边界检测,如果向右滑动则scrollerX不动,如果左滑则不能超过菜单距离
if (scrollerX < 0){
scrollerX = 0;
}else if (scrollerX > mMenuWidth){
scrollerX = mMenuWidth;
}
scrollTo(scrollerX,getScrollY());
//当x方向滑动大于y方向滑动,且x方向滑动大于最小滑动距离则说明这个控件要滑动了,告诉父控件不要把事件拦截了
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > Math.abs(mTouchSlop)){
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
int totalX = getScrollX();
//回弹检测
if (totalX < mMenuWidth/2){
closeSwipeMenu();
}else{
openSwipeMenu();
}
break;
case MotionEvent.ACTION_CANCEL:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
我们看到DOWN里面事件跟onInterceptTouchEvent里面是一样的,我们直接来看MOVE事件做了啥:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
int scrollerX = getScrollX() - deltaX;
//边界检测,如果向右滑动则scrollerX不动,如果左滑则不能超过菜单距离
if (scrollerX < 0){
scrollerX = 0;
}else if (scrollerX > mMenuWidth){
scrollerX = mMenuWidth;
}
scrollTo(scrollerX,getScrollY());
//当x方向滑动大于y方向滑动,且x方向滑动大于最小滑动距离则说明这个控件要滑动了,告诉父控件不要把事件拦截了
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > Math.abs(mTouchSlop)){
getParent().requestDisallowInterceptTouchEvent(true);
}
这个地方我们现实判断scrollerx来检测右滑的话则不能让他动,左滑的时候不能让他滑动距离超过删除菜单的距离,是典型的边界检测代码。然后我们看到有个代码非常重要getParent().requestDisallowInterceptTouchEvent(true),这个是让我们的父控件不要拦截这个事件,交给我来处理吧。我们可以从上一篇源码中可以看出,我这里重新贴一下这段代码:
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
可以看到我们设置了为true之后disallowIntercept 就会为true,那么父控件intercepted就返回为false即不拦截事件,所以这个地方ListView就不拦截侧滑事件了交给了我们的SwipeMenu来处理。现在就剩最后一步了就是UP事件:
int totalX = getScrollX();
//回弹检测
if (totalX < mMenuWidth/2){
closeSwipeMenu();
}else{
openSwipeMenu();
}
我们这里判断我们滑动了的距离,是滑动了小于删除菜单的一半还是大于删除菜单的一半,如果小于一半就关闭这个菜单,如果大于一半则就打开这个菜单。然后我们来看这里的打开和关闭是怎么实现的:
public void closeSwipeMenu(){
int distanceX = 0 - getScrollX();
mScroller.startScroll(getScrollX(), getScrollY(), distanceX, getScrollY());
invalidate();
if (mIStatusChangedListener != null){
mIStatusChangedListener.onClose(this);
}
}
public void openSwipeMenu(){
int distanceX = mMenuWidth - getScrollX();
mScroller.startScroll(getScrollX(), getScrollY(), distanceX, getScrollY());
invalidate();
if (mIStatusChangedListener != null){
mIStatusChangedListener.onOpen(this);
}
}
我们看到我们这里是用Scroller对象来操作的滑动,这个类也是为了可以平滑地滑动,我们startScoller完毕然后必须重写下面这个方法:
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
这个方法就是根据总距离和时间来进行平滑地滑动,记得一定要调用invalidate方法。而且我们关闭和打开菜单方法里面还回调了监听器里面的onDown和onOpen方法,这样我们就可以监听到这两个事件。到这里我们就已经讲解完这个控件了。
总结:之所以选这个控件主要是,我们在onInterceptTouchEvent里面我们如果是滑动事件我们拦截了事件,防止被子类消费了我们这个控件啥事件接收不到,而且我们在滑动的时候,我们又告诉我们的父视图,求求你,我滑动了你赶紧不要把我拦截了。所以向上向下都进行了事件了拦截,很典型地利用了事件的分发机制,值得大家思考