View是所有控件的基类,布局控件继承自ViewGroup,ViewGroup是所有View和ViewGroup的集合。ViewGroup也是继承自View。
Android坐标系:以屏幕左上方为原点,触控事件getRawX(),getRawY()得到的就是Android坐标。
View坐标系 :和Android坐标系共同存在,如图:
根据图,获取自身高度可以通过getBottom()-getTop()。也可以通过getHeight()直接得到View的高度。
MotionEvent提供的方法:
getX():获取到控件左边的距离,是视图坐标。
getY():获取到控件上方的距离,是视图坐标。
getRawX():获取到屏幕左边的距,是绝对坐标。
getRawY():获取到屏幕上方的距离,是绝对坐标。
View的滑动
layout()方法
view会调用onLayout()方法来显示位置。
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(getLeft()+offsetX,getTop()+offsetY,
getRight()+offsetX,getBottom()+offsetY);
break;
}
return true;
}
首先自定义View继承View,然后重写onTouchEvent方法,根据MotionEvent计算出偏移量,然后使用layout重新绘制,这样就能实现View的滑动了,注意,layout方法中的参数位置必须是左上右下。
offsetLeftAndRight和offsetTopAndBottom
方法和layout差不多,修改如下:
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
改变layoutParam
通过修改布局参数,修改如下:
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)
getLayoutParams();
layoutParams.leftMargin = getLeft()+offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
使用ScrollBy和ScrollTo
ScrollBy和ScrollTo是View的方法,ScrollBy是根据ScrollTo实现的,注意这里移动的是view的位置,因此偏移量应该是负值。
使用Scroller
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
public void smoothScrollerTo (int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
使用computeScroll计算位置,用invalidate启动View的重新绘制。用startScroll方法控制平滑移动。
动画
ObjectAnimator
可以通过静态工厂来返还一个ObjectAnimator对象,参数必须包括一个对象和对象的属性名字,这个属性必须有set和get方法,内部通过反射机制修改对象的属性名。
ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(mCustomView,
"translationX",200);
mObjectAnimator.setDuration(300);
mObjectAnimator.start();
因此我们可以通过包装类的方法来给View的属性加上get和set方法。
private static class MyView{
private View mTarget;
private MyView(View mTarget){
this.mTarget = mTarget;
}
public int getWidth(){
return mTarget.getLayoutParams().width;
}
public int setWidth(int width){
return mTarget.getLayoutParams().width;
}
}
ValueAnimator不提供任何动画效果,它像一个数值发生器,用来产生一定规律的数字,从而让调用者控制动画的实现过程。完整的动画具有start,repeat,end,cancel4个过程。大部分时候我们只关心onAnimationEnd的事件,Android也提供了AnimatorListenerAdapter来让我们选择的必要事件进行监听。AnimatorSet提供了一个play方法,如果在这个方法里面传入一个Animator对象,将会返回一个Animator.Builder的实例,Builder类采用建造者模式,每次调用方法都会返回自身用于继续构建,包括以下4个方法:
- after(Animator anim):将现有的动画插入到传入的动画后进行。
- after(long delay):将现有动画延迟指定毫秒后执行。
- before(Animator anim):将现有的动画插入到传入的动画前进行。
- with(Animator animator):将现有动画和传入的动画同时执行。
如下,创建3个动画,让动画3先执行,然后执行动画1和动画2.
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mCustomView,"translationX",0.0f,200.0f,0f);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(mCustomView,"scaleX",1.0f,2.0f);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(mCustomView,"rotationX",0.0f,90.0f,0.0f);
AnimatorSet set = new AnimatorSet();
set.setDuration(1000);
set.play(animator1).with(animator2).after(animator3);
set.start();
或者使用PropertyValuesHolder也可以实现组合动画,但是动画只能一起执行,如下:
PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX",
1.0f,1.5f);
PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("scaleX",
1.0f,1.5f);
PropertyValuesHolder valuesHolder3 = PropertyValuesHolder.ofFloat("scaleX",
1.0f,1.5f);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mCustomView,
valuesHolder1,valuesHolder2,valuesHolder3);
objectAnimator.setDuration(2000).start();
也可以使用xml来定义动画,然后用Animator的loadAnimator方法引用动画。
Scroller
Scroller有三个构造方法,通常情况下我们使用第一个构造方法,第二个构造方法传入一个插值器Interpolator。
在startScroll()方法中,并没有调用开始滑动的方法,而是保存了传入的各种参数,startX和startY表示滑动开始的起点,dx和dy表示滑动的距离。在startScroll()之后我们调用了invalidate方法进行了view的重绘,重绘调用了View的draw方法,而draw方法又会调用View的computeScroll方法,如下:
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
这样就实现了移动,在scrollTo之前,调用了方法computeScrollOffset,首先会计算动画的持续时间timePassed,如果动画的持续时间小于mDuration,那么执行Switch语句,因为之前在startScroll方法中mMode的值为SCROLL_MODE,所以执行该分支语句,然后根据插值器来计算该段时间移动的距离,赋值给mCurrX和mCurrY,这样就能获得当前的ScrollX和ScrollY了。另外computeScrollOffet返回值为true那么表示滑动没有结束,那么会一直移动,返回false表示滑动结束。原理就是根据computeScrollOffset计算当前位置,然后用scrollTo进行重绘,不断计算,不断重绘就形成了动画。
View中的事件分发机制。
Activity的构成:
点击事件用MotionEvent表示,当一个点击事件产生后,事件最先传递给Activity,当我们写Activity的时候会调用setContentView方法来加载布局,调用了getWindow方法,getWindow指的是PhoneWindow,然后PhoneWindow的setContentView方法中调用了installDecor,installDecor调用了generateDecor,generateDecor创建了一个DecorView,这个DecorView就是Activity中的根View。然后看PhoneView中的generateLayout方法,主要就是根据不同的情况给不同的布局loyoutResource。其中有布局R.layout.screen_title,布局中ViewStub标签是用来显示ActionBar的,下面两个FrameLauput,一个是title,用来显示标题,另一个是content,用来显示内容。如图:
当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent传递给View层级,在View层级中的传递过程就是事件分发。。然后系统就会将这个MotionEvent传递给View层级,在View层级中的传递过程就是事件分发。控制事件分发的三个重要方法:
- dispatchTouchEvent(MotionEvent ev) - 用来进行事件的分发。
- onInterceptTouchEvent(MotionEvent ev) - 用来进行事件的拦截,在dispatchTouchEvent中调用。
- onTouchEvent(MotionEvent ev) - 用来处理点击事件,在dispacherTouchEvent方法中调用。
view事件的分发机制:
事件交给Activity,具体的工作交给PhoneWindow,PhoneWindow把工作交给DecorView,DecorView再把工作交给ViewGroup,从ViewGroup的dispacherTouchEvent开始分析。首先判断事件是否是DOWN事件,如果是,那么进行初始化,resetTouchState会把mFirstTouchTarget的值置为null。因为每个事件都是DOWN开始,以UP结束的,所以如果是DOWN事件表示一个新事件开始了,需要进行重置。然后进行判断,如果当前拦截了事件,那么mFirtTouchTarget != null则为false,那么这时候如果是down事件,那么会触发onInterceptTouchEvent事件,然后出现了一个标志位,主要是为了防止拦截down以外的时间。那么就是ViewGroup需要拦截事件的时候,后序的事件都会交给他来处理,而不再调用onInterceptTouchEvent了。
onIntercepter
首先遍历ViewGroup中的子元素,判断子元素能否接受到点击事件,这个循环是从外层向里层遍历的,判断触摸点位置是否在子View的范围内或者View是否在播放动画,然后执行dispacherTransformedTouchEvent,如果有子View,那么调用子View的dispacherTouchEvent方法,如果OnTouchListener不为null并且onTouch方法返回true,表示事件被消费,就不会执行onTouchEvent,在onTouchEvent中,只要View的CLICKABLE和LONG_CLICKABLE中有一个为true,那么就会返回true消耗这个事件。
分发事件总结
当发生点击事件的时候,点击事件自上向下传递,如果没有被拦截,那么会传递到底层容器。同样的底层容器没有消耗事件并处理,那么就会传递给父view的onTouchEvenet处理,如果onTouchEvenet发生错误,那么继续向上传递。
View的工作流程
主要是指measure,layout和draw,其中measure用来测量view的宽高,layout用来确定View的位置,draw用来绘制View。
View的工作流程
当我们调用Activity的startAcitivty方法的时候,最终调用的是Activity的handlerLaunchActivity方法来创建Activity的,handlerLaunchActivity里调用了performLaunchAcitivty,这里会调用到Acitivity的onCreate方法,从而完成DecorView的创建,接着调用handleResumeActivity方法,然后在handleResumeActivity中的performResumeActivity方法中调用了Acitivity的onResume方法,然后相继得到了DecorView,WindowManager,WindowManager是一个接口并继承了接口ViewManager,之后调用了WindowManager的addView方法,实际上最终调用了WindowManagerGlobal的addView方法。然后创建了ViewRootImpl实例,然后调用了ViewRootImpl的setView方法并将DecorView作为参数传进去,这样就就把DecorView加载到了Window中。
ViewRootImpl的PerformTraveals方法
PerformTraversal使得ViewTree开始View的工作流程。主要执行了三个方法,分别是performMeasure,performLayout,
performDraw,内部调用了View的对应方法。其中performMeasure需要传入两个参数,分别是childWidthMeasureSpec和childWidthMeasureSpec。
MeasureSpec
MeasureSpec是View的内部类,主要是根据父容器的限制来确定MeasureSpec,然后在onMeasure中根据MeasureSpec来确定View的宽和高。那么顶层的DecorView的MeasureSpec是如何得到的呢?在PerformTraveals中调用了getRootMeasureSpec方法,根据自身的LayoutParams和窗口尺寸来确定MeasureSpec
View的measure流程
View的measure用来测量View的宽和高,ViewGroup的measure除了要完成自己的测量,还要遍历调用子元素的measure方法。SpecMode是View的测量模式,而SpecSize是View的测量大熊啊,View在AT_MOST和EXACTLY的模式下,都返回SpecSize的值,在UNSPECIFIED模式下,返回的是getDefaultSize方法的第一个参数Size的值,size是根据getSuggestedMinimumWidth或者getSuggestedMinimumWidth方法得到的,getSuggestedMinimumWidth会判断背景和设置的mMinWidth的大小,如果没有背景就返回mMinWidth,否则返回两者真的较大值。
ViewGroup的measure流程
不仅要测量自身,还要递归调用子元素的measure方法,在getChildMeasurSpec中根据父容器的MeasureSpec模式结合子元素的LayoutParams属性来得到子元素的MeasureSpec属性。比如在measureVertical定义了mTotalLength用来存储LinearLayout在垂直方向的高度,然后遍历子元素,根据子元素的MeasureSpec模式分别计算每个子元素的高度,最后加上padding值。
View的layout流程
layout方法用来确定子元素的位置,layout方法的4个参数l,t,r,b分别是View从左,上,右,下相对于其父容器的距离。setFrame方法用传进来的l,t,r,b4个参数fen’bie初始化mLeft,mTop,mRight,mBottom这4个值,这样就确定了View在父容器的位置。View和View Group中都没有实现onLayout方法,LinearLayout的onLayout方法,根据方向来调用不同的方法。比如layoutVertical方法,会遍历子元素,并调用setChildFrame方法,其中childTop的值是不断累加的,这样能够保证子元素不会重叠。
View的draw流程
(1) 如果需要,绘制背景。
(2) 保存当前的canvas层。
(3) 绘制View的内容。
(4) 绘制子View。
(5) 如果需要,绘制View的褪色边缘。
(6) 绘制装饰,比如滚动条。
绘制背景是调用了View的drawBackgroud方法,其中考虑了偏移参数scrollX和scrollY。绘制View的内容时,调用了View的onDraw方法,在自定义View中重写该方法来实现。绘制子View调用了dispatchDraw方法,也是一个空实现,在dispatch方法中对子类View进行遍历,并调用drawChild方法,主要调用了View的draw方法,判断是否有缓存,如果没有缓存那么正常绘制,如果有,那么利用缓存显示。绘制装饰的方法为View的onDrawForegroud方法,用于绘制ScrollBar以及其他装饰,并将它们绘制在视图内容的上层。
自定义View
继承系统控件的自定义View
一般是为了添加新的功能或者修改显示的效果,一般情况下载onDraw方法中处理。
继承View的自定义View
不只要实现onDraw方法,还要考虑到wrap_content和padding属性的设置,onTouchEvent方法等。
自定义属性
首先在values目录下创建attrs.xml,然后定义属性如下:
<resources>
<declare-styleable name="RectView">
<attr name="rect_color" format="color"/>
</declare-styleable>
</resources>
接下来需要在RectView中的构造方法解析自定义属性的值
public RectView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.RectView);
mColor = mTypedArray.getColor(R.styleable.RectView_rect_color,Color.RED);
mTypedArray.recycle();
initDraw();
}
然后就可用app:rect_color的方式来获取自定义的属性值了。
自定义组合控件
就是多个控件组合起来成为一个新的控件。首先需要定义组合控件的布局。然后组合控件继承布局,在布局中做一些初始化的工作。使用自定义属性,在布局的构造方法中解析自定义属性的值。然后使用组合控件的布局。然后在活动中调用自定义的组合控件,设置点击事件。