当前位置: 首页 > article >正文

Android View

前面我们了解了Android四大组件的工作流程,Android中还存在一个和四大组件地位相同的概念:View,用于向用户页面展示内容。我们经常使用的TextView、Button、ImageView控件等都继承于它,也会自定义View实现自定义效果。View类源码内容很庞大,有上万行。

文章目录

      • 首语
      • 位置参数
      • 触摸
        • TouchSlop
        • VelocityTracker
        • GestureDetector
        • Scroller
      • 滑动
        • scrollTo/scrollBy
        • 动画添加平移效果
          • View动画
          • 属性动画
        • 改变LayoutParams让View重新布局
        • layout方法
        • 弹性滑动
          • Scroller
        • 延时策略
      • 事件分发机制
        • 机制总结
        • 源码解析
      • 滑动冲突
        • 滑动冲突场景
        • 处理规则
        • 解决方式
          • 外部拦截法
          • 内部拦截法
      • View 工作原理
        • ViewRoot/ViewRootImpl
        • MeasureSpec
      • View工作流程
        • measure 过程
          • View的measure过程
          • ViewGroup的measure过程
          • 准确获取某个View的宽高方法
        • layout过程
        • draw过程
      • RemoteViews
        • 原理
      • 总结

首语

View它是一种控件,Android所有控件的基类,是给用户视觉上的一种呈现。ViewGroup内部包含了许多控件,但它继承于View,说明View可以是单个控件也可以是多个控件组成的一组控件。

位置参数

Android系统中有两种坐标系,分别是Android坐标系和View坐标系。

Android坐标系是将屏幕左上角作为原点,原点向右为x轴正方向,原点向下为y轴正方向。

View坐标系中View的位置是由四个顶点(Top,Left,Right,Bottom)决定,Top是左上角纵坐标,Left是左上角横坐标,Right是右下角横坐标,Bottom是右下角纵坐标。这些坐标都是相对于父布局来说的,是一种相对坐标。其中Top=getTop;Left=getLeft;Right=getRight;Bottom=getBottom。View的宽为width=getRightgetLeft=getWdith。View的高height=getBottomgetTop=getHeight,源码计算也是如此,具体位置坐标可见下图。
View位置坐标

中间的Motion指的是用户手指接触屏幕的点。使用getRawXgetRawY方法获取的是Android坐标系的坐标,使用getXgetY方法 获取的是视图坐标,相对于View的距离。这些方法都在处理移动和触摸事件的MotionEvent类中。

触摸

手指接触到屏幕的一瞬间会产生一系列事件。

  • ACTION_DOWN,手指接触屏幕
  • ACTION_UP,手指抬起
  • ACTION_MOVE,手指在屏幕上移动
public final class MotionEvent extends InputEvent implements Parcelable {
   public static final int ACTION_DOWN             = 0;
   public static final int ACTION_UP               = 1;
   public static final int ACTION_MOVE             = 2;
   //手势已经终止,不会受到更多触摸点信息
   public static final int ACTION_CANCEL           = 3;
   public static final int ACTION_OUTSIDE          = 4;
   .... 
}

点击屏幕后松开,事件序列为DOWN->UP,点击屏幕滑动然后松开,事件序列为DOWN->MOVE->UP。

TouchSlop

TouchSlop是系统能识别出滑动的最小距离,它是一个常量。如果手指在屏幕滑动的距离小于这个常量,系统则不认为这是一个滑动事件。可以通过以下API获取这个常量。查看源码它其实是通过frameworks config.xml中定义的config_viewConfigurationTouchSlop控制。

 val touchSlop: Int = ViewConfiguration.get(this).scaledTouchSlop
    <!-- Base "touch slop" value used by ViewConfiguration as a
         movement threshold where scrolling should begin. -->
    <dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。使用很简单,在View的onTouchEvent方法中追踪当前点击事件的速度,然后通过computeCurrentVelocity计算速度,这里的速度指的是一段时间内划过的像素数,比如将时间间隔设为1000ms,在1s内手指在水平方向从左向右划过100像素,那么水平速度就是100,当手指从右往左滑动时,速度可以为负数,具体公式为速度=(终点位置-起点位置)/时间段,接着通过getXVelocitygetXVelocity方法获取水平和竖直速度。最后计算完成重置并回收内存。

override fun onTouchEvent(event: MotionEvent?): Boolean {
        val velocityTracker = VelocityTracker.obtain()
        velocityTracker.addMovement(event)
        velocityTracker.computeCurrentVelocity(1000)
        val xVelocity = velocityTracker.xVelocity.toInt()
        val yVelocity = velocityTracker.yVelocity.toInt()
        velocityTracker.clear()
        velocityTracker.recycle()
        return super.onTouchEvent(event)
    }
GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。使用过程如下,首先创建一个GestureDetector实例,实现OnGestureListener监听,也可以实现OnDoubleTapListener监听双击行为,接着在View的onTouchEvent方法中设置event。最后就可以在各个回调方法处理逻辑了。

gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
            override fun onDown(e: MotionEvent): Boolean {
                //按下
            }
            override fun onShowPress(e: MotionEvent) {
                //按下没有移动或松开
            }
            override fun onSingleTapUp(e: MotionEvent): Boolean {
                //按下松开
            }
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                //按下拖动
            }
            override fun onLongPress(e: MotionEvent) {
                //长按
            }
            override fun onFling(
                e1: MotionEvent,
                e2: MotionEvent,
                velocityX: Float,
                velocityY: Float
            ): Boolean {
                //按下快速滑动后松开
            }
        })
override fun onTouchEvent(event: MotionEvent?): Boolean {
        return event?.let { gestureDetector.onTouchEvent(it) }
    }
Scroller

弹性滑动对象,用于实现View的弹性滑动, 它不是瞬间完成,而是在一定时间间隔内完成的。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用完成。

private val scroller = Scroller(context)
private fun smoothScrollTo(destX: Int, destY: Int) {
        val delta = destX - scrollX
    	//1000ms滑向destX
        scroller.startScroll(scrollX, 0, delta, 0, 1000)
        invalidate()

    }
    override fun computeScroll() {
        if (scroller.computeScrollOffset()){
            scrollTo(scroller.currX,scroller.currY)
            postInvalidate()
        }
    }

监听滑动相关的,建议在onTouchEvent中实现,监听双击这种行为,建议在GestureDetector实现。

滑动

Android设备由于屏幕有限,为了给用户呈现更多的内容,就需要使用滑动来隐藏或显示一些内容,因此,掌握滑动的方法实现是重要的。它的基本思想是当触摸事件传递给View时,记录下触摸点坐标,手指移动时记下移动后触摸的坐标,计算出偏移量,通过偏移量来改变View的坐标。可以通过以下方式实现View的滑动:

scrollTo/scrollBy

View提供了专门的方法来实现滑动,通过scrollToscrollBy方法,先看这两个方法的实现。scrollTo表示移动到一个具体的坐标点,而scrollBy表示移动的增量,scrollBy最终也会调用scrollTo。

public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
动画添加平移效果

通过动画让View进行平移,可以通过View动画和属性动画。

View动画

在res目录新建anim文件夹并创建动画文件,然后在代码中引用。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
     <!--保留动画后的状态-->
    <translate
        android:fromXDelta="0"
        android:toXDelta="100"
        android:fromYDelta="0"
        android:toYDelta="50"
        android:duration="500" />
</set>
val imageView = findViewById<ImageView>(R.id.my_image_view)  
val animation = AnimationUtils.loadAnimation(this, R.anim.translate_animation)  
imageView.startAnimation(animation)

这里需要注意的是,View动画并不能真正改变View的位置,它设置的点击事件在新位置不会生效,这是因为View本身没有发生改变,新位置只是它的分身而已。因此,通过动画实现View滑动请使用属性动画。

属性动画

myButton在水平方向向右平移200像素。通过操作translationX和translationY属性。

val myButton = findViewById<Button>(R.id.myButton)   
val animator = ObjectAnimator.ofArgb(myButton, "translationX", 0,200) 
animator.duration = 2000 // 2秒  
animator.start()
改变LayoutParams让View重新布局

LayoutParams保存了View的布局参数,通过改变View的布局参数margin就可以实现滑动的效果。

 val button = findViewById<TextView>(com.google.android.material.R.id.accelerate)
 val layoutParams : MarginLayoutParams= button.layoutParams as MarginLayoutParams
 layoutParams.width+=100
 layoutParams.leftMargin+=100
 button.layoutParams=layoutParams
layout方法

view绘制的时候会调用onLayout方法来设置显示的位置,因此可以通过修改view的left、top、bottom、right来控制View的坐标。offsetLeftAndRightoffsetTopAndBottom方法也可以设置左右和上下的偏移值。

private var lastX: Int = 0
    private var lastY: Int = 0
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        if(event != null){
            val x = event.x.toInt()
            val y = event.y.toInt()
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    lastX =x
                    lastY = y
                }
                MotionEvent.ACTION_MOVE -> {
                    val offsetX = x -lastX
                    val offsetY = y -lastY
                    layout(left+offsetX,top+offsetY,right+offsetX,bottom+offsetY)
                    //对Left和right进行偏移
                   // offsetLeftAndRight(offsetX)
                    //对top和bottom进行偏移
                   // offsetTopAndBottom(offsetY)
                }
            }
            return true
        }
        return super.onTouchEvent(event)
    }
弹性滑动

生硬的滑动用户体验很差,要实现渐进式滑动,提高用户体验,核心思想是将一个滑动分为若干个小滑动。

Scroller

Scroller使用我们在前面已经提到了,看下startScroll的源码,发现它内部只是做了几个参数的传递,startX和startY表示的是滑动的起点,dx和dy表示的要滑动的距离,duration表示的是滑动时间。那么它是如何实现滑动的呢?是它下面的invalidate方法,invalidate方法会导致view重绘,View的draw方法里会调用computeScroll方法,computeScroll方法会向Scroller获取当前的scrollX和scrollY,然后通过scrollTo滑动,接着又调用postInvalidate方法进行第二次重绘,滑动到新位置,如此反复。

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }
延时策略

通过发送一系列延时消息从而达到一种渐进式效果。Handler/View的postDelayd/线程的sleep。

事件分发机制

View事件分发是针对点击事件,所谓点击事件的事件分发,就是对MotionEvent事件的分发过程,系统将一个事件传递给一个具体的View,而这个传递过程就是分发过程。

View事件分发的本质是递归过程。点击事件自上而下是传递过程,点击事件自下向上是归过程。当一个点击事件产生后,它的传递过程遵循如下顺序,Activity->Window->ViewGroup->View,这个自上而下的过程是传递过程。顶级View接收到事件后,就会去分发事件,这个自下向上的是归过程。点击事件的分发过程是由三个很重要的方法来共同完成。

  • dispatchTouchEvent,用来进行事件的分发,如果事件能传递给当前View,那么它一定会被调用,返回结果受当前View的onTouchEvent方法和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件,针对Viewgroup。
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        return super.dispatchTouchEvent(event)
    }
  • onInterceptTouchEvent,用于判断是否拦截某个事件,如果当前View拦截了某个事件,同一个事件序列中,此方法不会再调用。
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return super.onInterceptTouchEvent(ev)
    }
  • onTouchEvent,在dispatchTouchEvent方法中调用,用于处理触摸事件,返回结果表示是否消耗当前事件,不消耗,则在同一个事件序列中,View无法再次接接收该事件。
override fun onTouchEvent(event: MotionEvent?): Boolean {
        return super.onTouchEvent(event)
    }

View接收到事件后,由于没有onInterceptTouchEvent方法,所以事件传递给它后,它的onTouchEvent方法就会被调用。onTouchEvent方法如果处理触摸事件,返回true,则不会继续归流程,如果不处理,则会继续归流程,中间没有处理的话,最终回到Activity。

ViewGroup接收到事件后,这时它的dispatchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true,表示拦截当前事件,那点击事件就会交给ViewGroup处理,它的onTouchEvent方法会被调用。如果返回false,表示不拦截当前事件,则会传递给子View,子View的dispatchTouchEvent调用。

一个View需要处理事件时,如果设置了onTouchListener,那么onTouchListener的onTouch方法会被回调,如果返回true,onTouchEvent方法将不会被会回调,因此可见onTouchListener会比onTouchEvent方法优先级更高,在onTouchEvent方法中,如果设置有onClickListener,那么onClick方法会被回调,优先级最低。总结优先级高低为:onTouch->onTouchEvent->onClick。

机制总结
  • View的onTouchEvent默认都会消耗事件,除非它是不可点击的(clickable和longClickable为false)。
  • View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable的。
  • View的onClick发生的前提是当前View是可点击的,并且收到了点击事件。
  • 事件传递是由上向下的,ViewGroup传递给子View,通过requestDisallowInterceptTouchEvent方法可以在子View处理ViewGroup的事件分发过程。
源码解析

当一个触摸事件产生时,这个触摸行为则是通过底层硬件来传递捕获,然后交给ViewRootImpl,接着将事件传递给DecorView,而DecorView再交给PhoneWindow,PhoneWindow再交给Activity,然后接下来就是我们常见的View事件分发了。从底层到输入系统(InputManagerService),后面参考输入系统解析。
首先传递给Activity,由Activity的dispatchTouchEvent方法来进行分发,具体的工作是由Activity内部的Window来完成的,Window会将事件传递给decorview,decorview是当前界面的底层容器(setContentView设置的View的父容器),可通过getWindow().getDecorView()进行获取。

从源码可以看出,首先通过Window进行分发,如果返回true表示事件处理结束了,返回false表示没有View处理,Activity的onTouchEvent方法就会被调用。返回false表示触摸事件无响应。

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //每当一个键盘、触摸或轨迹球事件被分发到活动时,都会调用此方法
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

Window是一个抽象类,而superDispatchTouchEvent也是一个抽象方法,它的唯一实现是PhoneWindow类。

//frameworks\base\core\java\com\android\internal\policy\PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
   private DecorView mDecor;
   @Override
   public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
   } 
}

DecorView继承于FrameLayout,FrameLayout继承于ViewGroup,dispatchTouchEvent方法实现在ViewGroup类中。

//frameworks\base\core\java\com\android\internal\policy\DecorView.java
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

ViewGroup类的dispatchTouchEvent方法代码庞大,分开解释。

  1. 首先收到ACTION_DOWN事件的话需要初始化,cancelAndClearTouchTargetsclearTouchTargets方法取消和清空所有的 touch targets。resetTouchState方法重置所有触摸状态以准备接收新触摸事件。
//frameworks/base/core/java/android/view/ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
        //应用安全策略过滤触摸事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            .....
}
private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
  1. 接下来判断是否需要拦截事件,由于是第一步ACTION_DOWN事件初始化会让mFirstTouchTarget为null。如果有子View接收了事件,那么mFirstTouchTarget就会被赋值,从后面子View分发事件可以看到。

子View可以调用父View的requestDisallowInterceptTouchEvent方法来设置mGroupFlags的值,告诉父View不要拦截事件。

如果disallowIntercept 为true,说明子View要求父View不要拦截,intercepted为false。

如果disallowIntercept 为false,说明子View没有要求父View拦截,那父View调用onInterceptTouchEvent方法看自己是否需要拦截。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        .....1.....
    		// 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 {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }      
        ....
    }
}
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
//默认不拦截,一般会重写拦截逻辑
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
  1. 拦截处理完成后,就会进行触摸事件的分发,首先会遍历所有子View,将触摸事件分发给子View。
  • 如果当前子View不存在焦点,不会分发。

  • 如果当前子View不可见且没有播放动画或不在触摸范围,不会分发。

  • 如果触摸列表找到了与该View对应的TouchTarget,说明该View正在接收事件,不需要再遍历,直接退出。

如果子View在触摸位置,调用dispatchTransformedTouchEvent方法将事件分发给子View,返回true表示消费了该事件,跳出遍历。

继续调用addTouchTarget 方法给mFirstTouchTarget 赋值了,这里就可以说明第二步,如果子View消费了事件,那么mFirstTouchTarget 不会为空,后续的move/up事件继续分发给这个TouchTarget。当子View没有消费事件或被拦截,那么mFirstTouchTarget 为空,这样这个事件就交给ViewGroup去处理了,从dispatchTransformedTouchEvent方法中可以看到,不论child是否为空,最终都会去调用View.dispatchTouchEvent方法。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ..............
    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
    //当前事件没有取消且没有拦截
    if (!canceled && !intercepted) {
                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
        		//获取焦点view
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍历子View
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            //没有焦点跳过
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount;
                            }
                            //不可见且没有播放动画或没在触摸范围内跳过
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            newTouchTarget = getTouchTarget(child);
                            //找到了对应的TouchTarget,说明这个View正在接收事件,不需要再遍历,退出
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            resetCancelNextUpFlag(child);
                            //处于触摸位置,分发给子View,返回true表示消费了这个事件,跳出
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //mFirstTouchTarget 赋值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    //没有子View接收事件,把最近一次的触摸目标赋值给newTouchTarget,先前接收的View
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
    //说明没有子View消费这个事件或被拦截
    if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
        		//ViewGroup自己处理事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
        		//子View消费了事件,后续的move/up事件继续分发给这个TouchTarget
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //设置了PFLAG_CANCEL_NEXT_UP_EVENT或被拦截,子View需要取消事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //分发事件给子View
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            // Update list of touch targets for pointer up or cancel, if needed.
            //当发生抬起或取消事件,更新触摸目标列表
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                //多点触摸下的抬起事件,根据idBits移除对应的触摸点
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
}
private static boolean resetCancelNextUpFlag(@NonNull View view) {
        if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
            view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
            return true;
        }
        return false;
    }
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }
        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    //子View不存在,ViewGroup调用View的dispatchTouchEvent分发事件,再调用ViewGroup的onTouchEvent 处理事件
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);
					//分发给子ViewGroup或子View
                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
  1. 第三步提到触摸事件分发给子View会调用它的dispatchTouchEvent方法。

如果给View设置了OnTouchListener 监听,且在onTouch方法中返回了true,说明View消费了事件。

如果没有设置,那就调用onTouchEvent方法进行处理事件。从源码可以看出OnTouchListener.onTouch方法是先于onTouchEvent方法的,onClickonLongClickonTouchEvent 中被调用,且 onLongClick 优先于 onClick 被执行;如果 onTouch 返回 true,就不会执行 onTouchEvent;onTouch 只有 View 设置了 OnTouchListener,且是 enable 的才执行该方法。

onTouchEvent方法中,可以看到,只要这个View满足CLICKABLE/LONG_CLICKABLE/CONTEXT_CLICKABLE其中一种,不论View状态是否是禁用状态,它都是返回true,代表消费事件。View的longClickable默认为false,clickable根据控件也不一样,例如button的clickable为true,TextView为false,但是View的setOnclickListener/setOnLongClickListener会将clickable/longClickable设置为true。

ACTION_DOWN事件中,如果是长按回调OnLongClickListener.onLongClick方法。ACTION_UP事件中,回调OnClickListener.onClick方法。

源码路径:frameworks/base/core/java/android/view/View.java

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            //停止滚动操作
            stopNestedScroll();
        }
    	//应用安全策略过滤触摸事件
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //设置了TouchListener且enabled
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //TouchListener 没有消费,onTouchEvent方法返回true
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
       //其它事件也停止滚动
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }
public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    	//view disabled
        if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them
            //View禁用,但是满足单击,长按,右击都是消费了事件,只是没有响应
            return clickable;
        }
        //扩大View实际触摸区域,称为委托视图,通过setTouchDelegate方法设置
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
    	//可点击或悬停、长按时显示工具提示
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        //获取焦点处于可触摸模式
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //调用View.OnClickListener
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();
                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY
                        //在滚动布局内延迟100模式,反馈按压状态,判断用户是否想滚动
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        //不再滚动,立即反馈按压状态
                        setPressed(true, x, y);
                        //检测长按,是则回调OnLongClickListener.onLongClick方法
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }
                    final int motionClassification = event.getClassification();
                    final boolean ambiguousGesture =
                            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                    int touchSlop = mTouchSlop;
                    if (ambiguousGesture && hasPendingLongPressCallback()) {
                        if (!pointInView(x, y, touchSlop)) {
                            // The default action here is to cancel long press. But instead, we
                            // just extend the timeout here, in case the classification
                            // stays ambiguous.
                            removeLongPressCallback();
                            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                    * mAmbiguousGestureMultiplier);
                            // Subtract the time already spent
                            delay -= event.getEventTime() - event.getDownTime();
                            checkForLongClick(
                                    delay,
                                    x,
                                    y,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        }
                        touchSlop *= mAmbiguousGestureMultiplier;
                    }
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, touchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    final boolean deepPress =
                            motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                    if (deepPress && hasPendingLongPressCallback()) {
                        // process the long click action immediately
                        removeLongPressCallback();
                        checkForLongClick(
                                0 /* send immediately */,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                    }
                    break;
            }
            return true;
        }
        return false;
    }
public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

流程图梳理如下:
事件分发机制总结

滑动冲突

滑动冲突场景
  1. 外部滑动方向和内部滑动方向不一致。

ViewPager和Fragment配合使用组成的页面滑动效果,主流应用都在使用,这种效果中,通过左右滑动来切换页面,每个页面存在一个ListView,上下滑动查看内容,这种场景本身应该存在滑动冲突,但是ViewPager内部处理了这种滑动冲突,因此采用ViewPager无须考虑这个问题,如果采用的是ScrollView而不是ViewPager,那就必须手动处理滑动冲突了,由于滑动冲突内外两层只有一层能滑动,还有其它场景诸如外部上下滑动,内部左右滑动等,属于同一类滑动冲突。

  1. 外部滑动方向和内部滑动方向一致。

内外两层都在同一方向,当手指滑动时,系统无法知道用户时想让那一层滑动,所以会出现只有一层能滑动或滑动卡顿。

  1. 以上两种场景的嵌套。
处理规则
  • 场景一

当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件,根据滑动过程中两个点之间的坐标可以得出到底是水平滑动还是竖直滑动,如何判断滑动方向呢?可以参考滑动路径和水平方向形成的夹角,也可以根据水平方向和竖直方向上的距离差来判断,与那个方向形成的夹角小,就按照那个方向滑动,那个方向距离差大,就按照那个方向滑动。

  • 场景二

这个需要根据业务来判定,什么状态需要外部View来响应滑动,另外一种状态需要内部View来响应滑动。

  • 场景三

根据业务来判定,制定不同的方案。

解决方式
外部拦截法

外部拦截法是指点击事件经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截,这样就解决滑动冲突问题,也就是通过事件分发机制,需要重写父容器的onInterceptTouchEvent方法,做相应的拦截即可。

以下是外部拦截法的伪代码,ACTION_DOWN事件,父容器必须返回false,这是因为父容器一旦拦截ACTION_DOWN事件,那么ACTION_MOVE和ACTION_UP事件都会交给它处理,这个从事件分发机制的源码分析就清楚。核心处理在ACTION_MOVE事件中,ACTION_UP事件中也必须返回false,否则会导致子元素的onClick事件无法正常响应。

private var downX: Float = 0F
    private var downY: Float = 0F
 
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                downX = ev.x
                downY = ev.y
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = ev.x - downX
                val deltaY = ev.y - downY
 
                // 根据滑动方向判断是否拦截事件
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    return true
                }
            }
        }
        return super.onInterceptTouchEvent(ev)
    }
 
    override fun onTouchEvent(event: MotionEvent): Boolean {
        // 处理滑动逻辑
        return true
    }	
内部拦截法

内部拦截法指父容器不拦截任何事件,所有事件传递给子元素,如果子元素需要就消耗掉,否则交给父容器处理,这和事件分发机制相悖,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,伪代码如下:需要重写dispatchTouchEvent方法

// 通过重写 dispatchTouchEvent 方法实现内部拦截
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 按下时,禁止父View拦截事件
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                // 根据业务逻辑判断是否拦截事件
                if (shouldInterceptTouchEvent(ev)) {
                    return true
                }
            }
            MotionEvent.ACTION_UP -> {
                // 手指抬起时,允许父View拦截事件
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        return super.dispatchTouchEvent(ev)
    }

除了子元素需要处理之外,父容器也需要处理,父容器默认拦截除了ACTION_DOWN以外的其它事件,当子元素调用requestDisallowInterceptTouchEvent时,父容器才能继续拦截所需事件。

由于ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标记位的控制,所以父容器一旦拦截ACTION_DOWN事件,那么后续的事件无法传递到子元素,这样内部拦截就无法工作了。伪代码如下:

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        return ev.action != MotionEvent.ACTION_DOWN
    }

外部拦截法简单易用,但可能存在父容器无法响应事件的问题,内部拦截法不会存在此问题,但代码逻辑复杂,根据场景灵活使用。

View 工作原理

ViewRoot/ViewRootImpl

ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程(measure/layout/draw)均是通过ViewRoot来完成的,ViewRootImpl是从WindowManagerGlobal中创建的,ViewRootImpl是View中的最高层级,属于所有View的根。

在ActivityThread中,当Activity对象被创建完毕,onResume后,会通过WindowManager将DecorView添加到window上,这个过程中会创建ViewRootImpl。

源码路径:frameworks/base/core/java/android/app/ActivityThread.java

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
            boolean isForward, boolean shouldSendCompatFakeFocus, String reason) {
    ...
    if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
        	//应用类型窗口
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //decorview 添加到window中
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
                ...
}

WindowManager的实现类为WindowManagerImpl,调用addView方法。

源码路径:frameworks/base/core/java/android/view/WindowManagerImpl.java

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyTokens(params);
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }

源码路径:frameworks/base/core/java/android/view/WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
     ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
          ...
            //创建ViewRootImpl
            if (windowlessSession == null) {
                root = new ViewRootImpl(view.getContext(), display);
            } else {
                root = new ViewRootImpl(view.getContext(), display,
                        windowlessSession, new WindowlessWindowLayout());
            }

            view.setLayoutParams(wparams);
            //view,ViewRootImpl,LayoutParams顺序添加到WindowManager中    
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                //将Window对应的View设置给创建的ViewImpl
                //通过ViewRootImpl来更新界面并完成window的添加过程
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
                final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
                // BadTokenException or InvalidDisplayException, clean up.
                if (viewIndex >= 0) {
                    removeViewLocked(viewIndex, true);
                }
                throw e;
            }
        }
}

SetView中会进行布局请求,对界面进行布局,开始测量,布局,绘制,调用performTraversals方法。

源码路径:frameworks/base/core/java/android/view/ViewRootImpl.java

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
    ...
                //布局请求,对界面进行布局 
                requestLayout();
                InputChannel inputChannel = null;
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    inputChannel = new InputChannel();
                }
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    adjustLayoutParamsForCompatibility(mWindowAttributes);
                    controlInsetsForCompatibility(mWindowAttributes);

                    Rect attachedFrame = new Rect();
                    final float[] compatScale = { 1f };
                    //将该window添加到屏幕,通过AIDL通知WindowManagerService添加window
                    res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId,
                            mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
                            mTempControls, attachedFrame, compatScale);
                    if (!attachedFrame.isValid()) {
                        attachedFrame = null;
                    }
}
@Override
public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();    
    
 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }  
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            //测量、布局、绘制
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }   

ViewRootImpl中对View进行测量,布局,绘制,其过程主要在performTraversals中。

private void performTraversals() {
    ...
    if (!mStopped || mReportNextDraw) {
                if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()
                        || dispatchApplyInsets || updatedConfiguration) {
                    ...
                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(mTag,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        //View 测量
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }
                }
     ...
     if (didLayout) {
         //View布局
            performLayout(lp, mWidth, mHeight);     
    ...
        //View绘制
    if (!performDraw() && mActiveSurfaceSyncGroup != null) {
                mActiveSurfaceSyncGroup.markSyncReady();
            }
}      
  • View测量。最终调用到View的measure方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            //View 测量
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mMeasuredWidth = mView.getMeasuredWidth();
        mMeasuredHeight = mView.getMeasuredHeight();
        mViewMeasureDeferred = false;
    }
  • View布局。最终调用到View的layout方法。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
     try {
         final View host = mView;
         //View的layout方法进行布局
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
     for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
         //请求对该View重新布局,最终回调ViewRootImpl的requestLayout方法中重新测量,布局,绘制。
                        view.requestLayout();
                    }
         ...
          getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    Log.w("View", "requestLayout() improperly called by " + view +
                                            " during second layout pass: posting in next frame");
                                    view.requestLayout();
                                }
                            }
                        });
}
  • View绘制。最终调用到View的draw方法。
private boolean performDraw() {
    try {
            boolean canUseAsync = draw(fullRedrawNeeded, usingAsyncReport && mSyncBuffer);
            if (usingAsyncReport && !canUseAsync) {
                mAttachInfo.mThreadedRenderer.setFrameCallback(null);
                usingAsyncReport = false;
            }
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}  
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
    ...
    //绘制window
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    ...
    if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            //View 绘制
            mView.draw(canvas);

            drawAccessibilityFocusedDrawableIfNeeded(canvas);
    ...
}
MeasureSpec

MeasureSpec 代表一个32位int值,高2位代表SpecMode,低三位代表specSize,SpecMode指测量模式(三种UNSPECIFIED/EXACTLY/AT_MOST),SpecSize指在某种测量模式下的规格大小,它存在范围为0-2的30次方-1。MeasureSpec决定了一个View的尺寸规格。

public static class MeasureSpec {
   
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MeasureSpecMode {}
    //父容器没有对子元素施加任何约束。它可以是不受限制的任何大小,这种情况用于系统内部,表示一种测量状态
	public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //父容器已经为子元素确定了确切的大小。无论子元素想要多大,它都将被赋予这些边界。对应LayoutParams的match_parent
	public static final int EXACTLY     = 1 << MODE_SHIFT;
    //子元素可以大到指定的尺寸为止。对应LayoutParams的wrap_parent
	public static final int AT_MOST     = 2 << MODE_SHIFT;
    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
    }
    public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
}

系统内部是通过MeasureSpec来进行View的测量,正常情况下使用View指定MeasureSpec,但也可以给View设置LayoutParams,在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而决定View的宽高。

对于顶级View(DecorView)和普通View来说,MeasureSpec转换过程略有不同,对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中展示了DecorView的MeasureSpec的创建过程。desiredWindowWidth和desiredWindowHeight是屏幕的尺寸。根据LayoutParams的宽高参数来划分

  • ViewGroup.LayoutParams.MATCH_PARENT

精确模式,大小就是窗口的大小。

  • ViewGroup.LayoutParams.WRAP_CONTENT

最大模式,大小不定,不能超过窗口的大小。

  • 固定大小

精确模式,大小为LayoutParams中指定的大小。

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width,
                    lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
                    lp.privateFlags);
if (!forRootSizeOnly || !setMeasuredRootSizeFromSpec(
                    childWidthMeasureSpec, childHeightMeasureSpec)) {
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
} else {
     // We already know how big the window should be before measuring the views.
     // We can measure the views before laying out them. This is to avoid unnecessary
     // measure.
     mViewMeasureDeferred = true;
}
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
      windowSizeMayChange = true;
}
private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
        int measureSpec;
        final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0
                ? MATCH_PARENT : measurement;
        switch (rootDimension) {
            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
        }
        return measureSpec;
    }

对于普通View来说,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法,它会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法得到子元素的MeasureSpec。从代码来看,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin及padding有关。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //子View的measure方法,传递创建的MeasureSpec
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //padding 父容器已占用的空间大小,子元素可用大小=父容器尺寸-padding
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let them have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

View工作流程

View的主要流程主要是指measure、layout、draw三大流程,即测量、布局、绘制,其中measure确定View测量宽高,layout确定View最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。

measure 过程
View的measure过程

对于View的measure过程,它是由measure方法来完成,measure方法是一个final类型的方法,子类不能重写,在View的measure方法中会调用View的onMeasure方法,setMeasuredDimension方法会设置View宽高的测量值,从getDefaultSize方法可用看出,因为AT_MOST和EXACTLY模式下View的宽高由specSize决定,所以继承View的自定义View使用wrap_content和match_parent没有区别,AT_MOST 模式下specSize就是父容器当前剩余的空间大小,这种和布局中使用match_parent一致,怎么解决这个问题呢?可以参考Text View等原生控件,核心思想就是指定默认的宽高大小。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
   if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
       ...
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
protected int getSuggestedMinimumHeight() {  
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
protected int getSuggestedMinimumWidth() {
    //View没有设置背景,则对应设置的android:minWidth,不设置为0,否则为android:minWidth和背景Drawable的原始宽度
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
//android\graphics\drawable\Drawable.java
//返回Drawable的原始宽度
public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
ViewGroup的measure过程

对于ViewGroup的measure过程,除了完成自身的measure过程外,还要遍历调用所有子View的measure方法,各个子View再递归执行这个过程。和View不同的是,ViewGroup是一个抽象类,它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法。从代码来看,子View没有GONE时,它就会调用measureChild方法,首先取出子View的LayoutParams,再通过getChildMeasureSpec方法来创建子View的MeasureSpec,接着传递给View的measure方法进行测量,这个前面已经提到了。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
		//子View的measure方法,传递MeasureSpec
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

ViewGroup是一个抽象类,用于测量的onMeasure方法需要各个子ViewGroup去实现,例如Linearlayout、RelativeLayout等,不做统一的实现是因为布局特性差异大,无法统一实现。下面以LinearLayout的onMeasure方法进行分析ViewGroup的measure过程。

以垂直布局的LinearLayout为例,遍历子View并对子View执行measureChildBeforeLayout方法,最终会调用到子View的measure方法,然后通过mTotalLength来保存LinearLayout在竖直方向的初步高度,每测量一个子View,mTotalLength增加,增加部分主要包括子View的高度及子View在竖直方向的margin等。子元素测量完毕后,开始测量自己的大小,如果高度是match_parent,则高度为specSize,如果布局高度是wrap_content,那么测量高度为所有子View占用的总和。宽度是通过resolveSizeAndState方法计算的。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   if (mOrientation == VERTICAL) {
       measureVertical(widthMeasureSpec, heightMeasureSpec);
   } else {
       measureHorizontal(widthMeasureSpec, heightMeasureSpec);
   }
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
   ...
   for (int i = 0; i < count; ++i) {
       measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                       heightMeasureSpec, usedHeight);

               final int childHeight = child.getMeasuredHeight();
               if (useExcessSpace) {
                   // Restore the original height and record how much space
                   // we've allocated to excess-only children so that we can
                   // match the behavior of EXACTLY measurement.
                   lp.height = 0;
                   consumedExcessSpace += childHeight;
               }

               final int totalLength = mTotalLength;
               mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                      lp.bottomMargin + getNextLocationOffset(child));
       ...
       final int margin = lp.leftMargin + lp.rightMargin;
           final int measuredWidth = child.getMeasuredWidth() + margin;
       	
           maxWidth = Math.max(maxWidth, measuredWidth);
       ...
       mTotalLength += mPaddingTop + mPaddingBottom;

       int heightSize = mTotalLength;

       // Check against our minimum height
       heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

       // Reconcile our calculated size with the heightMeasureSpec
       int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
       heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
       ...
       maxWidth += mPaddingLeft + mPaddingRight;

       // Check against our minimum width
       maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
   	//LinearLayout 测量宽度,然后保存
       setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
               heightSizeAndState);

       if (matchWidth) {
           forceUniformWidth(count, heightMeasureSpec);
       }
   }
}
void measureChildBeforeLayout(View child, int childIndex,
           int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
           int totalHeight) {
   	//ViewGroup的measureChildWithMargins方法,内部调用子View的measure方法
       measureChildWithMargins(child, widthMeasureSpec, totalWidth,
               heightMeasureSpec, totalHeight);
   }

//View.java
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

View的measure过程比较复杂,measure完成后,可以通过measuredWidthmeasuredHeight方法正确获取到View的测量宽高。但需要注意,系统可能需要多次测量measure才能确定最终的测量宽高,因此,最好在onLayout方法中获取View的测量宽高。

准确获取某个View的宽高方法
  1. onWindowFocusChanged方法,当视图窗口获得焦点时,它通常是可见的,也意味着View的位置已经确定。
override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (hasFocus) {
            val width =view.measuredWidth
            val height=view.measuredHeight
        }
    }

2.view.post方法,Android 系统保证在处理消息队列中的消息之前,所有的视图都已经完成了测量和布局。这是因为视图的绘制是 UI 线程的一个关键部分,必须在处理其他任务(如响应用户输入、执行 Runnable 等)之前完成。post方法将一个runnable添加到消息队列中,然后等待执行run方法。

view.post {
                val width = view.measuredWidth
                val height = view.measuredHeight
            }

3.addOnGlobalLayoutListener回调,当View状态树状态发生改变或View树内部View可见性发生变化时,会回调onGlobalLayout,需要注意onGlobalLayout方法会回调多次,需及时移除回调。

view.viewTreeObserver.addOnGlobalLayoutListener(object:ViewTreeObserver.OnGlobalLayoutListener{
           override fun onGlobalLayout() {
               val width = view.measuredWidth
               val height = view.measuredHeight
               view.viewTreeObserver.removeOnGlobalLayoutListener(this)
           }
       })
layout过程

View的layout方法中,首先通过setFrame方法来设定View四个顶点的位置,顶点确定,View在ViewGroup中的位置也就确定了,接着调用onLayout方法,onLayout没有默认实现,以LinearLayout的onLayout方法为例进行分析。

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
    //四个顶点位置
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        final boolean wasLayoutValid = isLayoutValid();
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated
                // and thus the safest action is to clear focus here.
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }
        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
        notifyAppearedOrDisappearedForContentCaptureIfNeeded(true);
    }
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
 protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    //四个顶点,getWidth/getHeight计算使用
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
     ....
 }

以竖直方向为例,layoutVertical方法中会遍历所有子View并通过setChildFrame方法为子元素指定对应的位置,childTop会不断增加也表示子View会越来越靠下,setChildFrame方法调用子View的layout方法确定位置。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
        int childTop;
        int childLeft;
        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;
        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;
        final int count = getVirtualChildCount();
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;
           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

从源码可以发现,getWidth方法getMeasuredWidth方法区别在于它们使用的变量赋值阶段不同,getMeasuredWidth方法早于getWidth方法,但最终获取的宽高一般情况下是相等。

public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
  }
public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }
 public final int getWidth() {
        return mRight - mLeft;
    }
 public final int getHeight() {
        return mBottom - mTop;
    }
draw过程

draw过程核心有4步,代码有注释说明

public void draw(Canvas canvas) {
    /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background  
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         *      7. If necessary, draw the default focus highlight
         */
    // Step 1, draw the background, if needed
        int saveCount;
        drawBackground(canvas);
    ...
     // Step 3, draw the content
        onDraw(canvas);
    ...
    // Step 4, draw the children
        dispatchDraw(canvas);
    ....
    // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
}
  1. 绘制背景
private void drawBackground(Canvas canvas) {
     final Drawable background = mBackground;
    ...
    //绘制背景
    background.draw(canvas);
}
  1. 绘制本身,还是以LinearLayout为例,最终还是调用Drawable对象的canvas方法绘制本身。
@Override
protected void onDraw(Canvas canvas) {
     if (mOrientation == VERTICAL) {
            drawDividersVertical(canvas);
        } else {
            drawDividersHorizontal(canvas);
        }
}
 void drawDividersVertical(Canvas canvas) {
        final int count = getVirtualChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child != null && child.getVisibility() != GONE) {
                if (hasDividerBeforeChildAt(i)) {
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    final int top = child.getTop() - lp.topMargin - mDividerHeight;
                    drawHorizontalDivider(canvas, top);
                }
            }
        }

        if (hasDividerBeforeChildAt(count)) {
            final View child = getLastNonGoneChild();
            int bottom = 0;
            if (child == null) {
                bottom = getHeight() - getPaddingBottom() - mDividerHeight;
            } else {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                bottom = child.getBottom() + lp.bottomMargin;
            }
            drawHorizontalDivider(canvas, bottom);
        }
    }
private Drawable mDivider;
 void drawHorizontalDivider(Canvas canvas, int top) {
        mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
                getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
        mDivider.draw(canvas);
    }
  1. 绘制View,会调用ViewGroup的dispatchDraw方法,LinearLayout没有实现此方法。最终调用子View的draw方法,完成View绘制。
@Override
protected void dispatchDraw(Canvas canvas) {
    ...
     for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
  1. 绘制装饰(前景色,滑动条)
 public void onDrawForeground(Canvas canvas) {
     onDrawScrollIndicators(canvas);
     onDrawScrollBars(canvas);
     final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
     ...
      foreground.draw(canvas);
 }

View类中存在一个setWillNotDraw方法,给View设置标记位WILL_NOT_DRAW,当View设置了这个标志位,它表示这个View不需要绘制任何内容。这主要用于优化性能,告诉系统这个视图在onDraw方法中不会进行任何绘制操作,因此系统可以跳过对这个视图的绘制过程。ViewGroup默认有WILL_NOT_DRAW标志位,它自身不会进行绘制,但它的子视图仍然可以根据需要进行绘制。

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

RemoteViews

RemoteViews是一种特殊的View机制,表面理解起来是远程View,其实它是为了跨进程显示、更新界面的一种View结构,RemoteViews在Android使用场景有通知和桌面小部件。对于如何创建通知和桌面小部件应该在开发中经常遇到,这里不展开分析了。

原理

RemoteView并不能支持所有View类型,源码中有说明支持哪些类型:、

RemoteViews is limited to support for the following layouts:

  • AdapterViewFlipper
  • FrameLayout
  • GridLayout
  • GridView
  • LinearLayout
  • ListView
  • RelativeLayout
  • StackView
  • ViewFlipper

And the following widgets:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextClock
  • TextView

As of API 31, the following widgets and layouts may also be used:

  • CheckBox
  • RadioButton
  • RadioGroup
  • Switch

Descendants of these classes are not supported.

它也没有findViewById方法去获取具体View,而是通过一些对应View set方法进行设置,了解过自定义通知应该了解这些API如何使用。当然也提供了一些反射方法来设置View对应属性,实际上大部分set方法都是通过反射完成的。

public void setInt(@IdRes int viewId, String methodName, int value) {
        addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value));
    }
public void setIntDimen(@IdRes int viewId, @NonNull String methodName,
            @DimenRes int dimenResource) {
        addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT,
                ResourceReflectionAction.DIMEN_RESOURCE, dimenResource));
    }	
public void setColor(@IdRes int viewId, @NonNull String methodName,
            @ColorRes int colorResource) {
        addAction(new ResourceReflectionAction(viewId, methodName, BaseReflectionAction.INT,
                ResourceReflectionAction.COLOR_RESOURCE, colorResource));
    }

更多API使用参考官网:https://developer.android.google.cn/reference/android/widget/RemoteViews

那RemoteViews跨进程进行通信,是哪些进程通信呢,首先第一个进程当然是应用进程,其次对于是NotificationManagerService和AppWidgetService对应的SystemServer进程。RemoteViews会通过Binder传递到SystemServer进程,系统根据RemoteViews中的包名和布局信息加载View。系统会对View进行一系列更新操作,这些操作就是通过set方法进行来提交的。set方法对View的更新并不是立即更新,而是RemoteViews内部会记录所有更新操作,更新时机在RemoteViews被加载后才能执行,更新通过NotificationManager和AppWidgetManager来提交更新。

系统没有通过Binder去直接支持View的跨进程访问,因为View的方法太多,大量的进程通信(IPC)操作会影响操作,因此系统提供了一个Action概念,Action代表一个View操作,系统首先将View操作封装到Action并将这些对象跨进程传输到SystemServer,接着在SystemServer中执行Action对象的具体操作,当调用一次set方法,RemoteViews就会添加一个对应action,当我们通过NotificationManager和AppWidgetManager提交更新时,这些action会传输到远程进程并在远程进程中依次执行。

SystemServer进程通过RemoteViews的apply方法进行View的更新操作,RemoteViews的apply方法内部会去遍历所有的Action对象并调用它们的apply方法,具体的View更新操作是由Action对象的apply方法完成。上述实现有点不言而喻,不需要定义大量的Binder接口,远程进程批量执行更新操作避免大量的进程通信操作,提高了程序性能。

BaseReflectionAction的apply方法中可以明显看到他对View的操作会以反射的方式调用。

public void setInt(@IdRes int viewId, String methodName, int value) {
        addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INT, value));
    }
private void addAction(Action a) {
        if (hasMultipleLayouts()) {
            throw new RuntimeException("RemoteViews specifying separate layouts for orientation"
                    + " or size cannot be modified. Instead, fully configure each layouts"
                    + " individually before constructing the combined layout.");
        }
        if (mActions == null) {
            mActions = new ArrayList<>();
        }
        mActions.add(a);
    }
public View apply(@NonNull Context context, @NonNull ViewGroup parent,
            @Nullable InteractionHandler handler, @Nullable SizeF size) {
        RemoteViews rvToApply = getRemoteViewsToApply(context, size);

        View result = inflateView(context, rvToApply, parent);
        rvToApply.performApply(result, parent, handler, null);
        return result;
    }
private void performApply(View v, ViewGroup parent, InteractionHandler handler,
            ColorResources colorResources) {
        if (mActions != null) {
            handler = handler == null ? DEFAULT_INTERACTION_HANDLER : handler;
            final int count = mActions.size();
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                a.apply(v, parent, handler, colorResources);
            }
        }
    }
//Action 反射获取对应的方法设置属性
private abstract class BaseReflectionAction extends Action {
    @Override
        public final void apply(View root, ViewGroup rootParent, InteractionHandler handler,
                ColorResources colorResources) {
            final View view = root.findViewById(viewId);
            if (view == null) return;

            Class<?> param = getParameterType(this.type);
            if (param == null) {
                throw new ActionException("bad type: " + this.type);
            }
            Object value = getParameterValue(view);
            try {
                getMethod(view, this.methodName, param, false /* async */).invoke(view, value);
            } catch (Throwable ex) {
                throw new ActionException(ex);
            }
        }
}

总结

View是Android UI中的核心,本文总结了一些Android View相关核心知识,理解它的原理能更好的进行Android UI开发。本文对View的工作过程、事件分发机制、滑动冲突等常见View理论进行总结,后续由其它View相关知识继续补充。


http://www.kler.cn/a/373512.html

相关文章:

  • 【AI开源项目】OneAPI -核心概念、特性、优缺点以及如何在本地和服务器上进行部署!
  • 算法定制LiteAIServer视频智能分析软件的过亮、过暗及抖动检测应用场景
  • 论文检索与写作1
  • [C++ 核心编程]笔记 4.2.6 初始化列表
  • 10.30学习
  • 传奇996_3——使用补丁添加怪物
  • 已解决sqlalchemy.exc.OperationalError: (pymssql._pymssql.OperationalError) (18456
  • 代码随想录算法训练营第十二天| 226.翻转二叉树、101. 对称二叉树、104.二叉树的最大深度 、111.二叉树的最小深度
  • java的字符串比较
  • Google map根据半径创建虚线边框的圆
  • Vision - 视觉分割开源算法 SAM2(Segment Anything 2) 配置与推理 教程 (1)
  • ValueError: Object arrays cannot be loaded when allow_pickle=False
  • “换行”与“回车”
  • OpenCV 学习笔记
  • 同步和异步
  • AprilTag在相机标定中的应用简介
  • 20 Docker容器集群网络架构:三、Docker集群部署
  • window11使用wsl2安装Ubuntu22.04
  • Linux_04 Linux常用命令——tar
  • 深度学习(九):推荐系统的新引擎(9/10)
  • 【Java并发编程】信号量Semaphore详解
  • docker pull 拉取镜像失败,使用Docker离线包
  • 零基础学西班牙语,柯桥专业小语种培训泓畅学校
  • Si24R05:125K接收2.4G收发SoC芯片规格书
  • CSS行块标签的显示方式
  • 无人机之目标检测算法篇