View体系之控件体系分析和view的事件分发机制

View体系之view控件体系分析
    
        前面我们讲过了利用activity来呈现界面,那么具体是如何呈现的呢?那么我们来回顾一下:
(1)定义一个布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn_start"
android:layout_width="100dp"
android:layout_height="100dp"
android:text="StartService"
/>
</LinearLayout>
(2)定义一个Acitvity
public class TestActivity extends AppCompatActivity implements View.OnClickListener
{
@Override
protected void onCreate ( @Nullable Bundle savedInstanceState)
{
super .onCreate(savedInstanceState) ;
setContentView(R.layout. layout_test ) ;
}
}

然后在manifest中声明,这里我就不写了。
到这里我们一个简单的界面展示就写完了,很简单对吧,那么我们来提出几个问题:
(1)在setContentView()中执行了什么操作?为什么执行完后界面就可以加载出来了
(2)布局中是一个LinearLayout中包含了一个Buttton,那么在绘制时他们是如何进行测量、定位、绘制的?
其实我们这里提到的问题就在今天的View控件体系中。
 好了,闲话不多说,进入正题。


一、Android的控件体系
        首先让我们接着之前的话题,来看一下setContentView()具体做了什么?
        首先我们看Activity的setContentView()源码

public void setContentView(View view) {
    getWindow().setContentView(view);
    initWindowDecorActionBar();
}

这里可以看到 调用了getWindow().setContentView(),这里的getWindow()如下:

public Window getWindow() {
    return mWindow;
}

    这里提到了Window,Window是一个抽象类,提供了各种窗口操作的方法,但是使用的使用,需要使用Window的唯一实现类PhoneWindow。而在PhoneWindow中有一个DecoreView的内部类。
DevoreView继承自FrameLayout,
    所以这里我们看PhoneWindow的setContentView方法

public void setContentView(View view, LayoutParams params) {
if (this.mContentParent == null) {
this.installDecor();
} else if (!this.hasFeature(12)) {
this.mContentParent.removeAllViews();
}

if (this.hasFeature(12)) {
view.setLayoutParams(params);
Scene newScene = new Scene(this.mContentParent, view);
this.transitionTo(newScene);
} else {
this.mContentParent.addView(view, params);
}

this.mContentParent.requestApplyInsets();
android.view.Window.Callback cb = this.getCallback();
if (cb != null && !this.isDestroyed()) {
cb.onContentChanged();
}

this.mContentParentExplicitlySet = true;
}

可以看到这里PhoneWindow的内部类DecoreView,DecoreView会将需要显示的内容呈现在phoneWindow上,而DecoreView包含了两个部分:TiltleView和ContentView,其中TitleView也就是Activity上的标题栏,而ContentView则是我们将要呈现的我们手动添加的布局。
到这里你会发现,原来对Acitivty的主题进行判断(比如说有没有标题栏等),都在setContentView中。
在使用中我们经常调用 requestWindowFeature(Window.FEATURE_NO_TITLE);来去掉标题栏,那么这段代码又做了什么呢?
首先看requestWindowFeature()

public boolean requestFeature(int featureId) {
    if (this.mContentParentExplicitlySet) {
        throw new AndroidRuntimeException("requestFeature() must be called before adding content");
    } else {
        int features = this.getFeatures();
        int newFeatures = features | 1 << featureId;
        if ((newFeatures & 128) != 0 && (newFeatures & -13506) != 0) {
            throw new AndroidRuntimeException("You cannot combine custom titles with other title features");
        } else if ((features & 2) != 0 && featureId == 8) {
            return false;
        } else {
            if ((features & 256) != 0 && featureId == 1) {
                this.removeFeature(8);
            }

            if ((features & 256) != 0 && featureId == 11) {
                throw new AndroidRuntimeException("You cannot combine swipe dismissal and the action bar.");
            } else if ((features & 2048) != 0 && featureId == 8) {
                throw new AndroidRuntimeException("You cannot combine swipe dismissal and the action bar.");
            } else if (featureId == 5 && this.getContext().getPackageManager().hasSystemFeature("android.hardware.type.watch")) {
                throw new AndroidRuntimeException("You cannot use indeterminate progress on a watch.");
            } else {
                return super.requestFeature(featureId);
            }
        }
    }
}
这是一个boolean返回值的方法,这里我们就忽略哪些异常和发挥false和true的地方,
public static final int FEATURE_NO_TITLE = 1
因此我们这里找feature=1的判断条件
  if ((features & 256) != 0 && featureId == 1) {
                this.removeFeature(8);
            }
可以看到是调用了一个removeFeature(8)的方法,并且传入值为8.
接下来再看
protected void removeFeature(int featureId) {
    int flag = 1 << featureId;
    this.mFeatures &= ~flag;
    this.mLocalFeatures &= ~(this.mContainer != null ? flag & ~this.mContainer.mFeatures : flag);
}

可以看到最后是利用传入的值对mFeature和mLocalFeature进行了赋值。也就是说到这里我们的requestFeature执行完了,那么接下来就是setContentView()方法了,接着我们之前介绍setContentView的介绍。因为TitileView是在DecoreView中,因此我们关注DecoreView就可以了。

private void installDecor() {
    this.mForceDecorInstall = false;
    if (this.mDecor == null) {
        this.mDecor = this.generateDecor(-1);
        this.mDecor.setDescendantFocusability(262144);
        this.mDecor.setIsRootNamespace(true);
        if (!this.mInvalidatePanelMenuPosted && this.mInvalidatePanelMenuFeatures != 0) {
            this.mDecor.postOnAnimation(this.mInvalidatePanelMenuRunnable);
        }
    } else {
        this.mDecor.setWindow(this);
    }

    if (this.mContentParent == null) {
        this.mContentParent = this.generateLayout(this.mDecor);
        this.mDecor.makeOptionalFitsSystemWindows();
        DecorContentParent decorContentParent = (DecorContentParent)this.mDecor.findViewById(16909300);
        int transitionRes;
        if (decorContentParent == null) {
            this.mTitleView = (TextView)this.findViewById(16908310);
            if (this.mTitleView != null) {
                if ((this.getLocalFeatures() & 2) != 0) {
                    View titleContainer = this.findViewById(16908357);
                    if (titleContainer != null) {
                        titleContainer.setVisibility(8);
                    } else {
                        this.mTitleView.setVisibility(8);
                    }

                    this.mContentParent.setForeground((Drawable)null);
                } else {
                    this.mTitleView.setText(this.mTitle);
                }
            }
        } else {
          。。。
    }

}
这里代码比较多,简单来说就是会调用generateLayout将setContentView的内容复制到mContentParent

protected ViewGroup generateLayout(DecorView decor) {
                    ......

int features = this.getLocalFeatures();
int layoutResource;
if ((features & 2048) != 0) {
    layoutResource = 17367250;
} else {
    TypedValue res;
    if ((features & 24) != 0) {
        if (this.mIsFloating) {
            res = new TypedValue();
            this.getContext().getTheme().resolveAttribute(18219049, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = 17367252;
        }

        this.removeFeature(8);
    } else if ((features & 36) != 0 && (features & 256) == 0) {
        layoutResource = 17367247;
    } else if ((features & 128) != 0) {
        if (this.mIsFloating) {
            res = new TypedValue();
            this.getContext().getTheme().resolveAttribute(18219050, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = 17367246;
        }

        this.removeFeature(8);
    } else if ((features & 2) == 0) {
        if (this.mIsFloating) {
            res = new TypedValue();
            this.getContext().getTheme().resolveAttribute(18219051, res, true);
            layoutResource = res.resourceId;
        } else if ((features & 256) != 0) {
            layoutResource = a.getResourceId(48, 17367245);
        } else {
            layoutResource = 17367251;
        }
    } else if ((features & 1024) != 0) {
        layoutResource = 17367249;
    } else {
        layoutResource = 17367248;
    }
}
             ......
}
到这里我们会发现,这里 int features = this.getLocalFeatures();会获取刚才我们设置的 Window.FEATURE_NO_TITLE,从而remove调DecoreView的TitleView。如果我们在之后调用,那么此时setContentView已经执行,也就是这里的方法已经执行,也就不起作用了,同时还会抛出异常AndroidRuntimeException("requestFeature() must be called before adding content");

前面说了这么多,现在整理一下:
  • 常见的控件本质上来说就两种:View和ViewGroup,而ViewGroup也是继承于View,常见的ViewGroup实现类有FramLayout、LinearLayout等。通常ViewGroup既包含view也包含其他ViewGroup,上层控件负责下层组件的绘制和测量并传递交互事件,从上到下形成了组件树
  • 每一个Activity都有一个Window对象,但是Window是一个抽象类,因此在使用时是使用它的唯一实现类PhoneWindow。而PhoneWindow有一个内部类DecorView作为Activity的根View,它是一个继承自FrameLayout的View,主要包括两部分,TitleView和ContentView,其中TitleView就是我们常说的标题栏,而ContentView就是我们自己定义的布局。
  • 在setContent()方法的调用过程。Activity--setContentView——>Window--setContentView()——>?phoneWindow--setContentView()——>phoneWindow--installDecor()——>phoneWindow--     generateLayout()
           

二、理解MeasureSpec和ViewRootimpl
        Android在绘制一个组件的时候,其实就像我们在画画一样,比如说我们桌上现在有一个苹果,要求画画的时候必须一模一样,也就是形状大小比如完全一致。我们在画的时候不可能直接去画,因为我们并不知道这个屏幕的大小,因此我们需要先量一下这个苹果是多大。在绘制组件时也一样,我们需要知道这个组件的宽和高是多少,然后才能在画布上去画,那么我们就需要完成它的测量工作。
        在说到view的测量前,需要了解一个概念,就是ViewRoot,ViewRoot对应ViewRootImpl,而ViewRootImpl负责处理view事件,是联系WindowManager和DecorView的纽带。view的measure、layout、onDraw都是通它来完成的。
            首先是RootRootImpl的performTraverals()方法,而performTraverals方法会一次调用PerformMeasure、performLayout、performDraw。这三个方法的处理流程是类似的。这里以performMeasure为例,performMeasure会调用measure方法,而measure又会调用onMeasure()方法,而onMeasure方法会对所有的子元素进行measure。在所有的view均measure完成后,此时组件的宽和高就确定了。
            下面是自定义view测量宽高的常用方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   setMeasuredDimension(measureWidth(widthMeasureSpec),
         measureHeight(heightMeasureSpec));
}

private int measureWidth(int measureSpec){
   int widthModle = MeasureSpec.getMode(measureSpec);
   widthSize = MeasureSpec.getSize(measureSpec);
   switch(widthModle){
   case MeasureSpec.AT_MOST:
      return widthSize;
   case MeasureSpec.EXACTLY:
      return widthSize;
   default:
      return this.getLayoutParams().width;
   }
}

private int measureHeight(int measureSpec){
   int widthModle = MeasureSpec.getMode(measureSpec);
   heightSize = MeasureSpec.getSize(measureSpec);
   switch(widthModle){
   case MeasureSpec.AT_MOST:
      return heightSize;
   case MeasureSpec.EXACTLY:
      return heightSize;
   default:
      return this.getLayoutParams().height;
   }
}

这里需要注意这个int值的measureSpec,它是一个32位的int值,其中高2为代表SpecMode测量模式,而后30位表示SpecSize在某种测量模式下的规格大小。
常见的SpecMode有
  • EXACTLY  父容器已经测量出view的精确大小,通常是在view设置为match_parent或者具体数值
  • AT_MOST 父容器指定了一个最大的可用大小,view的大小不能超过这个值,通常为view指定了wrap_content,通常会有一个具体的值,当指定为wrap_content,则默认大小为某一个具体的值,这个值大小是小于父容器的剩余空间的。
  • UNSPECIFIED 父容器不对子view做任何限制

对于普通view而言,view的测量是由父组件的MeasureSpec和view自身的Layoutparams来共同决定的。系统会将layoutParams在父组件的MeasureSpec的约束下,转换为子组件的MeasureSpec,然后我们再利用转换后的MeasureSpec来完成子组件的高和宽。
  • view为指定大小,此时父容器的MeasureSpec无论是什么模式,view的MeasureSpec都是EXACTLY,并且组件的大小就是由LayoutParams指定的值。如果父容器的大小小于子组件的大小, 那么就会出现子组件被裁剪的现象。
  • view为match_parent,如果父容器是精确模式,那么子组件也是精确模式,并且大小为父容器的剩余空间;如果父容器是最大模式,那么子组件也是最大模式,并且子组件的大小不会超过父子间的剩余空间
  • view为wrap_content,此时不论父组件是精确还是最大,子组件的模式始终为最大并且大小不会超过父组件的剩余空间。
三、view的工作流程
        view的工作流程指的是measure、layout、draw即测量、布局、绘制三大过程。
1、measure过程
     measure过程需要分情况,如果说view下没有其他子元素,那么只需要调用你自己的measure方法就好了;如果view是一个viewGroup,那么除了完成自己的测量外,还会遍历所有子元素的measure方法。然后子元素再递归执行这个过程。
2、layout过程 
     调用layout()方法   ,viewGroup会确定自己的位置,同时在layout()中遍历所有的子view并调用他们的onLayout()。·
3、draw过程
       view的绘制过程是通过dispatchDraw来实现的,它会遍历调用 所有子view的draw()方法

四、自定义view
    1、  常见的自定义view分为四类:
  •   继承VIew  需要自己处理wrap_Content以及自己处理padding支持的问题
  • 继承ViewGroup  需要处理自身的测量和布局以及子元素的测量和布局过程
  • 继承特定的view(比如TextView)
  • 继承特定的ViewGroup(LinearLayout)
2、自定义view需要注意的事:
  • 直接继承view的控件,需要自己在onMeasure中处理wrap_content,此外如果使用padding属性,那么就需要在draw方法中处理padding
  • 直接继承viewGrou的控件,需要在处理测量和布局两个过程以及子view的测量和布局,此外,如果使用padding属性或者子组件使用margin属性,那么需要在onMeasure和onLayout中处理。
  • 尽量不要在view内部使用Handler,view内部提供了post方法
  • view找那如果有线程和动画,需要及时停止。可以在onDetachFromWindow()方法调用时停止线程或者动画(通常在包含view的Activity退出以及调用remove)
  • 如果会存在滑动冲突,需要处理滑动冲突

五、事件拦截机制分析
       无论是 点击时间还是触摸时间还是按键事件,在事件拦截上都是类似的,这里以触摸事件的拦截来分析。
       首先类比一个例子,技术总监,开发组长,组员,现在有一个需求需要开发,技术总监分发给组长,组长分配个组员,组员做好后汇报到组长这里,然后组长汇报到总监这里。在这里例子中,需求从总监到组长到组员的过程就是事件分发的过程,而组员到组长到总监的工作汇报过程就是事件处理的过程。
现在回到我们的事件拦截机制,在事件拦截机制中我们会涉及到下面三个方法
public boolean dispatchTouchEvent(MotionEvent event) {

}
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;
}
public boolean onTouchEvent(MotionEvent event) {
return false;
}

其中继承ViewGroup的组件中有以上三个方法,而继承View的组件只有dispatchTouchEvent和onTouchEvent()方法。
  • dispatchTouchEvent  事件分发
  • onInterceptTouchEvent  事件拦截
  • onTouchEvent 触摸事件处理
现在从源代码中来分析上面的例子:
事件分发                                                                     事件处理
总监
dispatchTouchEvent                                                      组员onTouchEvent
onInterceptTouchEvent                                                组长onTouchEvent
组长                                                                                总监onTouchEvent
dispatchTouchEvent
onInterceptTouchEvent
组员
dispatchTouchEvent

现在我们修改这个例子,假如总监发现这个需求很简单,于是自己就做完了,而没有分配给其他人。在总监的onInterceptTouchEvent()方法中返回true,表示将事件拦截下来了。于是有:
总监
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent
可以看到事件只到了总监这里,没有继续分发

接着我们继续修改例子,总监分发给组长,组长自己完成了并且汇报给了总监。那么就在组长的 onInterceptTouchEvent()方法中返回true,于是有:

事件分发                                                                     事件处理
总监
onInterceptTouchEvent                                                            组长onTouchEvent
组长                                                                                                    总监onTouchEvent
dispatchTouchEvent
onInterceptTouchEvent

接下来我们继续修改例子,加入组员做完了汇报给组长,但是组长觉得做的不好,于是没有汇报给总监,于是在组长的onTouchEvent中返回true,于是有:
事件分发                                                                     事件处理
总监
dispatchTouchEvent                                                                    组员onTouchEvent
onInterceptTouchEvent                                                            组长onTouchEvent
组长                                                                                             
dispatchTouchEvent
onInterceptTouchEvent
组员
dispatchTouchEvent



猜你喜欢

转载自blog.csdn.net/ckq5254/article/details/79952840
今日推荐