37、自定义控件详解(二)-- View和ViewGroup
一、View和ViewGroup绘制
1.1、View的位置参数
我们很容易就得出宽高和坐标的关系:
width = right - left
height = bottom - top
那么,如何获得View的位置参数:
- Left = getLeft();
- Right = getRight();
- Top = getTop();
- Bottom = getBottom();
在Android3.0时,View增加了额外的参数:x、y、translationX、translationY。
其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。换算关系:
x = left + translationX
y = top + translationY
View在平移过程中,top和left表示的是原始左上角的位置信息,并不会发生改变,此时发生改变的是x、y、tanslationX、translationY这四个参数。
1.2、View的绘制流程
View的工作流程主要是指measure、layout和draw三大流程,即测量、布局和绘制。
1.3、View的测量
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量View的宽高。
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。
SpecMode是指测量模式,而SpecSize是指某种测量模式下的规格大小。我们查看下MeasureSpec源码中部分代码:
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } ..... }
MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为方便操作,并提供了打包和解包的方法。
测量的模式可以分为以下三种:
- EXACTLY
即精确模式,当控件的layout_width或layout_height为具体数值时,系统使用的是EXACTLY模式。
- AT_MOST
最大值模式,当控件的layout_width或layout_height为wrap_content或match_parent时,控件的尺寸不要超过父控件允许的最大尺寸即可。
- UNSPECIFIED
未指定模式,它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。
系统最终会调用setMeasuredDimension(int measuredWidth,int measuredHeight)方法将测量后的宽高传递进去,以完成测量操作。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } /**测量宽度的模板代码*/ private int measureWidth(int measureSpec){ int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if(specMode == MeasureSpec.EXACTLY){ // 精确数值 result = specSize; }else{ // 非精确数值 result = 200; if(specMode == MeasureSpec.AT_MOST){// 自动包含 result = Math.min(result, specSize);// 取出指定大小与specSize中最小一个作为最后测量值。 } } return result; } /**测量高度的模板代码*/ private int measureHeight(int measureSpec){ int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if(specMode == MeasureSpec.EXACTLY){ result = specSize; }else{ result = 200; if(specMode == MeasureSpec.AT_MOST){ result = Math.min(result, specSize); } } return result; }
a) 布局文件中指定精确的宽高值是400px时,View会根据指定的宽高进行设定。
b) 当指定宽高属性为match_parent时,View会填充整个父布局。
c) 当指定宽高属性为wrap_content时,如果不重写onMeasure()方法则会填充整个父布局,重写的话则会根据内容自动包含。
1.4、View的绘制
1、装载画布
创建画布有两种方式:
Canvas canvas = new Canvas(); 或 Canvas canvas = new Canvas(bitmap);
当在创建画布传入bitmap对象时,bitmap和画布是紧紧相连的,这个过程我们称之为装载画布。
这个bitmap用来存储所有绘制在Canvas上的像素信息。且Canvas调用所有的Canvas.drawXXX方法都发生在该bitmap上。
装载画布时,当Canvas将绘制效果作用在bitmap时,刷新view就会改变bitmap,如果非装载画布模式下,改变的是bitmap对象,并让view重绘。
2、draw的源码
Android系统中要自定义view,首先需要了解Android的view加载机制。主要有三个方法:
1、onMeasure() //计算出view自身大小
2、onLayout() //仅在ViewGroup中,用来为子view指定位置(left,top)
3、onDraw() //view绘制内容
下面根据源码中的相关说明,进一步分析控件的绘制操作及顺序:
/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background (绘制控件设置的背景,系统已在view.draw()中绘制,只要在xml中指定背景即可) * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content (可以重写, onDraw(canvas);) * 4. Draw children (可重写,用来分发canvas到子控件,具体看ViewGroup。 * 对应方法dispatchDraw(canvas);此方法依次调用了子控件的draw()方法) * 5. If necessary, draw the fading edges and restore layers (绘制控件四周的阴影渐变效果) * 6. Draw decorations (scrollbars for instance) (用来绘制滚动条,对应方法onDrawScrollBars(canvas);。 * onDrawHorizontalScrollBar()和onDrawVerticalScrollBar()被隐藏了无法重写,也许有其他方法重写滚动条) */
1.5、ViewGroup的测量
1、MeasureSpec和LayoutParams
在测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec来确定View测量后的宽高。
顶级View(DecorView)和普通View的MeasureSpec是存在区别的:
- 对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。
- 对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。
2、三个测量方法
// measureChild(View, int, int)为子组件添加Padding measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec); // measureChildren(int, int)根据指定的高和宽来测量所有子View中显示参数非GONE的组件。 measureChildren(widthMeasureSpec, heightMeasureSpec); // measureChildWithMargins(View, int, int, int, int)测量指定的子组件,为子组件添加Padding和Margin。 measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
3、三种宽度和高度
- getMeasuredWidth(): 对View上的内容进行测量后得到的View内容占据的宽度。
- getWidth(): View在设定好布局后整个View的宽度,也就是在onLayout之后。
- getLayoutParams().width:测量后就确定值,猜测getLayoutParams.width在getMeasureWidth基础上多了margin和padding。
此处未完成###########待续
1.6、ViewGroup的绘制
ViewGroup为wrap_content时,测量是通过遍历所有子view,从而调子View的Measure方法获得每个子View的测量结果,
然后将子View放到合适的位置进行Layout过程。且Layout过程同样是遍历调用子View的Layout方法,指定其具体的位置来决定其布局位置。
通常情况下ViewGroup不需要进行绘制,因为其本身没有需要绘制的东西,如果不是指定背景色,那么ViewGroup的onDraw方法不会被调用。
但是,ViewGroup会通过dispatchDraw()方法来绘制其子View。
1、onDraw()和dispatchDraw的区别:
- 绘制View本身内容时,可以调用View.onDraw(Canvas canvas)方法。
- 绘制View的子View的内容时,可以调用diapatchDraw方法。
2、View中通常有如下重要的回调方法:
- onFinishInflate(): 从XML加载组件后回调。
- onSizeChanged(): 组件大小改变时回调。
- onMeasure(): 回调该方法进行测量。
- onLayout(): 回调该方法来确定显示的位置。
- onTouchEvent(): 监听到触摸事件时回调。
3、回调的顺序
View:
onFinishInflate -> onMeasure() -> onMeasure() -> onSizeChange() -> onLayout() -> onMeasure() -> onMeasure() -> onLayout() -> onDraw() 之后的操作会一直onDraw()去重绘
ViewGroup
二、手势、速度追踪、滑动
1.1、MotionEvent和TouchSlop
1. MotionEvent
在手指接触屏幕后会产生一系列事,典型的事件类型如下:
- ACTION_DOWN--------手指刚接触屏幕。
- ACTION_MOVE--------手指在屏幕上移动。
- ACTION_UP----------手指从屏幕上松开的一瞬间。
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:
点击屏幕后离开松开,事件序列为:DOWN --> UP
点击屏幕滑动一会再松开,事件序列为:DOWN --> MOVE -->...--> MOVE --> UP
上面是典型的事件序列,同时通过MotionEvent对象可以得到 x、y的坐标:
- getX和getY:返回的是相对于当前View的左上角的x和y坐标。
- getRawX和getRawY:返回的是相对于手机屏幕左上角的x和y坐标。
2. TouchSlop
TouchSlop是系统所能识别出被认为滑动的最小距离。
当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统会认为不是一个有效的滑动。
不同的设备,该常量的值也是不同的,可以通过如下方式获取这个常量:
ViewConfiguration.get(this).getScaledTouchSlop();
当然,在源码中我们可以找到该常量的定义,在framework.base/core/res/res/values/config.xml中。
1.2、VelocityTracker、GestureDetector和Scroller
1. VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度。
使用方式也非常简单,在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
当我们想知道滑动的速度,可以采用如下的方法:
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000); int xVelocity = (int) velocityTracker.getXVelocity(); int yVelocity = (int) velocityTracker.getYVelocity();
调用getXVelocity或getYVelocity获取速度前,必须调用computeCurrentVelocity方法(参数表示时间间隔)。
这里的速度是指一段时间内手指所滑过的像素数,这里时间间隔为1000毫秒,如果从左到右移动100像素,那么水平速度是100/s。
注意速度是可以为负数的,当手指从右向左滑动时,水平方向的速度即为负值,速度计算公式如下:
速度 = (终点位置 - 起点位置)/ 时间段
当不需要再使用该对象时,需要释放资源等操作
velocityTracker.clear();
velocityTracker.recycle();
2. GestureDetector
手势识别,用于辅助用户的单击、滑动、长按、双击等事件。
首先需要创建一个GestureDetector对象并实现onGestureListener接口,或者实现onDoubleTapListener来监听双击事件。
a)声明左右滑动的动画,一共有四个,必须放在res/anim目录下:
trans_next_in.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:fromXDelta="100%" android:toXDelta="0" android:fromYDelta="0" android:toYDelta="0" android:duration="300"> </translate>
trans_next_out.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:fromXDelta="0" android:toXDelta="-100%" android:fromYDelta="0" android:toYDelta="0" android:duration="300"> </translate>
trans_pre_in.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:fromXDelta="-100%" android:toXDelta="0" android:fromYDelta="0" android:toYDelta="0" android:duration="300"> </translate>
tran_pre_out.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:fromXDelta="0" android:toXDelta="100%" android:fromYDelta="0" android:toYDelta="0" android:duration="300"> </translate>
动画的播放需要用到如下方法:
//两个参数表示进入和退出的动画,要写在startactivity的后面生效 overridePendingTransition(R.anim.trans_next_in, R.anim.trans_next_out);
b) 在代码中加入如下代码来对手势进行判断
(1)声明手势识别器
(2)初始化手势识别器
(3)使用手势识别器去识别动作
//该方法最好做成成员变量可以响应下方的触摸动作 GestureDetector gestureDetector = new GestureDetector(this,new GestureDetector.SimpleOnGestureListener(){ //手指在屏幕上滑动 //e1,e2:手指的事件:手指第一次触摸屏幕触发->手指离开屏幕触发 //vX vY:水平和垂直方向的速度 @Override public boolean onFling(MotionEvent e1, MotionEvent e2,float velocityX, float velocityY) { //过滤掉Y的移动 if(Math.abs(e1.getRawY()-e2.getRawY())>100){ //ToastUtils.show(SetupBaseActivity.this,"动作不合法"); return true; } if(e1.getRawX()-e2.getRawX()>150){ //从右向左滑,显示下一个界面 //showNext(); overridePendingTransition(R.anim.trans_next_in, R.anim.trans_next_out); return true; }else if (e2.getRawX()-e1.getRawX()>150) { //从左向右滑,显示上一个界面 //showPre(); overridePendingTransition(R.anim.trans_pre_in, R.anim.trans_pre_out); return true; } return super.onFling(e1, e2, velocityX, velocityY); } });
c) 使用手势识别器去识别动作,重写Activity触屏的方法onTouchEvent
//3.使用手势识别器去识别用户的动作 @Override public boolean onTouchEvent(MotionEvent event) { //将手势事件传递给手势识别器 gestureDetector.onTouchEvent(event); return super.onTouchEvent(event); }
3. onTouchEvent
除了使用GestureDetector来判断手势事件之外,我们还可以使用onTouchEvent来判断手势事件
@Override public boolean onTouchEvent(MotionEvent event) { float downX = 0, downY = 0, upX, upY; //继承了Activity的onTouchEvent方法,直接监听点击事件 if(event.getAction() == MotionEvent.ACTION_DOWN) { //当手指按下的时候 downX = event.getX(); downY = event.getY(); } if(event.getAction() == MotionEvent.ACTION_UP) { //当手指离开的时候 upX = event.getX(); upY = event.getY(); if((downY - upY > 50) && (Math.abs(upX - downX) <= ( downY - upY))) { Toast.makeText(TaskToMoneyActivity.this, "向上滑", Toast.LENGTH_SHORT).show(); } else if((upY - downY > 50) && (Math.abs(upX - downX) <= (upY - downY))) { Toast.makeText(TaskToMoneyActivity.this, "向下滑", Toast.LENGTH_SHORT).show(); } else if((downX - upX>50) && (Math.abs(upY - downY) < (downX - upX))) { Toast.makeText(TaskToMoneyActivity.this, "向左滑", Toast.LENGTH_SHORT).show(); } else if((upX - downX>50) && (Math.abs(upY - downY) < (upX - downX))) { Toast.makeText(TaskToMoneyActivity.this, "向右滑", Toast.LENGTH_SHORT).show(); } } return super.onTouchEvent(event); }
4. Scroller
弹性滑动对象,用于实现View的弹性滑动。
当我们使用scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成,此时就可以考虑使用Scoller来实现过渡滑动的效果。
它需要和computeScroll方法配合使用才能完成共同的功能。
Scroller mScroller = new Scroller(getContext()); // 缓慢滚动到指定位置 private void smoothScrollTo(int destX , int destY){ int scrollX = getScrollX(); int scrollY = getScrollY(); int delta = destX - scrollX; // 1000ms内滑向destX,缓慢移动 mScroller.startScroll(scrollX, 0, delta, 0, 1000); invalidate(); } @Override public void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
##############################################################################