EditText与scrollview的滑动冲突

ScrollView为什么会出现嵌套EditText出现滑动事件冲突呢?相信你会有这种疑问,我们来看这么一种情况:

有一个固定高度的EditText,假设它只能显示3行文本,但是,我们在其中输入的文本多余三行时,那么这时就需要可以在EditText内部进行小幅滚动了。那么将这个EditText放入了ScrollView当中, 并且ScrollView内容过多以致ScrollView也可以滑动,这时候就会出现EditText不能滑动的现象。就像下面这张图所示:

上图中,EditText文本的高度已经超出了EditText本身的高度,所以这时EditText应该是可以滑动的,但是由于被放入到了可滑动的ScrollView当中,那么EditText的触摸事件就被屏蔽掉了。我们接下里以非常详细的过程细说触摸事件的分发机制以及这种滑动事件的处理办法。

我们分析的入口是ScrollView的dispatchTouchEvent方法,为什么入口在这里呢,因为该方法是View触摸事件的第一个入口。

由于ScrollView没有重写dispatchTouchEvent,所以我们找到其父类的实现是在ViewGroup当中:


public boolean dispatchTouchEvent(MotionEvent ev) {  

    ...  

boolean handled = false;  

if (onFilterTouchEventForSecurity(ev)) {  

final int action = ev.getAction();  

final int actionMasked = action & MotionEvent.ACTION_MASK;  

        ...  

// Check for interception.  

final boolean intercepted;  

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;  

        }  

        ...  

if (!canceled && !intercepted) {  

            ...  

if (actionMasked == MotionEvent.ACTION_DOWN  

                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)  

                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {  

                ...  

final int childrenCount = mChildrenCount;  

if (newTouchTarget == null && childrenCount != 0) {  

                    ...  

final View[] children = mChildren;  

for (int i = childrenCount - 1; i >= 0; i--) {  

                        ...  

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {  

                            ...  

                            newTouchTarget = addTouchTarget(child, idBitsToAssign);  

alreadyDispatchedToNewTouchTarget =true;  

break;  

            }  

   ...  

                    }  

                }  

  ...  

            }  

        }  

        ...  

    }  

    ...  

return handled;  

}  

这里代码不少,我们挑重点部分看:

上图中,在dispatchTouchEvent中发现了在调用onInterceptTouchEvent方法,而onInterceptTouchEvent方法的触发是有条件的:ACTION_DOWN事件或者mFirstTouchTarget != null,并且设置的disallowIntercept为false。

所以,当我们先触发按下事件时,无论是按到了EditText还是ScrollView,那么首先会调用ScrollView的onInterceptTouchEvent方法,为什么我这么肯定呢,难道disallowIntercept不会被置为true吗?因为在每次按下事件触发时,所有的状态都会被初始化,就算是子View提前请求disallowIntercept为true,那么在每次按下时也会被重置为false。

继续往下,程序会执行到这里:

其中,dispatchTransformedTouchEvent方法会调用每一个子View的dispatchTouchEvent方法,来询问子View是否会处理这次事件。如果子View表示要处理,那么这次事件的目标View就是该子View,那么这里mFirstTouchTarget就会指向这个View,由上面的代码可知,接下来的事件都会询问ScrollView是否要拦截,如果子View没有要求不拦截的话。

这时,这次的按下事件就被传入到了EditText的dispatchTouchEvent中去,由于EditText没有重写dispatchTouchEvent,所以这次调用会在View的dispatchTouchEvent方法中进行:

public boolean dispatchTouchEvent(MotionEvent event) {  

if (onFilterTouchEventForSecurity(event)) {  

        ListenerInfo li = mListenerInfo;  

if (li != null && li.mOnTouchListener != null  

                && (mViewFlags & ENABLED_MASK) == ENABLED  

&& li.mOnTouchListener.onTouch(this, event)) {  

result =true;  

        }  

if (!result && onTouchEvent(event)) {  

result =true;  

        }  

    }  

    ...  

return result;  

}  

View的dispatchTouchEvent要比ViewGroup相对来说简单的多,这里会先调用mOnTouchListener.onTouch方法,如果设置了OnTouchListener的话。不过如果调用了mOnTouchListener.onTouch方法的话,那么View本身的onTouchEvent方法就不会被调用,这两者之间是互斥的。由于我们在这里没有设置OnTouchListener,所以,我们进入onTouchEvent方法,当然这里需要看的是EditText的onTouchEvent方法,该方法位于TextView内部:

总体来说它的内部还是相对简单的,我们挑一些重点来看:

这里有3处方法使用了event对象。先看mEditor.onTouchEvent(event):

void onTouchEvent(MotionEvent event) {  

    updateFloatingToolbarVisibility(event);  

if (hasSelectionController()) {  

        getSelectionController().onTouchEvent(event);  

    }  

if (mShowSuggestionRunnable != null) {  

        mTextView.removeCallbacks(mShowSuggestionRunnable);  

mShowSuggestionRunnable =null;  

    }  

if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {  

        mLastDownPositionX = event.getX();  

        mLastDownPositionY = event.getY();  

// Reset this state; it will be re-set if super.onTouchEvent  

// causes focus to move to the view.  

mTouchFocusSelected =false;  

mIgnoreActionUpEvent =false;  

    }  

}  

这个方法位于Editor类的内部,这个类用于对EditText的编辑做辅助功能,这里不是我们所要关心的,所以返回调用处,进入mMovement.onTouchEvent这个地方:

if (mMovement != null) {  

handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);  

}  

我们由上下文可知,mMovement的实现位于类android.text.method.ArrowKeyMovementMethod的内部:

public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {  

    ...  

boolean handled = Touch.onTouchEvent(widget, buffer, event);  

if (widget.didTouchFocusSelect() && !isMouse) {  

return handled;  

    }  

if (action == MotionEvent.ACTION_DOWN) {  

        f (isMouse || isTouchSelecting(isMouse, buffer)) {  

            ...  

widget.getParent().requestDisallowInterceptTouchEvent(true);  

        }  

}else if (widget.isFocused()) {  

if (action == MotionEvent.ACTION_MOVE) {  

            ...  

}else if (action == MotionEvent.ACTION_UP) {  

            ...  

return true;  

        }  

    }  

return handled;  

}  

我们将不重要的信息删除,发现这里调用了Touch.onTouchEvent(widget, buffer, event)方法,这个方法是这么解释的:Handles touch events for dragging.  You may want to do other actions like moving the cursor on touch as well.那么就是说它是用来辅助处理TextView内部的事件滑动的:

public static boolean onTouchEvent(TextView widget, Spannable buffer,  

                                   MotionEvent event) {  

    DragState[] ds;  

switch (event.getActionMasked()) {  

case MotionEvent.ACTION_DOWN:  

ds = buffer.getSpans(0, buffer.length(), DragState.class);  

for (int i = 0; i < ds.length; i++) {  

            buffer.removeSpan(ds[i]);  

        }  

buffer.setSpan(new DragState(event.getX(), event.getY(),  

                        widget.getScrollX(), widget.getScrollY()),  

0, 0, Spannable.SPAN_MARK_MARK);  

return true;  

case MotionEvent.ACTION_UP:  

        ...  

case MotionEvent.ACTION_MOVE:  

ds = buffer.getSpans(0, buffer.length(), DragState.class);  

if (ds.length > 0) {  

            ...  

if (!event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {  

                    scrollTo(widget, layout, nx, ny);  

                }  

                ...  

return true;  

            }  

        }  

    }  

return false;  

}  

这个方法内部的ACTION_DOWN方法也没有做什么处理,到了这里,事件传递的方法调用栈就应该返回了,但是我们的问题还没解决,就是如何解决事件冲突的问题:

因为一开始,我们就知道ScrollView是否会拦截事件是有条件的,那么,执行了一次ACTION_DOWN之后,唯一我们可以动的地方就是更改disallowIntercept的值,我们通过上下文发现,可以更改这个值的唯一方式就是让子类调用requestDisallowInterceptTouchEvent方法,这个方法会一层层将这个标志传递给父布局容器,最后作用到ScrollView这里。试试在EditText的子类中重写onTouchEvent方法,并且在方法结束之前我们调用requestDisallowInterceptTouchEvent方法,并设置其参数为true,是不是它们之间的事件冲突就可以初步解决呢?

其实,到这里,我们的事件冲突就算解决完成了,但是,我们的标题还说要分析TextView的基本实现,没错,其实,我本身的目的是要实现在EditText在内部滑动到顶部或者底部的时候,要触发外部ScrollView的滑动,那么这里我们就需要对滑动事件的处理以及滑动距离的计算方式了如指掌。有了这个问题,我们就需要从ACTION_MOVE的事件开始分析了,我们还是需要从ViewGroup处开始分析,当然在ViewGroup的dispatchTouchEvent方法中,并没有对ACTION_MOVE进行特殊处理,因为它被全部交给了真是的事件处理对象EditText,所以,按照上面的分析方法来说,这一路分析下来,唯一不同的就是Touch.onTouchEvent(widget, buffer, event)方法,它对ACTION_MOVE进行了特别的处理,就像上面最后一部分代码所展示的那样:

这里经过一系列计算之后,又调用了scrollTo方法:

public static void scrollTo(TextView widget, Layout layout, int x, int y) {  

     widget.scrollTo(x, y);  

}  

这个方法内部经过一系列的计算,又调用了View的scrollTo方法,这里就涉及到了View的scroll方法,这个方法的原理请自行查找,这里只提一下,就是它会滑动它的内容,如果有注意的话,在调用上面方法时会传入一个Layout类型的参数,这是何物呢?其实,这就是EditText滑动时滚动的真正内容,我们所有的文本都是直接被放置在这个layout上,我们可以从EditText的onMeasure方法中找到这个layout对象被实例化的地方,那么,如何监听这个layout滚动时的高度信息呢?

如果观察View的scrollTo方法的话,会得知该方法内部会调用onScrollChanged方法,所以,我们在EditText的子类中重写这个方法就好:

   @Override  

protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {  

super.onScrollChanged(horiz, vert, oldHoriz, oldVert);  

//这里是滑动到底部的示例,滑动到顶部只用计算vert的值是否为0就可以  

//这里可以提前计算好一个值,不用每次进行计算,这里只是做示例  

if (vert == mLayoutHeight + paddingTop + paddingBottom - mHeight) {  

//这里触发父布局或祖父布局的滑动事件  

getParent().requestDisallowInterceptTouchEvent(false);  

       }  

   }  

我来简单解释一下这几个计算参数的作用,如下图所示:

我们实际可滑动的范围就是0~N,N等于 mLayoutHeight + paddingTop + paddingBottom - mHeight,这几个值可在onMeasure方法中获得:

@Override  

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

super.onMeasure(widthMeasureSpec, heightMeasureSpec);  

    mLayout = getLayout();  

    mLayoutHeight = mLayout.getHeight();  

    paddingTop = getTotalPaddingTop();  

    paddingBottom = getTotalPaddingBottom();  

    mHeight = getHeight();  

}  

那么,整个EditText看起来应该是这样的:

import android.content.Context;  

import android.text.Layout;  

import android.util.AttributeSet;  

import android.view.MotionEvent;  

import android.widget.EditText;  

public class MyEditText extends EditText {  

public Layout mLayout;  

public int paddingTop;  

public int paddingBottom;  

public int mHeight;  

public int mLayoutHeight;  

public MyEditText(Context context) {  

super(context);  

        init();  

    }  

public MyEditText(Context context, AttributeSet attrs) {  

super(context, attrs);  

        init();  

    }  

public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) {  

super(context, attrs, defStyleAttr);  

        init();  

    }  

private void init() {  

    }  

@Override  

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

super.onMeasure(widthMeasureSpec, heightMeasureSpec);  

        mLayout = getLayout();  

        mLayoutHeight = mLayout.getHeight();  

        paddingTop = getTotalPaddingTop();  

        paddingBottom = getTotalPaddingBottom();  

        mHeight = getHeight();  

    }  

@Override  

public boolean onTouchEvent(MotionEvent event) {  

boolean result = super.onTouchEvent(event);  

getParent().requestDisallowInterceptTouchEvent(true);  

return result;  

    }  

@Override  

protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {  

super.onScrollChanged(horiz, vert, oldHoriz, oldVert);  

//这里是滑动到底部的示例,滑动到顶部只用计算vert的值是否为0就可以  

//这里可以提前计算好一个值,不用每次进行计算,这里只是做示例  

if (vert == mLayoutHeight + paddingTop + paddingBottom - mHeight) {  

//这里触发父布局或祖父布局的滑动事件,下面这行代码只是示例作用,并没有实现真正的效果  

getParent().requestDisallowInterceptTouchEvent(false);  

        }  

    }  

}  

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容