Android View体系

目录

View与ViewGroup

坐标系

Android坐标系 

View坐标系

View的滑动 

layout()方法 

offsetLeftAndRight()与offsetTopAndBottom()

LayoutParams 改变布局参数 

动画 

scollTo与scollBy 

Scroller 

View的事件分发机制 

源码解析Activity的构成

图解Activity的构成

View的事件分发机制

点击事件

事件分发的重要方法 

事件分发机制流程分析

Activity的事件分发机制

ViewGroup的事件分发机制

实例分析 (Button类)

View的事件分发机制

View的工作流程

View的工作流程入口

DecorView被加载到Window中

理解MeasureSpec

View的measure流程

单一View的measure过程

ViewGroup的measure过程

View的layouv

View的draw流程 

单一View的draw过程

ViewGroup的draw过程


View与ViewGroup

 View的部分继承关系

坐标系

Android坐标系 

在Android中,将屏幕左上角的顶点作为Android坐标系的原点,

这个原点向右是X轴正方向,向下是Y 轴正方向,

如下图所示。另外在触控事件中,使用getRawX()和getRawY()方法获得的坐标也是 Android坐标系的坐标。

Android坐标系

View坐标系

View坐标系 

View获取自身的宽和高

系统已经向我们提供了获取View宽和高的方法
getHeight()用来获 取 View 自身的高度
getWidth()用来获取 View 自身的宽度

View自身的坐标 
通过如下方法可以获得View到其父控件(ViewGroup)的距离 
getTop():获取View自身顶边到其父布局顶边的距离 
getLeft():获取View自身左边到其父布局左边的距离 
getRight():获取View自身右边到其父布局左边的距离 
getBottom():获取View自身底边到其父布局顶边的距离

MotionEvent提供的方法 
我们知道无论是View还是ViewGroup,最终的点击事 件都会由onTouchEvent(MotionEvent event)方法来处理
MotionEvent在用户交互中作用重大,其内部提供了很多事件常量,比如我们常用的ACTION_DOWN、ACTION_UP和ACTION_MOVE
此外, MotionEvent也提供了获取焦点坐标的各种方法 
getX():获取点击事件距离控件左边的距离,即视图坐标
getY():获取点击事件距离控件顶边的距离,即视图坐标 
getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标
getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标
 

View的滑动 

View的滑动是Android实现自定义控件的基础,同时在开发中我们也难免会遇到View的滑动处理。
其实不管是哪种滑动方式,其基本思想都是类似的:
当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。
实现View滑动有很多种方法,

在这里主要讲解6种滑动方法,分别是

layout()
offsetLeftAndRight()与 offsetTopAndBottom()
LayoutParams
动画
scollTo 与 scollBy
以及Scroller

layout()方法 

view进行绘制的时候会调用onLayout()方法来设置显示的位置,因此我们同样也可以通过修改View的left、top、right、bottom这四种属性来控制View的坐标。首先我们要自定义一个View,在onTouchEvent()方法中获取触摸点的坐标,

    public boolean onTouchEvent(MotionEvent event) {
        //获取到手指处的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //调用layout方法来重新放置它的位置
                layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX, getBottom() + offsetY);
                break;
        }
    }

offsetLeftAndRight()与offsetTopAndBottom()

将ACTION_MOVE中的代码替换成如下代码,

case MotionEvent.ACTION_MOVE:
    //计算移动的距离
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    //对left和right进行偏移
    offsetLeftAndRight(offsetX);
    //对top和bottom进行偏移
    offsetTopAndBottom(offsetY);
    break;

LayoutParams 改变布局参数 

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果

同样的我们将ACTION_MOVE中的代码替换成如下代码,

    LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
    layoutParams.leftMargin = getLeft() + offsetX;
    layoutParams.topMargin = getTop() + offsetY;
    setLayoutParams(layoutParams);

动画 

在res目录新建anim文件夹并创建translate.xml,

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="300" android:duration="1000"/>
</set>

在Java代码中使用,

mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));

使用属性动画移动 ,

ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();

scollTo与scollBy 

scollTo(x,y)表示移动到一个具体的坐标点
scollBy(dx,dy)则表示移动的增量为dx、dy, scollBy最终也是要调用scollTo的
scollTo、scollBy移动的是View的内容
如果在ViewGroup中使用则是移动他所有的子View

我们将ACTION_MOVE中的代码替换成如下代码

((View)getParent()).scrollBy(-offsetX,-offsetY);

Scroller 

我们用scollTo/scollBy方法来进行滑动,瞬间完成的, 用户体验不大好
这里我们可以使用Scroller来实现有过度效果的滑动, 是在一定的时间间隔完成的
Scroller本身是不能实现View的滑动的, 它需要配合View的computeScroll( )方法才能弹性滑动的效果

要初始化Scroller,

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

重写View的computeScroll()方法,系统会在绘制View的时候在draw()方法中调用该方法,
这个方法中我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,
每滑动一小段距离我们就调用invalidate()方法不断的进行重绘,重绘就会调用computeScroll()方法, 这样我们就通过不断的移动一个小的距离并连贯起来就实现了平滑移动的效果 

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //通过不断的重绘不断的调用computeScroll方法
            invalidate();
        }
    }

在CustomView中写一个smoothScrollTo()方法, 调用Scroller.startScroll()方法, 在2000毫秒内沿X轴平移delta像素

    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        //1000秒内滑向destX
        mScroller.startScroll(scrollX, 0, delta, 0, 2000);
        invalidate();
    }

使用CustomView的smoothScrollTo(),

//使用Scroll来进行平滑移动
mCustomView.smoothScrollTo(-400,0);

View的事件分发机制 

源码解析Activity的构成

点击事件用MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity,所以我们首先要了解一下Activity的构成。当我们写Activity时会调用setContentView()方法来加载布局。分析setContentView()实现

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
}

调用了getWindow().setContentView(layoutResID), getWindow()返回mWindow

public Window getWindow() {
    return mWindow;
}

在Activity的attach()方法发现mWindow,

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
			
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this);
		...
}

mWindow指的就是PhoneWindow, PhoneWindow是继承抽象类Window的, getWindow()得到的是一个PhoneWindow, 查看PhoneWindow.java的setContentView()方法(PhoneWindow.java)


@Override
   public void setContentView(View view, ViewGroup.LayoutParams params) {
       // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
       // decor, when theme attributes and the like are crystalized. Do not check the feature
       // before this happens.
       if (mContentParent == null) {
           installDecor(); // tag1
       } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           mContentParent.removeAllViews();
       }

       if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
           view.setLayoutParams(params);
           final Scene newScene = new Scene(mContentParent, view);
           transitionTo(newScene);
       } else {
           mContentParent.addView(view, params);
       }
       final Callback cb = getCallback();
       if (cb != null && !isDestroyed()) {
           cb.onContentChanged();
       }
   }

在tag1 处的installDecor()方法

if (mDecor == null) {
           mDecor = generateDecor(); // tag2
           mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
           mDecor.setIsRootNamespace(true);
           if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
               mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
           }
       }
       if (mContentParent == null) {
           mContentParent = generateLayout(mDecor); // tag3  
       }
       ...
 } 
 ...      
}

查看 tag2 处的generateDecor()方法,

protected DecorView generateDecor() {
     return new DecorView(getContext(), -1);
}

创建了一个DecorView,这个DecorView就是Activity中的根View。接着查看DecorView的源码,发现DecorView是PhoneWindow类的内部类,并且继承FrameLayout

查看tag3 处的 generateLayout()方法

 protected ViewGroup generateLayout(DecorView decor) {
...
        //根据不同的情况加载不同的布局给layoutResource
        int layoutResource; //tag5
        int features = getLocalFeatures();
        // System.out.println("Features: 0x" + Integer.toHexString(features));
        if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = com.android.internal.R.layout.screen_title_icons;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = com.android.internal.R.layout.screen_progress;
            // System.out.println("Progress!");
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        com.android.internal.R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = com.android.internal.R.layout.screen_custom_title;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
...

 mDecor.startChanging();
        //将layoutResource加载到View中并添加到DecorView中
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
...

}

加载layoutResource的布局,来看看其中的一种布局R.layout.screen_title,这个文件在frameworks\base\core\res\res\layout目录中(screen_title.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

ViewStub是用来显示ActionBar的,下面的两个FrameLayout,一个是title用来显示标题,一个是content用来显示内容

图解Activity的构成

一个Activity包含一个window对象,这个对象是由PhoneWindow来实现的,
PhoneWindow将DecorView做为整个应用窗口的根View,
而这个DecorView又将屏幕划分为两个区域一个是TitleView一个是ContentView,
而我们平常做应用所写的布局正是展示在ContentView中的

View的事件分发机制

点击事件

当我们点击屏幕时, 就产生了点击事件, 这个事件被封装成了一个类 MotionEvent

一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件

 事件分发的本质, 将点击事件(MotionEvent),按照 Activity -> ViewGroup -> View 的顺序传递

事件分发的重要方法 

dispatchTouchEvent(MotionEvent ev) 
onInterceptTouchEvent(MotionEvent ev)
onTouchEvent(MotionEvent ev)

事件分发机制流程分析

Activity的事件分发机制

   // 源码分析:Activity.dispatchTouchEvent()
   public boolean dispatchTouchEvent(MotionEvent ev) {

    // ->>tag1
    if (getWindow().superDispatchTouchEvent(ev)) {

        return true;
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
        // 否则:继续往下调用Activity.onTouchEvent

    }
    // ->>tag3
    return onTouchEvent(ev);
  }
/**
  * tag1:getWindow().superDispatchTouchEvent(ev)
  * 说明:
  *     a. getWindow() = 获取Window类的对象
  *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类
  *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
  */
  @Override
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return mDecor.superDispatchTouchEvent(event);
      // mDecor = 顶层View(DecorView)的实例对象
      // ->> tag2
  }

/**
  * tag2:mDecor.superDispatchTouchEvent(event)
  * 定义:属于顶层View(DecorView)
  * 说明:
  *     a. DecorView类是PhoneWindow类的一个内部类
  *     b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return super.dispatchTouchEvent(event);
      // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
      // 即将事件传递到ViewGroup去处理,详细请看后续章节tag的ViewGroup的事件分发机制

  }
  // 回到最初的tag2入口处
/**
  * tag3:Activity.onTouchEvent()
  * 调用场景:当一个点击事件未被Activity下任何一个View接收/处理时,就会调用该方法
  */
  public boolean onTouchEvent(MotionEvent event) {

        // ->> tag5
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        
        return false;
        // 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,tag完毕
    }

/**
  * tag4:mWindow.shouldCloseOnTouch(this, event)
  * 作用:主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
  */
  public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

  if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
          && isOutOfBounds(context, event) && peekDecorView() != null) {

        // 返回true:说明事件在边界外,即 消费事件
        return true;
    }

    // 返回false:在边界内,即未消费(默认)
    return false;
  } 

总结

ViewGroup的事件分发机制

从Activity的事件分发机制可知
在Activity.dispatchTouchEvent()实现了将事件从Activity->ViewGroup的传递
ViewGroup的事件分发机制从dispatchTouchEvent()开始

/**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) { 

  ... 

  if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
  // tag1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
    // 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
    // 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
        // a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
        // b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
        // c. 关于onInterceptTouchEvent() ->>tag1

  // tag2
    // 1. 通过for循环,遍历当前ViewGroup下的所有子View
    for (int i = count - 1; i >= 0; i--) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                || child.getAnimation() != null) {  
            child.getHitRect(frame);  

            // 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
            if (frame.contains(scrolledXInt, scrolledYInt)) {  
                final float xc = scrolledXFloat - child.mLeft;  
                final float yc = scrolledYFloat - child.mTop;  
                ev.setLocation(xc, yc);  
                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                // 3. 条件判断的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
                if (child.dispatchTouchEvent(ev))  { 

                // 调用子View的dispatchTouchEvent后是有返回值的
                // 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即该子View把ViewGroup的点击事件消费掉了

                mMotionTarget = child;  
                return true; 
                      }  
                  }  
              }  
          }  
      }  
    }  

  ...

  return super.dispatchTouchEvent(ev);
  // 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
  // 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
  // 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
  // 具体请参考View事件分发机制中的View.dispatchTouchEvent()

  ... 

}

/**
  * tag1:ViewGroup.onInterceptTouchEvent()
  * 作用:是否拦截事件
  * 说明:
  *     a. 返回false:不拦截(默认)
  *     b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
    // 默认不拦截
    return false;

  } 
  // 回到调用原处

总结 

实例分析 (Button类)

布局说明

测试代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮2" />

</LinearLayout>
public class MainActivity extends AppCompatActivity {

  Button button1,button2;
  ViewGroup myLayout;

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      button1 = (Button)findViewById(R.id.button1);
      button2 = (Button)findViewById(R.id.button2);
      myLayout = (LinearLayout)findViewById(R.id.my_layout);

      // 1.为ViewGroup布局设置监听事件
      myLayout.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              Log.d("TAG", "点击了ViewGroup");
          }
      });

      // 2. 为按钮1设置监听事件
      button1.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              Log.d("TAG", "点击了button1");
          }
      });

      // 3. 为按钮2设置监听事件
      button2.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              Log.d("TAG", "点击了button2");
          }
      });
   }
}

// 点击按钮1,输出如下
点击了button1

// 点击按钮2,输出如下
点击了button2

// 点击空白处,输出如下
点击了ViewGroup
  • 点击Button时,因为ViewGroup默认不拦截,所以事件会传递到子View Button,于是执行Button.onClick()。
  • 此时ViewGroup. dispatchTouchEvent()会直接返回true,所以ViewGroup自身不会处理该事件,于是ViewGroupLayout的dispatchTouchEvent()不会执行,所以注册的onTouch()不会执行,即onTouchEvent() -> performClick() -> onClick()整个链路都不会执行,所以最后不会执行ViewGroup设置的onClick()里。
  • 点击空白区域时,ViewGroup. dispatchTouchEvent()里遍历所有子View希望找到被点击子View时找不到,所以ViewGroup自身会处理该事件,于是执行onTouchEvent() -> performClick() -> onClick(),最终执行ViewGroupLayout的设置的onClick()

View的事件分发机制

从ViewGroup事件分发机制知道,View事件分发机制从dispatchTouchEvent()开始

/**
  * 源码分析:View.dispatchTouchEvent()
  */
  public boolean dispatchTouchEvent(MotionEvent event) {  

       
        if ( (mViewFlags & ENABLED_MASK) == ENABLED && 
              mOnTouchListener != null &&  
              mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 

        return onTouchEvent(event);  
  }
  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
  //   1. (mViewFlags & ENABLED_MASK) == ENABLED
  //   2. mOnTouchListener != null
  //   3. mOnTouchListener.onTouch(this, event)
  // 下面对这3个条件逐个分析

/**
  * 条件1:(mViewFlags & ENABLED_MASK) == ENABLED
  * 说明:
  *    1. 该条件是判断当前点击的控件是否enable
  *    2. 由于很多View默认enable,故该条件恒定为true(除非手动设置为false)
  */

/**
  * 条件2:mOnTouchListener != null
  * 说明:
  *   1. mOnTouchListener变量在View.setOnTouchListener()里赋值
  *   2. 即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空)
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  

} 

/**
  * 条件3:mOnTouchListener.onTouch(this, event)
  * 说明:
  *   1. 即回调控件注册Touch事件时的onTouch();
  *   2. 需手动复写设置,具体如下(以按钮Button为例)
  */
  button.setOnTouchListener(new OnTouchListener() {  
      @Override  
      public boolean onTouch(View v, MotionEvent event) {  
   
        return false;  
        // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
        // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
        // onTouchEvent()源码分析 -> 分析1
      }  
  });
/**
  * tag1:onTouchEvent()
  */
  public boolean onTouchEvent(MotionEvent event) {  

    ... // 仅展示关键代码

    // 若该控件可点击,则进入switch判断中
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

        // 根据当前事件类型进行判断处理
        switch (event.getAction()) { 

            // a. 事件类型=抬起View(主要tag)
            case MotionEvent.ACTION_UP:  
                    performClick(); 
                    // ->>tag2
                    break;  

            // b. 事件类型=按下View
            case MotionEvent.ACTION_DOWN:  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  

            // c. 事件类型=结束事件
            case MotionEvent.ACTION_CANCEL:  
                refreshDrawableState();  
                removeTapCallback();  
                break;

            // d. 事件类型=滑动View
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  

                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        removeLongPressCallback();  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  

        // 若该控件可点击,就一定返回true
        return true;  
    }  
  // 若该控件不可点击,就一定返回false
  return false;  
}

/**
  * tag2:performClick()
  */  
  public boolean performClick() {  

      if (mOnClickListener != null) {
          // 只要通过setOnClickListener()为控件View注册1个点击事件
          // 那么就会给mOnClickListener变量赋值(即不为空)
          // 则会往下回调onClick() & performClick()返回true
          playSoundEffect(SoundEffectConstants.CLICK);  
          mOnClickListener.onClick(this);  
          return true;  
      }  
      return false;  
  }  

总结 

View的工作流程

View的工作流程,指的就是measure、layout和draw
其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View

View的工作流程入口

DecorView被加载到Window中

当DecorView创建完毕, 要加载到Window中时, 先了解一下Activity的创建过程
当我们调用Activity的startActivity方法时, 最终是调用ActivityThread的handleLaunchActivity方法来创建Activity的代码如下所示

public final class ActivityThread {
    ...
 
    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        ...
 
        Activity a = performLaunchActivity(r, customIntent); // tag1
 
        if (a != null) {
            ...
            handleResumeActivity(r.token, false, r.isForward, // tag2
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
 
        }
    }
}

上面代码tag1 处调用 performLaunchActivity 方法来创建 Activity,在这里面会调用到Activity的onCreate方法,从而完成DecorView的创建,
tag2处调用 handleResumeActivity方法,代码如下所示,

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ActivityClientRecord r = mActivities.get(token); //tag1
      if (r != null) {
        final Activity a = r.activity;
           if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView(); //tag2
            decor.setVisibility(View.INVISIBLE); 
            ViewManager wm = a.getWindowManager(); //tag3
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient && !a.mWindowAdded) {
          a.mWindowAdded = true;
          wm.addView(decor, l); //tag4
}

tag2:得到了DecorView
tag3:得到了WindowManager,WindowManger是一个接口并且继承了ViewManager,它的实现类是WindowManagerImpl
tag4:调用了WindowManager的addView方法

查看WindowManagerImpl.addView方法

public final class WindowManagerImpl implements WindowManager {
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

WindowManagerGlobal的addView方法

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
....
ViewRootImpl root;
View panelParentView = null;
root = new ViewRootImpl(view.getContext(), display); //tag1
view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
root.setView(view, wparams, panelParentView);  //tag2

tag1:创建一个ViewRootImpl实例
tag2:调用了ViewRootImpl的setView方法并将DecorView作为参数传进去,这样就把DecoreView加载到Window中,但是界面仍然不会显示出来,因为view的工作流程还每执行完成 ,需要经过measure ,layout ,draw彩绘把view绘制出

是通过ViewRootImpl的performTraveals函数,这个方法使ViewTree开始view的工作流程

private void performTraversals(){
	...
    if(!mStopped){
		int childWidthMeasureSpec = getRootMeasureSpec(mWidth,lp.width);
		int childHeightMeasureSpec = getRootMeasureSpec(mHeight,lp.height);
		performMeasure(lp,childWidthMeasureSpec ,childHeightMeasureSpec );
    }
}
 
   if(didLayout) {
	 performLayout(lp,desiredWindowWidth,desiredWindowHeight);
	 ..
   }
   if(!cancelDraw && !newSureface) {
		if(!skipDraw || mReportNextDraw) {
			if(mPendingTransition != null && mPendingTransitions.size > 0) {
				for(int i=0; i< mPendingTransitions.size;++i) {
					mPendingTransitions.get(i).startChangingAimations();
				}
				mPendingTransitions.clear();
			}
			performDraw();
		}
   }
   ...
}


这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法的内部又会分别调用View的measure、layout和draw方法。同时这里还会调用view的measure方法(进行测量)

performMeasure需要传入两个参数,需要了解MeasureSpec


理解MeasureSpec

MeasureSpec是View的内部类, 其封装了一个View的规格尺寸, 包括View的宽和高的信息, 
它的作用是在Measure流程中, 系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec, 然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高

public class MeasureSpec {

  // 进位大小 = 2的30次方
  // int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
  private static final int MODE_SHIFT = 30;  
    
  // 运算遮罩:0x3为16进制,10进制为3,二进制为11
  // 3向左进位30 = 11 00000000000(11后跟30个0)  
  // 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
  private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

  // UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
  // 通过高2位
  public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  
  // EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
  public static final int EXACTLY = 1 << MODE_SHIFT;  

  // AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
  public static final int AT_MOST = 2 << MODE_SHIFT;  

  /**
    * makeMeasureSpec()方法
    * 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
    **/ 
      public static int makeMeasureSpec(int size, int mode) {  
          return size + mode;  
      // measureSpec = size + mode;此为二进制的加法 而不是十进制
      // 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
      // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  

      }  

  /**
    * getMode()方法
    * 作用:通过measureSpec获得测量模式(mode)
    **/    
      public static int getMode(int measureSpec) {  
       
          return (measureSpec & MODE_MASK);  
          // 即:测量模式(mode) = measureSpec & MODE_MASK;  
          // MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
          //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
          // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值

      }  
  /**
    * getSize方法
    * 作用:通过measureSpec获得测量大小size
    **/       
      public static int getSize(int measureSpec) {  
       
          return (measureSpec & ~MODE_MASK);  
          // size = measureSpec & ~MODE_MASK;  
         // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size  
      } 
} 

从MeasureSpec的常量可以看出, 它代表了32位的int值, 其中高2位代表了SpecMode, 低30位则代表 SpecSize,SpecMode指的是测量模式, SpecSize指的是测量大小

SpecMode有3种模式,如下所示
• UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量
• AT_MOST:最大模式,对应于wrap_comtent属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值
• EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值

对于每一个View,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽和高的信息。通过getMode或getSize得到模式和宽、高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影响的

作为顶层View的DecorView来说, 其并没有父容器, 它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定


View的measure流程

单一View的measure过程
/**
  * 源码分析:measure()
  * 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
  * 作用:基本测量逻辑的判断
  */ 
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

      // 参数说明:View的宽 / 高测量规格
      ...

      int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
              mMeasureCache.indexOfKey(key);

      if (cacheIndex < 0 || sIgnoreMeasureCache) {
          
          onMeasure(widthMeasureSpec, heightMeasureSpec);
          // 计算视图大小 ->>分析1
      } else {
          ...
  }
/**
  * 分析1:onMeasure()
  * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
  *      b. 存储测量后的View宽 / 高:setMeasuredDimension()
  */ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 参数说明:View的宽 / 高测量规格
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :获得View宽/高的测量值 ->>分析2
    // 传入的参数通过getDefaultSize()获得 ->>分析3
}

/**
  * 分析2:setMeasuredDimension()
  * 作用:存储测量后的View宽 / 高
  * 注:该方法即为我们重写onMeasure()所要实现的最终目的
  */
  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  

    //参数说明:测量后子View的宽 / 高值

    // 将测量后子View的宽 / 高值进行传递
        mMeasuredWidth = measuredWidth;  
        mMeasuredHeight = measuredHeight;  
      
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
    } 
  // 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
  // 下面继续看getDefaultSize()的介绍
/**
  * 分析3:getDefaultSize()
  * 作用:根据View宽/高的测量规格计算View的宽/高值
  */
  public static int getDefaultSize(int size, int measureSpec) {  

    // 参数说明:
    // size:提供的默认大小
    // measureSpec:宽/高的测量规格(含模式 & 测量大小)

    // 设置默认大小
    int result = size; 
        
    // 获取宽/高测量规格的模式 & 测量大小
    int specMode = MeasureSpec.getMode(measureSpec);  
    int specSize = MeasureSpec.getSize(measureSpec);  
  
    switch (specMode) {  
        // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  

        // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  

    // 返回View的宽/高值
    return result;  
  }   

总结

ViewGroup的measure过程

测量原理
从ViewGroup至子View、自上而下遍历进行(即树形递归), 通过计算整个ViewGroup中各个View的属性, 从而最终确定整个ViewGroup的属性

遍历测量所有子View的尺寸(宽/高)
合并所有子View的尺寸(宽/高),最终得到ViewGroup父视图的测量值

/**
  * 源码分析:measure()
  * 作用:
  *    1. 基本测量逻辑的判断;
  *    2. 调用onMeasure()
  * 注:与单一View measure过程中讲的measure()一致
  */ 
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    // 仅展示核心代码
    // ...

    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
            mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {

        // 调用onMeasure()计算视图大小 -> 分析1
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
      // ...
}

/**
  * 分析1:onMeasure()
  * 作用:遍历子View &测量
  * 注:ViewGroup = 一个抽象类 = 无重写View的onMeasure(),需自身复写
  **/
/**
  * onMeasure()
  * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
  *      b. 存储测量后的View宽 / 高:setMeasuredDimension()
  */ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 参数说明:View的宽 / 高测量规格
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :获得View宽/高的测量值 ->>分析2
    // 传入的参数通过getDefaultSize()获得 ->>分析3
}

复写onMeasure()
针对Measure流程,自定义ViewGroup的关键在于:根据需求复写onMeasure(),从而实现子View的测量逻辑。复写onMeasure()的步骤主要分为三步:

遍历所有子View及测量:measureChildren()
合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:需自定义实现
存储测量后View宽/高的值:setMeasuredDimension()
具体如下所示

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

      //仅展示关键代码
      ...

      // 步骤1:遍历所有子View & 测量 -> 分析1
      measureChildren(widthMeasureSpec, heightMeasureSpec);

      // 步骤2:合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
       void measureCarson{
           ... // 需自定义实现
       }

      // 步骤3:存储测量后View宽/高的值
      setMeasuredDimension(widthMeasure,  heightMeasure);  
      // 类似单一View的过程,此处不作过多描述
}



/**
  * 分析1:measureChildren()
  * 作用:遍历子View & 调用measureChild()进行下一步测量
  */ 
  protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
      // 参数说明:父视图的测量规格(MeasureSpec)

      final int size = mChildrenCount;
      final View[] children = mChildren;

      // 遍历所有子view
      for (int i = 0; i < size; ++i) {

          final View child = children[i];

          // 调用measureChild()进行下一步的测量 ->分析2
          measureChild(child, widthMeasureSpec, heightMeasureSpec);

      }
  }

/**
  * 分析2:measureChild()
  * 作用:1. 计算单个子View的MeasureSpec
  *      2. 测量每个子View最后的宽 / 高:调用子View的measure()
  */ 
  protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {

      // 1. 获取子视图的布局参数
      final LayoutParams lp = child.getLayoutParams();

      // 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);

      // 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
      // 下面的流程即类似单一View的过程,此处不作过多描述
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 }

 总结


View的layouv

layout 方法的作用是确定元素的位置。 ViewGroup 中的 layout 方法用来确定子元素的位置, View 中的 layout方法则用来确定自身的位置

View的draw流程 

单一View的draw过程

ViewGroup的draw过程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值