自定义View、事件体系等基础温习笔记

学习笔记,备忘。完整学习资源:hencoder

一、Canvas 的 drawXXX() 系列方法及 Paint 最常见的使用

  • Canvas类下的所有的draw-打头的方法
  1. 绘制常规的指定图形:drawCircle、drawBitmap等
  2. 绘制自定义图形:drawPath(Path path, Paint paint)

Path的相关方法,细分为两类:

  1. addXxx - 添加子图形,如addCircle
  2. xxxTo - 画线(直线或者曲线 - 贝塞尔曲线),如lineTo等。与1的区别是:1添加完整的封闭图形,而2只是添加一条线。
  3. moveTo(不论是直线还是贝塞尔曲线,都是以当前位置作为起点,而不能指定起点。但是你可以通过moveTo或者rMoveTo来改变当前位置,从而间接地设置这些方法的起点。moveTo 和rMoveTo的区别在于rMoveTo是相对当前位置坐标)等。
  4. Path.setFillType设置填充方式。
  • Paint类的几个常用的方法,例如:
  1. Paint.setStyle(Style style)设置绘制模式。比如Style.Fill、Style.Stroke等
  2. Paint.setColor(int color) 设置颜色
  3. Paint.setStrokeWidth(float width)
  4. Paint.setTextSize(float textSize)
  5. Paint.setAniAlias(boolean aa) 设置抗锯齿开关
  6. Paint.setStrokeCap(cap)设置线条端点形状的方法。端点有圆头(ROUND)、平头(BUTT)和方头(SQUARE)

关于Android中的坐标系:

在Android中,每个View都有自己的视图坐标系。这个坐标系的原点是View左上角的那个点。水平方向是x轴,右正左负。竖直方向是Y轴,下正上负。对于旋转的弧度,顺时针是正,逆时针是负。

二、Paint 的完全攻略

Paint Api大致分为四类:

2.1、颜色

  1. 直接设置颜色:paint.setColor/setRGB/setARGB
  2. 设置着色方案:paint.setShader,在Android的绘制里使用Shader,并不直接用Shader这个类,而是用它的几个子类 - LinearGradient、RadialGradient、SweepGradient、BitmapShader、ComposeShader(可以混合前面几种着色器 - ComposeShader在硬件加速下是不支持两个相同类型的Shader的,需要关闭硬件加速才可以。ComposeShader的第三个参数PorterDuff.Mode用来指定两个图像共同绘制时的颜色策略,颜色策略就是说把源图像绘制到目标图像处时,应该怎样确定两者结合后的颜色。ComposeShader(shaderA,shaderB,mode) - 指应该怎样把shaderB绘制在shaderA上来得到一个结合后的Shader)。注意:在设置了Shader的情况下,paint.setColor/AGRB所设置的颜色就不起作用了。这里看下BitmapShader,该着色器的意思就是用Bitmap的像素来作为图形或者文字的填充。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.batman);  
Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);  
paint.setShader(shader);

...

canvas.drawCircle(300, 300, 200, paint); 

 效果如下:

  •  paint.setColorFilter

为绘制设置颜色过滤。颜色过滤的意思是为绘制的内容设置一个统一的过滤策略,然后Canvas.drawXXX()方法会对每个像素都进行过滤后再绘制出来。比如胶卷效果、有色光照射效果等。

  • setXfermode

Xfermode指的是你要绘制的内容和Canvas的目标位置的内容应该怎样结合计算出最终的颜色。通俗的说就是要你以绘制的内容作为源图像,以View中已有的内容作为目标图像,选取一个PorterDuff.mode作为绘制内容的颜色处理方案。

Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

...

canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方  
paint.setXfermode(xfermode); // 设置 Xfermode  
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆  
paint.setXfermode(null); // 用完及时清除 Xfermode  

PorterDuff.Mode在Paint一共有三处API,它们的工作原理都一样,只是用途不同:

 

另外,设置Xfermode的时候其实是创建的它的子类PorterDuffXfermode。事实上Xfermode也只有这一个子类。

 注意:使用Xfermode的时候,必须使用离屏缓冲,即把内容绘制在额外的层上,再把绘制好的内容贴回View中。

离屏缓存有两种方式:

1、Canvas.saveLayer - 可以做短时间的离屏缓冲。使用方式简单,在绘制代码前后各加一行代码即可。

int saved = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);

canvas.drawBitmap(rectBitmap, 0, 0, paint); // 画方
paint.setXfermode(xfermode); // 设置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 画圆
paint.setXfermode(null); // 用完及时清除 Xfermode

canvas.restoreToCount(saved);

2、View.setLayerType

View.setLayerType() 是直接把整个 View 都绘制在离屏缓冲中。  
setLayerType(LAYER_TYPE_HARDWARE) 是使用 GPU 来缓冲,  
setLayerType(LAYER_TYPE_SOFTWARE) 是直接直接用一个 Bitmap 来缓冲。

如无特殊需求,可以选择第一种方法,以此获取更高的性能。

2.2、效果

效果类Api,指的是

  • 抗锯齿(paint.setAntiAlias);
  • 填充/轮廓(paint.setStyle);
  • 线条宽度(paint.setStrokeWidth);
  • setStrokeCap(设置线头形状,BUTT平头、ROUND圆头、SQUARE方头。默认为BUTT);
  • setStrokeJoin(Paint.Join join)设置拐角的形状,有三个值可以选择:MITER 尖角、 BEVEL 平角和 ROUND 圆角。默认为 MITER;
  • setStrokeMiter是对setStrokeJoin的一个补充,用于设置MITER型拐角的延长线的最大值,即如果拐角的角度太小,有可能由于出现连接点过长的情况,如果过长,则尖角转为平角。
  • Paint的色彩优化有两个方法:setDither(设置图像的抖动,即通过在图像中有意地插入噪点,通过有规律地扰乱图像来让图像对于肉眼更加真实的做法)和setFilterBitmap(图像在放大绘制的时候,如果开启了双线性过滤,就可以让结果图像显得更加平滑),作用是让画面颜色变得更加顺眼。
  • setPathEffect(PathEffect effect)给图形的轮廓设置效果 - 比如虚线的圆、把拐角变平角等,对Canvas的所有图形绘制有效,也就是drawLine、drawCircle等
  • setShaowLayer:在此方法之后绘制的内容,会加一层阴影。清除阴影使用clearShadowLayer。注:硬件加速的情况下,只支持文字的绘制,文字之外的绘制必须关闭硬件加速才可以正常绘制。
  • setMaskFilter:setShadowLayer() 是设置的在绘制层下方的附加效果;而这个 MaskFilter 和它相反,设置的是在绘制层上方的附加效果。

等等这些。

2.3、初始化

  • reSet():重置Paint的所有属性的默认值。相当于重新new一个,不过性能更高一些。
  • set(Paint src):把 src 的所有属性全部复制过来。相当于调用 src 所有的 get 方法,然后调用这个 Paint 的对应的 set 方法来设置它们。
  • setFlags(int flag):批量设置 flags。

三、文字的绘制

3.1、文字绘制的起始坐标

drawText(String text, float x, float y , Paint paint)

x,y是文字的坐标,但是这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。

因此,在绘制文字的时候把坐标填成(0,0),文字并不会显示在View的左上角,而几乎完全显示在View的上方,到了View外部看不到的位置。如下图:

注:其他的drawXXX都是以左上角作为基准点的,而drawText却是文字左下方。drawText中的y指的是文字的基线。

3.2、drawTextOnPath

沿着一条Path来绘制文字。

3.3、StaticLayout

Canvas.drawText只能绘制单行文字,不能换行。要想实现绘制多行文字,可以使用StaticLayout。

3.4、文字绘制辅助

  • setTextSize:设置文字大小;
  • setTypeface:设置字体;
  • setFakeBoldText:是否使用为粗体;
  • setStrikeThruText:是否加删除线;
  • setUnderlineText:是否加下划线;
  • setTextSkewX:文字倾斜度;
  • setTextScaleX:字体横向放缩;
  • setLetterSpacing:设置字符间距;

3.5、测量字体尺寸类

  • getFontSpaceing:获取推荐的行距;
  • getFontMetrics:获取Paint的FontMetrics。FontMetrics。是一个相对专业的工具类。提供了几个文字排印方面的数值:ascent、descent、top、bottom、leading。
  • getTextBounds:获取文字的显示范围; - 测量文字的显示范围;
  • measureText:测量文字的宽度并返回。测量文字绘制时所占用的宽度;因此,measureText测量出来的值总是比getTextBounds测量出来的值大一些。

四、Canvas 对绘制的辅助——范围裁切和几何变换

4.1、范围裁切

  • clipRect
记得要加上 Canvas.save() 和 Canvas.restore() 来及时恢复绘制范围

canvas.save();  
canvas.clipRect(left, top, right, bottom);  
canvas.drawBitmap(bitmap, x, y, paint);  
canvas.restore();  
  • clipPath

用法与clipRect完全一样,只是把参数换成了Path。可以裁切的形状更多一些。

4.2、几何变换

  • 使用 Canvas 来做常见的二维变换;

Canvas .translate、Canvas .scale、Canvas .rotate、skew(错切)

注:变换前后注意保存与恢复绘制范围。另外,Canvas的几何变换顺序反的,比如你想先平移再缩放,就需要把缩放的代码写在平移的代码前面。

canvas.save();  
canvas.skew(0, 0.5f);  
canvas.drawBitmap(bitmap, x, y, paint);  
canvas.restore();
  • 使用 Matrix 来做常见和不常见的二维变换;

Matrix 做常见变换的方式:

  1. 创建 Matrix 对象;
  2. 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas

上面已经说了,Canvas的几何变换顺序是反的,但是Matrix可以自己来定义顺序。preXXX往前插入变换操作,postXXX就是往后面插入平移。

Matrix matrix = new Matrix();

...

matrix.reset();  
matrix.postTranslate();  
matrix.postRotate();

canvas.save();  
canvas.concat(matrix);  
canvas.drawBitmap(bitmap, x, y, paint);  
canvas.restore(); 

效果和Canvas是一样的。把Matrix应用到Canvas有两个方法:Canvas.setMatrix和Canvas.concat:

  1. Canvas.setMatrix(matrix):用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换(注:根据下面评论里以及我在微信公众号中收到的反馈,不同的系统中 setMatrix(matrix) 的行为可能不一致,所以还是尽量用 concat(matrix) 吧);
  2. Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。

 可以使用Matrix来做自定义变换。自定义变换使用的是Matrix.setPolyToPoly方法。

  • 使用 Camera 来做三维变换。

Camera三维变换有三类:旋转、平移、移动相机。

Camera的工作原理:

与二维的View坐标系不同,x轴 - 右正左负,y轴 - 上正下负,z轴 - 外负里正

相机的位置位于图中的小黄点,可以通过setLocation来改变相机在z轴的位置。

1、三维旋转:rotateX(deg) rotateY(deg) rotateZ(deg) rotate(x, y, z);

canvas.save();

// Camera和Canvas一样,也需要保存和恢复状态才能正常绘制,不然在界面刷新之后,会出现问题
camera.save(); // 保存 Camera 的状态  
camera.rotateX(30); // 旋转 Camera 的三维空间  
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas  
camera.restore(); // 恢复 Camera 的状态

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);  
canvas.restore(); 

 2.translate/setLocation

五、使用不同的绘制方法来控制绘制顺序

一个完整的绘制过程会依次绘制以下几个内容:

  1. 背景
  2. 主体(onDraw()
  3. 子 View(dispatchDraw()
  4. 滑动边缘渐变和滑动条
  5. 前景

一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

这其中的第 2、3 两步,前面已经讲过了;第 1 步——背景,它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法,这个每个人都用得很 6 了),而不能自定义绘制;而第 4、5 两步——滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。

 

draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。前面讲到的背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。

// View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):

public void draw(Canvas canvas) {  
    ...

    drawBackground(Canvas); // 绘制背景(不能重写)
    onDraw(Canvas); // 绘制主体
    dispatchDraw(Canvas); // 绘制子 View
    onDrawForeground(Canvas); // 绘制滑动相关和前景

    ...
}

注意

关于绘制方法,有两点需要注意一下:

  1. 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false)这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。
  2. 有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。

View滑动之ScrollTo与ScrollBy、getScrollX/Y

  • scrollTo:实现了基于所传递参数的绝对滑动;
  • scrollBy:实现了基于当前位置的相对滑动;实际上scrollBy也是调用了scrollTo;
  • 使用scrollTo和scrollBy来实现View的滑动,只能将View的内容进行移动,并不能将View本身进行移动
  • scrollX/Y正负值问题:View的左边缘在View的内容左边缘右边时,scrollX的值为正,否则,为负。

View的事件体系

父View的事件拦截分析,比如线性布局、相对布局,并没有复写相关方法,而是直接使用的ViewGroup基类提供的的方法逻辑,比如事件分发、拦截、处理等,所以分析了ViewGroup基类的各相关方法,也就分析了各子类的事件拦截分析。
     ------  ViewGroup的事件分发流程  ------    
public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if (onInterceptTouchEvent(ev)) {
            consume = onTouchEvent(ev);
        } else {
    ------------------------
       // Dispatch to touch targets.
      if (mFirstTouchTarget == null) {
       // No touch targets so treat this as an ordinary view.
         consume = super.dispatchTouchEvent(ev)// -- 1
      } else {
         consume = child.dispatchTouchEvent(ev);
      }
    ------------------------
     }
        return consume;
   }

1 处:注意:ViewGroup的父类是View,因此接下来走的是View的dispatchTouchEvent方法。

上述伪代码将涉及事件拦截的三个方法的关系表现的淋漓尽致。

  • dispatchTouchEvent:如果事件能够传递给当前View,则此方法一定会被调用;
  • onInterceptTouchEvent:如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用;View无该事件;
  • onTouchEvent:如果此方法返回false,则在同一事件序列中,当前View不会再次接收到该事件。
------  View默认的事件处理流程  ------
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null
            && enable
            && mOnTouchListener.onTouch(this, event)) {
                result = true;
        }

    if (!result && onTouchEvent(event)) {
         result = true;
     }
}

// 不同的子View可能需要复写onTouchEvent,比如TextView
public boolean onTouchEvent(MotionEvent event) { 

  if (!enable) {
   // A disabled view that is clickable still consumes the touch
   // events, it just doesn't respond to them.
     return clickable;
  }
  
  if (clickable) {
  ----------事件处理开始---------
    switch(event.getAction()){
     case MtionEvent.ACTION_UP:
       // 在UP事件中处理onClick
     if (mOnClickListener != null) {
            mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
     break;
     ......
    }
  ----------事件处理完成---------
  return true;// clickable为true,则直接返回true
 }
 return false;

}

OnTouchListener、onTouchEvent、OnClickListener的优先级问题

   //dispatchTouchEvent中的部分源码
            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;
            }

可以看出OnTouchListener的优先级高于onTouchEvent;
注:OnTouchListener是需要外部设置的,onTouchEvent是View内部的事件拦截处理回调方法。

优先级:OnTouchListener > onTouchEvent > OnClickListener,即当一个View进行事件处理时,如果设置了OnTouchListener,那么OnTouchListener中的onTouch方法将会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则View的onTouchEvent方法会被调用,如果返回true,那么onTouchEvent方法将不会被调用。在onTouchEvent方法中,如果当前设置了OnClickListener,那么它的onClick方法会被调用。

事件的传递方向

当一个点击事件产生后,传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件之后,就会按照事件分发机制去分发事件。极端情况:如果所有的元素都不处理这个事件,那么这个事件将会最终再传递给Activity处理,即Activity的onTouchEvent方法会被调用。

事件传递机制的一些结论

  • 同一个事件序列是指从手指触摸屏幕的那一刻起,到手指离开屏幕的那一刻结束。在这一过程中会所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
  • 正常情况下,一个事件序列只能被一个View拦截且消耗。
  • 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一个事件序列中的其他事件都不会交给它处理了,并且事件将重新交由它的父元素处理,即父元素的onTouchEvent会被调用。
  • 如果View不消耗除了ACTION_DOWN以外的其他事件,那么这个点击事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会被传递给Activity处理。
  • ViewGroup默认不拦截任何事件。注意是ViewGroup,不是ViewGroup的子类。
  • View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法将会被调用。
  • View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认值是false,clickable属性要分情况,比如Button的clickable默认是true,TextView的clickable默认为false;
  • View的enable属性不影响onTouchEvent的默认返回值;;
  • onClick会发生的前提是当前的View是可点击的,并且它收到了down和up的事件;
  • 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外;为什么呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。 - ViewGroup会在事件类型是ACTION_DOWN时,判断是否拦截当前事件。

onInterceptTouchEvent的事件接管与onTouchEvent事件处理、ACTION_CANCEL

onTouchEvent是否消费一组事件,是需要在DOWN事件中决定的,如果你在DOWN事件发过来的时候返回了false,以后你就跟这组事件无缘了,没有第二次机会。

而onInterceptTouchEvent则是你在整个过程中对事件流中的每个事件进行监听,你可以选择先行观望,给子View一个处理事件的机会,而一旦事件流的发展达到了你的触发条件,比如你发现用户是在滑动了,这个时候你再返回true,立刻就可以实现事件流的接管。这样就做到了两不耽误,既让子View有机会去处理事件,又可以在需要的时候把处理事件的工作接管过来。

当onInterceptTouchEvent返回true的时候,除了完成事件的接管,这个View还会做一件事,就是它会对它的子View发送一个额外的取消事件ACTION_CANCEL。因为你在接管事件的时候子View可能正处在一个中间的状态,比如用户先按下了一个按钮,然后手指一滑,你就知道用户是要滑动,这个时候你就把事件拦截接管了,但是现在上面的那个按钮它是按下状态,你需要让它恢复,因此,在onInterceptTouchEvent返回true的时候,子View会接收到一个CANCEL事件,告诉他通知他,这个事件序列你不要接管了,把状态恢复吧。

由上可以看出,事件流的结束有两种情况:

  • ACTION_DOWN -> ACTION_MOVE -> ...... -> ACTION_UP
  • ACTION_DOWN -> ACTION_MOVE -> ...... -> ACTION_CANCEL(非人为的情况)

滑动冲突解决方案

  • 外部拦截法 - 父容器的onInterceptTouchEvent

父容器决定是否拦截事件。

// onInterceptTouchEvent 一旦完成了拦截,该方法不再调用 
   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isIntercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            // 拦截后的父View可以处理MOVE和UP事件,但是没有DOWN事件了,因为同一事件序列的DOWN事件触发完成,子View已经消费了该事件
            if(父容器需要当前点击事件){
                isIntercepted = true;
            }else{
                isIntercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            isIntercepted = false;
            break;

        default:
            break;
        }
        mLastX = x;
        mLastY = y;
        return isIntercepted;
    }

ACTION_DOWN必须返回false,否则一旦父容器拦截了ACTION_DOWN,那么后续的事件都会直接交由父容器处理了。其次,ACTION_UP事件这里也必须返回false,因为如果返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发。父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都交给它处理了。即父容器是Boss,可以随时接管。

  • 内部拦截法 - 子View的dispatchTouchEvent

父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则,交给父容器进行处理。这种处理方案需配合requestDisallowInterceptTouchEvent完成。

父:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        }else{
            return true;
        }
    }

除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用requestDisallowInterceptTouchEvent(false)的时候,父元素才能继续拦截所需的事件。

子:

  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            int dx = x - mLastX;
            int dy = y - mLastY;
            if(父View需要当前点击事件){
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            
            break;
        default:
            break;
        }
        
        mLastX = x;
        mLastY = y;
        
        return super.dispatchTouchEvent(ev);
    }

父容器为啥不能拦截ACTION_DOWN呢?因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT这个标记位的影响,所以一旦父容器拦截了ACTION_DOWN,那么所有的事件都无法传递给子元素了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值