第三章 View体系与自定义View
- 本章将介绍Android中十分重要的View,在多本书中View是必讲的一节,Android群英传就讲了不少的View的知识,那么在这里我们再去复习一遍吧
3.1 View与ViewGroup
- View是所有控件的基类,ViewGroup也都是继承View的,View和ViewGroup形成一个View树
- 常用的TextView、ImageView、继承自View
- LinearLayout、RelativeLayout继承自ViewGroup
- 来一张图自己体会:
3.2 坐标系
3.2.1 Android坐标系
- 屏幕左上方为圆点,向右为x正方向,向下为y正方向,getRawX()和getRawY()获得的是Android坐标系的坐标
3.2.2 View坐标系
- 直接甩图:
1.View获取自身的宽和高
width = getRight() - getLeft(); height = getBottom - getTop(); // 然而系统提供的getWidth()和getHeight()的源码就是这么写的
2.View自身的坐标
通过如下方法可以获得View到其父控件(ViewGroup)的距离,记住要获取的都是距离x轴和y轴方向的距离:
- getLeft():获取View自身左边到其父布局左边的距离。
- getRight():获取View自身右边到其父布局左边的距离。
- getTop():获取View自身顶边到其父布局顶边的距离。
- getBottom():获取View自身底边到其父布局顶边的距离。
3.MotionEvent提供的方法
- getX():获取点击事件距离控件左边的距离,即视图坐标。
- getY():获取点击事件距离控件顶边的距离,即视图坐标。
- getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。
- getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。
3.3 View的滑动
- 不管哪种滑动方式,其基本思想都是类似的,当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来改变View的坐标
- 在这里讲6中滑动方法:
- layout()
- offsetLeftAndRight()与offsetTopAndBottom()
- LayoutParams
- 动画
- scrollTo与scrollBy
- Scroller
3.3.1 layout()方法
- View在进行绘制的时候会调用layout()方法来设置显示的位置,我们可以通过在onTouchEvent()方法的ACTION_MOVE事件中算出偏移量,然后去调用layout()方法来进行重绘,写完后在xml中直接定义就能用了,来写一次吧:
public class CustomView extends View { private int lastX; private int lastY; public CustomView(Context context) { super(context); } public CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override 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; default: } return true; } }
3.3.2 offsetLeftAndRight()与offsetTopAndBottom()
这两个方法和layout()方法基本差不多了,调一个方法变为调两个方法
case MotionEvent.ACTION_MOVE: // 计算移动的距离 int offsetX = x - lastX; int offsetY = y - lastY; // 左右方向来进行偏移 offsetLeftAndRight(offsetX); // 上下方向来进行偏移 offsetTopAndBottom(offsetY); break;
3.3.3 LayoutParams(改变布局参数)
通过改变margin来改变位置,还是替换ACTION_MOVE中的代码:
// 通过LinearLayuot.LayoutParams
LinearLayout.LayoutParams layoutParams =(LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
// 通过ViewGroup.MarginLayoutParams
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
3.3.4 动画
- 通过View动画来进行移动,在res目录新建anim文件夹并创建translate.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" xmlns:andorid="http://schemas.android.com/apk/res-auto" android:fillAfter="true"> <translate andorid:duration="1000" android:fromXDelta="0" android:toXDelta="300"/> </set>
- 然后直接在代码中调用:
mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
- 我们设置向右平移300px,然后又会回到原来的位置,如果在translate.xml中加入fillAfter="true"(上面的代码中已加),那么平移后就不会移动了
- View动画不能改变View的位置参数,比如一个button平移后,点击平移后的button并不会响应,点之前的位置才会响应,这个问题,可以通过属性动画来解决,属性动画不仅可以执行动画,还可以改变View的位置参数,使用代码:
ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();
3.3.5 scrollTo与scrollBy
- scrollTo(x, y)表示移动到一个具体的坐标点
- scrollBy(dx, dy)表示移动的增量为dx、dy
- scrollBy在源码中也是通过调用scrollTo来实现的
- scrollTo、scrollBy是移动Veiw的内容,在ViewGroup中使用代表移动其所有的子view
ACTION_MOVE中代码改为:
((View)getParent()).scrollBy(-offsetX,-offsetY);
- 为什么要改为负值呢,这里说一下,比如:scrollBy(50, 50)
- scrollBy(dx, dy)意味着手机屏幕相对画布移动的距离,所以屏幕向右移动50,向下移动50,结果控件向左上方移动了,那么我们如果想让子控件向右下移动,就得使用负值才能达到想要的效果
- 移动前:
- 移动后:
3.3.6 Scroller
- 在使用scrollTo()与scrollBy()的时候,这个过程是瞬间完成的,所以用户体验不好,我们可以使用Scroller来实现有过渡效果的滑动,Scroller本身不能实现View的滑动,它需要与View的computeScroll()方法配合才能实现弹性滑动效果
public class CustomView extends View { private Scroller mScroller; public CustomView(Context context) { super(context); } public CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(context); } public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 绘制的时候draw()方法中会调用该方法,并且只要invalidate(),也就是重绘就会调用该方法 */ @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { ((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } /** * 自定义一个方法,调用Scroller的startScroll()方法,在2000ms内沿X轴平移delta像素 * @param destX * @param destY */ public void smoothScrollTo(int destX, int destY) { int scrollX = getScrollX(); int delta = destX - scrollX; mScroller.startScroll(scrollX,0,delta,0,2000); invalidate(); } }
3.4 属性动画
在属性动画出现之前,Android系统提供的动画只有帧动画和View动画
1、帧动画其实就是不停的切换图片,还记得我们自己手画的小人书,然后从第一页开始快速拨动,就成了好看的动画
2、View动画应该很熟悉了,它提供了四种动画方式:
- AlphaAnimation(透明)、
- RotateAnimation(旋转)、
- TranslateAnimation(平移)、
- ScaleAnimation(缩放)、
- 并且提供了AnimationSet集合来混合使用多种动画
- 缺点:只能做普通的动画效果,无法交互,比如某一个元素发生View动画后,其响应事件的位置仍然在动画进行之前的地方
- 优点:就是效率比较高,使用也方便
3、Android3.0之后,谷歌推出了新的动画框架,Animator框架中使用最多的就是AnimatorSet和ObjectAnimator配合:使用ObjectAnimator进行更精细化强大的属性动画框架基本可以实现所有的动画效果
1.ObjectAnimator
- ObjectAnimator是属性动画最重要的类,创建其对象通过其静态工厂创建,参数包括一个对象和对象的属性名字,而且这个属性名字必须有get和set方法
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)
第一个参数是对象,得个参数是对象的属性,第三个参数是属性变化的取值过程
• translationX和translationY:用来沿着X轴或者Y轴进行平移。
• rotation、rotationX、rotationY:用来围绕View的支点进行旋转。
• PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点 位置就是View对象的中心点。
• alpha:透明度,默认是1(不透明),0代表完全透明。
• x和y:描述View对象在其容器中的最终位置。
使用ObjectAnimator的时候要操作的属性必须要有get和set方法
2.ValueAnimator
- ValueAnimator不提供任何动画效果,它更像是一个数值发生器,用来产生有一定规律的数字
- 通常情况下,在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从而完成动画的变换
3.动画的监听
- 完整的动画具有start、End、Cancel、Repeat这4个过程
- 大部分时候只关心onAnimationEnd事件,Android提供了AnimatorListenerAdapter来让我们选择事件来进行监听
ObjectAnimator animator = ObjectAnimator.ofFloat(mScroller,"alpha",1.5f); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); } @Override public void onAnimationRepeat(Animator animation) { super.onAnimationRepeat(animation); } @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); } @Override public void onAnimationPause(Animator animation) { super.onAnimationPause(animation); } @Override public void onAnimationResume(Animator animation) { super.onAnimationResume(animation); } });
4.组合动画——AnimatorSet
- AnimatorSet类提供了一个play()方法,向此方法中传入一个Animator对象,将会返回一个AnimatorSet.Builder的实例
AnimatorSet.Builder中包括以下4个方法。
- after(Animator anim):将现有动画插入到传入的动画之后执行。
- after(long delay):将现有动画延迟指定毫秒后执行。
- before(Animator anim):将现有动画插入到传入的动画之前执行。
- with(Animator anim):将现有动画和传入的动画同时执行。
// 比如有三个动画,那么可以这样执行 AnimatorSet set = new AnimatorSet(); set.play(animator1).with(animator2).after(animator3);
5.组合动画——PropertyValuesHolder
- 使用此类只能是多个动画一起执行,关键代码:
ObjectAnimator objectAnimator = ObjectAnimator.ofPrepertyValuesHolder(mCustomView, valuesHolder1, valuesHolder2,valuessHolder3); objectAnimator.setDuration(2000).start();
6.在XML中使用属性动画
- 和View动画一样,属性动画也可以直接写在xml文件当中
- 然后在代码中直接引用xml动画即可
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.scale); animator.setTarget(view); animator.start();
3.5 解析Scroller
这个不写了,一些源码,想看的可以看看,大体说一下
- Scroller三个构造方法,一般用第一个
- startScroll()做了一些前期数据准备,然后调用invalidate()方法进行View的重绘,View的重绘会调用View的draw()方法,draw()方法又会调用View的computeScroll()方法,我们去重写computeScroll()方法
- 通过Scroller来获取当前的ScrollX和ScrollY,然后调用ScrollTo()来进行View的滑动,接着调用invalidate()来让View进行重绘,重绘就会再次调用computeScroll()方法来实现View的滑动,这样通过不断的移动一个小的距离来连贯的实现平滑移动的效果,如何获取当前位置的ScrollX和ScrollY呢,那就是在scrollTo()方法前会调用Scroller的computeScrollOffset()方法
3.6 View的事件分发机制
3.6.1 源码解析Activity的构成
- 点击事件用MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity
- Activity的setContentView()方法大家很熟悉了吧,此方法中调用了getWindow().setContentView()方法
- getWindow()得到的是PhoneWindow,PhoneWindow中的setContentView方法中又调用了installDecor()方法
- 而installDecor()方法又调用了generateDecor()方法来返回一个DecorView(意为:装饰、布景),调用了generateLayout()方法来根据不同的情况加载不同的布局给layoutResource
总的来说,一个Activity包含一个PhoneWindow对象,PhoneWindow又将DecorView作为整个窗口的根布局,而这个根布局又将屏幕划分为两个区域,一个是TitleView,另一个是ContentView,来个图吧,清晰明了的:
3.6.2 源码解析View的事件分发机制
当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent,当MotionEvent产生后,系统就会将这个MotionEvent传递给View的层级,MotionEvent在View中的层级传递过程就是点击事件分发,三个重要的方法:
- dispatchTouchEvent(MotionEvent ev)——用来进行事件的分发,dispatch意为发出、派遣
- onInterceptTouchEvent(MotionEvent ev)——用来进行事件的拦截,在dispatchTouchEvent()中调用,View没有提供该方法,intercept意为拦截、阻止
- onTouchEvent(MotionEvent ev)——用来处理点击事件,在dispatchTouchEvent()中调用
1.View的事件分发机制
当点击事件发生时,事件先传递给Activity,这会调用Activity的dispatchTouchEvent()方法,当然具体的事件处理工作都是交给了PhoneWindow,PhoneWindow再把事件处理工作交给DecorView,然后DecorView再交给根ViewGroup
分析根ViewGroup的dispatchTouchEvent()方法:
- 简单说几句吧,mFirstTouchTarget==null时,代表拦截了事件,如果此时拦截了事件,如果这时触发DOWN事件,则会执行onInterceptTouchEvent(),如果是MOVE或UP事件则不执行onInterceptTouchEvent(),直接设置intercepted = true,onInterceptTouchEvent()并不是每次都会调用的
- 如果想要ViewGroup拦截事件,可以重写onInterceptTouchEvent()方法并返回true
- 如果有子View,则调用子View的dispatchTouchEvent(event)方法,如果没有子View则调用super方法
- 如果onTouchListener不为null并且onTouch方法返回true,表示事件被消费,就不会执行onTouchEvent(),否则就会去执行
2.点击事件分发的传递规则
书中用了一个倚天屠龙记的例子,就用这个例子来说吧,我们缩小范围为两个角色
- 我们分为武当掌门(根ViewGroup)、武当弟子(底层View)
- 当有敌人来犯(点击事件),先汇报给掌门,如果掌门决定亲自出马,直接解决(onInterceptTouchEvent()返回true,交给onTouchEvent()处理),掌门不想亲自出马,将任务交给武当小弟(onInterceptTouchEvent()返回false),事件由上而下传递返回值的规则如下:为true拦截,不传递;为false不拦截,继续传递
- 如果武当小弟把敌人解决了(onTouchEvent()返回true,直接消耗处理掉了),如果武当小弟打不过敌人(onTouchEvent()返回false),还是让掌门的来打敌人吧(传递给父类的onTouchEvent()来处理,父类消耗处理掉了,父类的onTouchEvent()返回true),事件由下而上传递返回值的规则如下:为true,处理,为false,不处理,向上传递
3.7 View的工作流程
measure、layout、draw:测量、确定位置、绘制
3.7.1 View的工作流程入口
1.DecorView被加载到Window中
- 当我们调用Activity的startActivity方法时,最终调用的是ActivityThread的handleLaunchActivity方法来创建Activity的
- 在handleLaunchActivity中调用performLaunchActivity方法来创建Activity,在这里会调用onCreate()方法,从而完成对DecorView的创建,接着调用handleResumeActivity()方法,此方法中的performResumeActivity方法中会调用Activity的onResume()方法,接着handleResumeActivity()方法中会得到DecorView、WindowManager,而在WIndowManagerImpl中则会调用addView()方法,此addView()方法又调用了WindowManagerGlobal的addView方法,在此addView()方法中创建了ViewRootImpl实例,该实例通过setView方法将DecorView作为参数传进去,这样就把DecorView加载到了Window中,但是界面还是什么都不会显示出来,因为View的工作流程还没走完,还需要经过measure、layout、draw才会把view绘制出来
2.ViewRootImpl的PerformTraveals方法
- 在此方法中执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法内部又会分别调用View的measure、layout、draw方法
3.7.2 理解MeasureSpec
意为:计量单位
在onMeasure中根据这个MeasureSpec来确定View的宽和高
从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,作为顶层View的DecorView,它的MeasureSpec由自身的LayoutParams来得到不同的MeasureSpec
3.7.3 View的measure流程
1.View的measure流程
- onMeasure()方法调用setMeasureDimension(),用来设置宽高
- 去了解getDefaultSize()方法、getSuggustedMinimumWidth()方法
2.ViewGroup的measure流程
。。。
3.7.4 View的layout流程
- ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置
- layout()方法中的4个参数l、t、r、b分别是view从左、上、右、下相对于父容器的距离
- setFrame()方法中传进来的4个参数可以确定该View在父容器中的位置,调用setFrame()方法后,就会调用onLayout()方法
3.7.5 View的draw流程
View的draw流程很简单,下面先来看看View的draw方法
官方注释清楚地说明了每一步的做法,它们分别是:
- 如果需要,则绘制背景。
- 保存当前canvas层。
- 绘制View的内容。
- 绘制子View。
- 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
- 绘制装饰,比如滚动条。
其中第2步和第5步可以跳过,所以这里不做分析,重点分析其他步骤。
步骤1:绘制背景
- 绘制背景调用了View的drawBackground方法
步骤3:绘制View的内容
- 调用了View的onDraw()方法,空实现,自定义View的时候自己去实现
步骤4:绘制子View
- 调用了dispatchDraw()方法,也是一个空实现,ViewGroup中此方法对子类进行了遍历,并调用drawChild方法
步骤6:绘制装饰
- 绘制装饰的方法为View的onDrawForeground()方法
3.8 自定义View
3.8.1 继承系统空间的自定义View
- 这种自定义控件一般是添加新的功能或者修改显示的效果,一般在draw()方法中进行处理
3.8.2 继承View的自定义View
- 不光要实现onDraw()方法,还得考虑wrap_content和padding等属性的设置,改变触控的逻辑,就要重写onTouchEvent()等方法
3.8.3 自定义组合控件
- 比如TopBar或者固定样式的Dialog
- 写xml布局文件,然后填充布局,设置必要的方法,比如setTitle()或者点击事件等,在attrs.xml中添加自定义属性
3.8.4 自定义ViewGroup
1.继承ViewGroup
- 2.对wrap_content属性进行处理,padding、margin
3.实现onLayout
- 如果子元素不是GONE,就将它放到合适的位置上
4.处理滑动冲突
- 这个自定义ViewGroup为水平滑动,如果里面有一个ListView会导致滑动冲突,如果检测到是水平滑动,就让父View进行拦截,具体实现就是重写onInterceptTouchEvent方法,判断如果是水平滑动就返回true来拦截
5.弹性滑动到其他页面
- 在刚进入onTouchEvent()方法中就得到点击事件的坐标,判断滑动距离是否大于1/2然后来进行对应的滑动
6.快速滑动到其他页面
- 类似,如果速度的绝对值大于50,就被认为快速滑动,去切换到下一个界面
7.再次触摸屏幕阻止页面
- 在onInterceptTouchEvent()方法中执行对应的操作
8.应用HorizontalView
- 。。。
3.9 本章小结
这章的内容挺多挺丰富的,是很重要的一章,好好读会有收获的