Android View

安卓基础知识之View篇(一):View 必须要知道的基础知识 安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门。同时自己在工作中,也没法完全记住所有的基础细节,写这样的系列文章,可以让自己形成一个更完备的知识体系,同时给自己日后留个知识参考。 开始的开始 View 是除了安卓四大组件以外用的最多的控件,我们需要利用 Android 内置的 View 控件(TextView、LinearLayout等)完成业务需求,有时甚至需要自定义控件来满足特殊的业务需求。View 还负责与用户的交互(点击、滑动、长按等操作),掌. 阅读详情

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

首语

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=getRight—getLeft=getWdith。View的高height=getBottom—getTop=getHeight,源码计算也是如此,具体位置坐标可见下图。
View位置坐标

中间的Motion指的是用户手指接触屏幕的点。使用getRawX和getRawY方法获取的是Android坐标系的坐标,使用getX和getY方法 获取的是视图坐标,相对于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,当手指从右往左滑动时,速度可以为负数,具体公式为速度=(终点位置-起点位置)/时间段,接着通过getXVelocity和getXVelocity方法获取水平和竖直速度。最后计算完成重置并回收内存。

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提供了专门的方法来实现滑动,通过scrollTo和scrollBy方法,先看这两个方法的实现。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的坐标。offsetLeftAndRight和offsetTopAndBottom方法也可以设置左右和上下的偏移值。

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事件的话需要初始化,cancelAndClearTouchTargets和clearTouchTargets方法取消和清空所有的 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方法的,onClick 和 onLongClick 在 onTouchEvent 中被调用,且 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(); 
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }   
    
 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完成后,可以通过measuredWidth和measuredHeight方法正确获取到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相关知识继续补充。

Android View(一)——View的基础知识 目录一.View的基础知识1.什么是View2.View的位置参数3.MotionEvent4. TouchSlop5. VelocityTracker6. GestureDetector 一.View的基础知识 1.什么是View View是一种界面层的控件的一种抽象,它代表了一个控件,是Android中所有控件的基类。 2.View的位置参数 3.MotionEvent 在我们触摸屏幕的过程中,可以分为三种情况,分别是按下、滑动、弹起。Android中为我们封装好了一个MotionEvent类,使 阅读详情

相关推荐

Android 的 View (三)

View 简单理解就是,UI 代表的就是这个应用的界面,View 就是这个界面上一个个的小组件。

即使再小的帆也能远航,加油~ 940

深入理解Android之自定义View

自定义View、多线程、网络,被认为是Android开发者必须牢固掌握的最基础的三大基本功。而自定义View又是Android开发中特别重要的一环,很多地方都需要用到自定义View。这篇文章我就梳理一下自定义View的相关知识。 目录 (一)什么是自定义View (二)自定义组合View (三)自定义View的绘制 1. Canvas.drawXXX() Pa...

Fighting_初心的博客 1万+

安卓设备通过USB串口与STM32单片机通讯之一

这里写自定义目录标题欢迎使用Markdown编辑器`在这里插入代码片`新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 欢迎使用Markdown编辑器在这里插入代码片 你好下Markdown的基本语法知识。 新的改变 我们对

JGB联合商务组的博客 7244

android开发艺术(三)之View

View是android中所有控件的基类,Button、TextView、LinearLayout共同基类都是View;ViewGroup 继承自View,View本身可以是单个控件,也可以是多个控件组成的一组控件。

梵高话葵 1398

【脑电分析系列】第7篇:频域分析基石 — 功率谱密度(PSD)的计算与常见方法对比

本文探讨了脑电图(EEG)频域分析的核心方法——功率谱密度(PSD)。通过对比基于快速傅里叶变换的周期图法和更稳健的Welch's方法,指出后者通过分段、加窗和平均处理,能有效降低估计方差,更适合分析非稳态的脑电信号。文章详细解析了关键参数设置对频率分辨率和统计稳定性的影响,并提供了Python计算实现。PSD分析将复杂的脑电信号转换为直观的频域特征,为研究认知功能和神经疾病提供重要依据。

qq_32516809的博客 1801

卷积神经网络-猫狗识别(附源码)

利用python实现一个卷积神经网络,训练猫狗数据集,实现对猫狗图片的预测识别

m0_63223213的博客 2万+

android统计view数量,安卓view曝光统计实现方案

安卓端有效曝光统计步骤1 ,要确定什么样的算有效曝光(在屏幕停留时间超过一个值如2秒)2,监听到每个view移入和移出屏幕的事件3,把数据绑定到view(view相当于数据的载体)4,根据监听事件拿到view里的数据5,根据曝光字典筛选出运营想要的有效数据1,什么算有效曝光需要和运营同学一起确认下,一个view在屏幕停留的时长大于多少秒时算有效曝光。目前安卓这边是可以拿到view从进屏幕到移出屏幕...

weixin_29288653的博客 623

笔记本电脑更换主板后出现2203:System configuration is invalid,以及2201、2202系统错误的解决

主板更换后刷新S/N

晓夏IT笔记 3516

安卓View理解总结

安卓View理解总结 参考 link.网站写的一篇博客; 一.视图坐标系. View是Android所有控件的基类,同时ViewGroup也是继承自View,下面这张图可以直观了解。 1.Android坐标系. Android中有两种坐标系,分别为Android坐标系和视图坐标系,首先我们先来看看Android坐标系。 在Android中,将屏幕的左上角的顶点作为Android坐标系的原点,这个...

gzy19805026167的博客 2562

5G初始注册流程详解(2)-Registration Request

1:了解注册的本质,5G终端为什么要注册? 2:SUCI,SUPI,5G-GUTI的区别? 3:为什么会有COMPLETE REGISTRATION REQUEST呢?

weixin_53220301的博客 3794

ArcGIS基础:城市交通网络构建及设施服务分析(一):道路数据处理与拓扑操作

首先需要加载数据, 1:选择这个数据库:,右键导入,选择导入单个要素,弹出“要素类至要素类” 2:输出位置(导入的数据是CAD格式数据)。 3:输出要素类为道路。 4:字段只保留道路类型字段。 这个操作是分类合并各级道路数据。 1:通过属性选择各类道路数据。 2:比如此类都是“干道”。 3:编辑器下来,然后合并即可。 得到如图所示,需要用到三个字段。 上面这个操作是打断道路相交线。 1:选中所有数据。 2:找到高级编辑中的打断相交线。 3:确定即可。 下面进行拓扑检查,虽然我们已经进行合并和打断相交.

yuanYW7556的博客 2万+

android view 曝光,安卓View点击/曝光的监听方式

Android点击、曝光事件的监听分散在代码的各个角落, 不利于开发维护。其实可以将这2种行为收敛到一起, 运行时监听Activity的生命周期,即ActivityLifeCallBack回调; 在onResume或onStart函数里判断当前acitivity是否添加了自定义ViewGroup(暂且命名为TrackerViewGroup), 如果未添加则添加;在onDestory函数中移除该自定...

weixin_39761647的博客 927

HQChart实战教程21 - uniapp app端K线图定制tooltip

HQChart实战教程21 - uniapp app端K线图定制tooltip序效果步骤2. 注册标题绘制事件3. 标题绘制回调函数格式说明eventdataobjtooltip显示函数完整代码HQChart代码地址 序 hqchartK线图内置一个tooltip, 如果需要定制一个tooltip, 我们可以通过注册绘制标题事件,来自己做一个div显示数据。 参考教程HQChart实战教程7-自定义显示手势点击K线显示信息 效果 步骤 在外部创建一个view,并在view里创建需要显示的信息字段. 例子里

小银 2556

Android :安卓学习笔记之 Android View绘制 的简单理解和使用

Android View的简单理解和使用Android View参考 Android View 参考 1、Android View详解 2、教你搞定Android自定义View 3、每日一问(十九)请描述一下View的绘制流程

JMW1407的博客 2632

单反相机物镜设计[DG-10]

REAR(要打在像面,操作数要找像面对应位置15):以透镜单位表示的真实光线径向坐标,波长由Wave定义,参见“Hx、Hy、Px和Py”。最小玻璃厚度最小不能小于semi-dia的1/10,要根据layout看有没有过厚过薄,反复调节。PROD:两个操作数的乘积(操作#1 X操作#2)。3x4就是57.3x0.41=23.5。拦光一般在第一面和最后一面操作,减少边缘无用光线。光阑面与镜面贴近,要调距离,同时设置光阑像差。后截距是指从镜头后表面到焦点的距离。CONS:弧度转为角度。镜头相互嵌入,有错误。

weixin_72050316的博客 600

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

android view translationx,【安卓笔记】view.getX和view.getTranslationX区别

【安卓笔记】view.getX和view.getTranslationX区别http://www.68idc.cn/help/buildlang/ask/20150104156778.html​Scrollto 的真正意思​This is called in response to an internal scroll in this view(i.e., the view scrolled it...

weixin_29131709的博客 222

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

推荐一款引人注目的安卓日历库——Collapsible Calendar View Android

推荐一款引人注目的安卓日历库——Collapsible Calendar View Android 在寻找一个灵活、可定制且易于集成的日历视图来提升你的Android应用体验吗?Collapsible Calendar View Android可能正是你需要的解决方案。这个开源项目由Shrikanth Ravi开发,并以MIT许可证开放,旨在提供一个可折叠式日历视图,既美观又实用。 项目介绍 C...

gitblog_00055的博客 595

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

android view 屏幕外,安卓如何让View往屏幕外隐藏?

以下两种方案都亲测可行:方案一:(无动画)listView.setSelection(问题14的position);方案二:(带动画)ListView lvBaseList添加问题14后调用final int targetPosition = 2;//假设你要置顶的ItemView的position = 2final long targetId = adapter.getItemId(target...

weixin_35914724的博客 801

安卓动态添加View

在安卓应用中,有很多时候需要动态添加View。比如从后台获取商品列表,根据商品数量在页面渲染对应数量的条目,这时候就需要动态添加View。

java_t_t的博客 2033

安卓Android---自定义view

android的自定义view

Chenbaozeng的博客 2161

How Android Draws Views 安卓如何绘制view

安卓如何绘制view(How Android Draws Views) 当一个activity获得焦点,activity就会绘制他的布局。安卓框架会处理绘制的步骤,但是activity必须提供它布局的根节点。 绘制从布局根节点开始。它被要求按照布局的树形结构进行测量和绘制。 Drawing is handled by walking the tree and rendering each V

xlxiaoluo的专栏 582

android的 view 的绘制

view的绘制

qq_42538455的博客 547

安卓自定义View

安卓自定义View

eos-i的博客 5250

安卓View — View (一)

目录Android —— View (一)一、 View的基础知识1.什么是View2.View的位置参数3.MotionEvent4.TouchSlop5.VelocityTracker6.GestureDetector二、View的滑动1.scrollTo/scrollBy2.使用动画View动画属性动画3.改变布局参数4.滑动方式对比5.跟随手指滑动的View编写三.弹性滑动1. Scrol...

睡客的博客 1839

[安卓] 深度掌握Android传统View系统:View、布局与关键控件全解析

自定义View需要注意哪些问题?正确处理padding和margin支持wrap_content和match_parent在onDraw()中避免创建对象处理触摸事件冲突考虑性能优化如何优化自定义View的性能?使用Canvas.clipRect()减少绘制区域启用硬件加速避免在onDraw()中分配内存使用invalidate(Rect)只重绘脏区域。

2301_77946674的博客 1826

Android App View——团结引擎车机版实现安卓应用原生嵌入 3D 开发场景

团结引擎 1.5.0 版本已于 4 月 14 日正式发布,从 1.5.0 版本开始,团结引擎车机版带来了一个激动人心的新能力 —— Android App View。现在,开发者可以将任意第三方安卓应用以 2D 组件或 3D 组件的形式,原生嵌入到 Tuanjie 开发的 3D 场景中,打造更具沉浸感与交互性的体验。

unityofficial的博客 958
上一篇: SystemUI 解析
下一篇: ActivityManagerService
八归少年
八归少年 新星创作者: 操作系统技术领域 新星创作者: 操作系统技术领域
博客等级 码龄9年 9011粉丝 63原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

八归少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值