view,viewgroup,layoutparam介绍,
view的measure(),layout(),draw(),
添加到window显示,
自定义view,
事件处理motionevent,
gestureDetector,
坐标系,mScrollX/mScrollY
measureSpec,
滑动,
动画(分开写),
等会看看这个文章https://www.jianshu.com/p/a790982fd20e
是什么:
View是所有组件的基类,负责在屏幕占据一个矩形区域,在里面绘制和交互,用来创建交互式的UI组件,比如按钮、文本框等。
ViewGroup是View的子类,是一个隐形的负责在其中的view的布局摆放,要配合LayoutParam类使用
怎么使用:
一般不直接使用View或ViewGroup,而是使用上面提到的具体View子类,比如Button/TextView/RadioButton等,ViewGroup一般用线性布局LinearLayout,相对布局RelativeLayout,约束布局ConstraintLayout等
有两种使用方式
在xml文件中声明使用
1.现在xml文件中声明需要的控件,这些控件外一般会有一个viewgroup负责控制控件的摆放。一个控件对应的有哪些属性可以在xml里指定,比如textview直接点击他的text属性,就会跳转到他所有属性声明的attr.xml文件,就能看到所有textview的属性。
//layout_one_activity.xml(只看单个控件声明)
<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:clickable="true"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"/>
2.获取对应的xml文件并展示
xml对应的获取方式是他的ID ,例如layout_one_activity.xml,
就应该是Inflater.inflate(R.layout.layout_one_activity,rootView,null);或者activity直接在onCreate()中setContentView(R.layout.layout_one_activity);
3.根据id找到对应的控件
例如当前layout_one_activity.xml已经经过setContentView()设置给了activity这个textview就可以通过TextView tv1 = getViewById(R.id.tv1)获取。
在代码中初始化
1.使用new来初始化一个控件对象
TextView tv1 = new TextView(context);
//设置一些属性
tv1.setText("text_display");
2.添加到需要的地方,这个地方指activity、dialog、window、remoteview等,这些都是view可以依附展示的(系统展示view的过程)。
这里举例添加到activity里activity这篇文章里面说到contentview是一个viewgroup,如果用的是线性布局LinearLayout,则添加是需要配合LinearLayout.LayoutParam
LinearLayout.LayoutParam lp = new LinearLayout.LayoutParam(width,height);
addContentView(tv1,lp);
布局ViewGroup
这里布局,主要说的是ViewGroup怎么摆放内部的子view,系统是提供了一些容器可以直接使用,比如LinearLayout 在他的onLayout方法里去摆放子view,如果是竖直排列,它会获取所有的子view遍历,那子view的top坐标就会top +=之前子view高度。那么子view就是顺序向下摆放。
有两种使用方式
在xml文件中声明使用
在代码中初始化
LayoutParam
是保存子view的宽、高、边距、内边距等等信息的,告诉他的父容器,我想摆在哪个地方。每个对应的viewgroup都有自己的layoutparam子类比如LinearLayout.LayoutParam RelativeLayout.LayoutParam等使用时要对应使用,否则会抛异常。ViewGroup类中的LayoutParam是默认获取了layout_width,layout_height两个属性,所以他初始化的方法有这两个参数。这些都可以自定义时也可以添加其他属性。底下再展开。
使用
View measure() layout() draw()
上面说布局就是涉及到了layout这个步骤,自定义viewGroup指定要重写这个方法,这个自定义view官方文档说要自定义要重写onDraw(),onMeasure()。他们属于一个view的初始化过程,为什么是measure->layout->draw呢,可以点代码跳进去看,从view.onMeasure跳到一个performMeasure最终就到了一个八百多行的viewRootImpl.performTraversals里,里面反正就是按顺序调用了三个方法
performMeasure(){mView.measure()},
preformLayout(mView.layout()),
performDraw(){mView.draw()}
这三个方法都干啥了呢
measure()
- 子view的measure就是在getDefaultSize()中根据MeasureSpec返回。如果是AT_MOST和EXACTLY,那返回的就是MeasureSpec中的size。UNSPECIFIED就返回系统内部测量的宽高,如果有drawable背景,那么返回的就是这个drawable的宽高。
- viewgroup的measure是没有具体实现的,主要的逻辑就是完成自己的measure并且获取所有的子view完成他们的测量。所以看下系统自带的实现。
measure之后,就可以通过getMeasureWidth()getMeasureHeight()获取宽高了
layout()方法是确定view本身的位置,onLayout()是确定子view的位置,主要通过setFrame()确定left、top、right、bottom四个顶点的坐标,viewGroup就是获取子view的宽高,根据自己的逻辑来计算坐标放置子view。
draw()
view.draw()里面也是一步一步的绘制,这个注释就写了
1.绘制背景 background.draw();
2.绘制自己 onDraw();
3.绘制子view
4.绘制装饰

一个小知识 在view中有个setWillNotDraw方法,设置为true,就标记这个view不具备绘制功能,系统就会自动对它进行优化。设置为false,就是view具备绘制功能。
一个小知识 如何在activity一启动就获取某个view的宽高
1.activity/view.onWindowFocusChanged()这个方法说明view已经初始化完成了,但是如果activity频繁的onResume/onPause()那么这个方法也会被频繁地调用。
2. view.post(runnable) post提交一个runnable到消息队列的尾部,要消费的时候view也就初始化完成了
3. viewTreeObserver 在view树的状态发生改变时,会调用onGlobalLayout,但这个方法会伴随view树状态改变调用多次,所以要及时移除监听。
Activity
void onStart() {
ViewTreeObserver vto = view.addViewTreeObserver;
vto.addOnGlobalLayoutListener(new ) {
void onGlobalLayout() {
//要及时移除
view.removeGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
}
}
}
自定义View
上面说了可以自定View,它主要有以下几种
1.继承View 重写onDraw(),要自己支持WARP_CONTENT, padding这些属性。
2. 继承ViewGroup,重写measure(),layout(),要处理好自己的测量还有子view的measure和layout
3. 继承现有的View 比如TextView 、Button等
4. 继承现有的ViewGroup 比如LinearLayout、FrameLayout等
一个小知识 尽量不要在view中使用handler,view有自己的post方法。
一个小知识 view中如果有线程、动画等,可以在view.onDetachedFromWindow()里处理,对应Activity退出或view被remove的时候。onAttachedFromWindow()是启动的时候会调用。这个就要跟window初始化放在一起讲讲
事件分发
就是当用户点击屏幕,会有一个MotionEvent生成,这个event在view中层级传递的过程就叫事件分发。从activity层级传递如下图。如果view的onTouchEvent返回false,说明view无法处理,会一层一层向上传递调用父类的onTouchEvent,最终会调用Activity的onTouchEvent直到被消费。

一个小知识 如果viewgroup把onInterceptTouchEvent方法重写并且返回true的话,这样viewgroup会在dispatchTouchEvent里当move、up事件来的时候,会把自己的intercept参数设置成true,这样再来事件的时候除了down事件每次都会走onInterceptTouchEvent方法,move、up就都不会再走onInterceptTouchEvent这个方法了。
三个方法主要逻辑伪代码如下《Android进阶之光》他俩写的都差不多,就记录一下
public void onDispatchTouchEvent(MotionEvent event) {
boolean result = false;
if(onInterceptTouchEvent(event)){
result = super.onTouchEvent(event);
}else {
result = child.onTouchEvent(event);
}
}
一个小知识 如果view设置了onTouchListener,那么这个优先级比onTouchEvent()方法高,有了触摸事件会先走listener的onTouch()方法,但如果这个onTouch()返回false,那他又会走onTouchEvent()方法。
滑动冲突
这个就是从上面的事件分发里面整出来的,关键一句话就是拦截需要的事件并处理。一般遇到的是比如父组件上下滑动和子view左右滑动冲突,要不就是两个嵌套的都是可以上下滑动的。ViewGroup拦截,重写onInterceptTouchEvent方法。比如父组件上下子view左右滑动冲突,就在父组件中只拦截上下滑动的事件(ΔY>ΔX的时候返回true),其他的传递给下层view。
GestureDetector
手势检测,辅助检测单击、双击、长按、滑动等行为。要用就初始化一个对象GestureDetector mGesture = GestureDetector(OnGestureListener的实现类对象),然后用在view的onTouchEvent里面result = mGesture.onTouchEvent(event)。return result;
一个小知识如果只是处理滑动,可以不用这个类,直接在onTouchEvent里面处理就行,如果要监听双击或者快速滑动啥的(就是你看看Listener里面都支持啥功能)还有一个类OnDoubleTapListener,也是双击啥的,可以了解了解
动画
一些重要概念
1.屏幕坐标系
屏幕左上角是原点,向右x轴正方向,向下y轴正方向。
系统提供api可以获取view的具体位置

viewA 到ViewGroupB: getLeft(),getTop(),getRight(),getBottom()
view的宽度:width = getRight() - getLeft()
view的高度:height = getBottom() - getTop()
触摸点到屏幕 :getRawX(),getRawY()
触摸点到view: getX(),getY()
MeasureSpec
MeasureSpec是把布局需要的尺寸size和模式mood压缩成一个32位int值,前2位是mode,后30位是size。通过这个int值获取到mode,就是前面说的AT_MOST,EXACTLY,UNSPECIFIED这三种。通过Mode算出对应的view的宽高.
MeasureSpec和LayoutParam的关系: DecorView MeasureSpec是由窗口尺寸和自身的LayoutParam决定的,普通View通过父容器的MeasureSpec和自身的LayoutParam决定自己的MeasureSpec.我们给View设置LayoutParam系统会在父容器的约束下转换为对应的MeasureSpec,然后由这个确定View的宽高,调用ViewGroup.getChildMeasureSpec。
ViewGroup.getChildMeasureSpec总结表
父容器 | AT_MOST | EXACTLY | UNSPECIFIED |
---|---|---|---|
子view 指定大小A | A (EXACTLY) | A (EXACTLY) | A (EXACTLY) |
子view MATCH_PARENT | 父亲剩余空间(EXACTLY) | 父亲剩余空间(AT_MOST) | 0 (UNSPECIFIED) |
子view WARP_CONTENT | 父亲剩余空间(AT_MOST) | 父亲剩余空间(AT_MOST) | 0 (UNSPECIFIED) |
滑动
一个View声明的时候,高度高度可以是指定的数值EXACTLY,也可以是AT_MOST指定取值为match_parent,但如果指定高度超过屏幕,那么也无法展示出来,这就需要我们的控件支持滑动。
实现滑动有以下几种方式
- layout();
- offsetLeftAndRight();/offsetTopAndBottom();
- LayoutParams
- 动画
- scrollTo/scrollBy
- Scroller
1.layout()方法
通过view的onTouchEvent方法,在ACTION_MOVE的时候调用layout()方法,将坐标设置为最新的左上右下(通过过去触摸滑动的距离(x,y),将view坐标加上这些偏移量进行移动)
2.offsetLeftAndRight();/offsetTopAndBottom();
调用的地方是相同的,不过是把MOVE的偏移量通过这两个方法设置即可x位移量通过offsetLeftAndRight();Y的偏移量通过offsetTopAndBottom();他俩是改变view的位置的,改变之后就是getLeft()/getTop()啥的获取值就变了。
3.LayoutParams
调用的地方是相同的,setLayoutParam()将偏移量设置给layoutParam.X =x+Δx;layoutParam.Y =y+Δy
4.动画
通过属性动画设置位移
5.scrollTo-->移动到指定x,y
6.scrollBy-->移动指定的偏移量Δx,Δy。这个偏移量是相对于view和view内容的位置来的,只会移动内容,不会移动view,如果是想作用到view上,应该获取他的parent来执行scrollBy。scrollBy内部其实也是调用了scrollTo实现的。
第一次调用肯定觉得设置正数就是跟着手指向右边滑动,设置成负数就会跟着手指香左边滑动。但是实际效果会奇怪,为啥要-(x)-(y)才会跟着手走,源码用到mScrollX/Y是在重绘的时候。

可以想象View的左上顶点A和它的Content的左上顶点B,scrollX= BA向量,与x轴同向时为正,scrollY= BA向量,与y轴同向时为正。可以看看下图。

7.Scroller
就这个可以实现平滑有过渡的移动,上面的方法就设置偏移量一步到位了,有些生硬
是一个滑动类,如果要让view配合scroller平滑有过度的移动,那么要和view的computeScroll()方法联合使用。第一步是初始化一个Scroller。第二步重写view的computeScroll()方法。第三步在需要的地方提供mScroller.startScroll(mScrollX, mScrollY, dx, dy,time); 这四个值都是像素值,并且正值往屏幕坐标系的负方向走的。time不传默认250毫秒

网友评论