一、学习脑图
二、View基础
2.1 什么是View
?
Q1:怎么理解View
?
View
是界面层的控件的一种抽象,代表了一个控件。- 是
android
在视觉上的呈现。- 是所有控件是基类,可以是单个控件
View
可以是一组控件ViewGroup
。
Q2:View
的重要性?
View
在Android
中是一个十分重要的概念,虽然说View
不属于四大组件,但是它的作用堪比四大组件,在开发中,Activity
承担了可视化的功能,Android
提供了很多基础的控件,当我们不满足于这些基础控件的功能时,可以用自定义控件,而控件的自定义就需要对View
体系有深入的了解。
2.2 View
的位置参数
Android
系统中,有两种坐标系,分别是Android
坐标系和View
坐标系。
2.2.1 Android
坐标系
- 将屏幕左上角作为坐标原点
- 原点向右是X轴正方向
- 原点向下是Y轴正方向
注意:使用
getRawX()
和getRawY()
方法获得的坐标是Android坐标系的坐标
2.2.2 View
坐标系
Q1:View
的位置由什么来决定?
四个顶点:top
(左上角纵坐标)、left
(左上角横坐标)、right
(右下角横坐标)、bottom
(右下角纵坐标)
注意:这些坐标都是相对于父容器来说的,是一种相对坐标
Top = getTop()
,Left = getLeft()
,Right = getRight()
,Bottom=getBottom()
自Anroid3.0后,增加了
x
、y
、translationX
、translationY
这几个参数。
x
、y
:View
左上角的坐标translationX
、translationY
:左上角相对于父容器的偏移量
注意:View
在平移过程中,top
和left
表示原始左上角的位置信息,发生改变的值是x
、y
、translationX
、translationY
这四个参数。
Q2:getX()
、getY()
和getRawX()
、getRawY()
有什么区别?
getX
和getY
是视图坐标,是相对于控件的距离
getRawX
和getRawY
是绝对坐标,是与整个屏幕的距离
Q3:View
怎么获取自身的宽和高?
width
=getRight()
-getLeft()
=getWidth()
height
=getBottom()
-getTop()
=getHeight()
2.2.3 View
的触控
2.2.3.1 MotionEvent
手指接触屏幕后所产生的一系列事件。
ACTION_DOWN
—— 手指刚接触屏幕ACTION_MOVE
—— 手指在屏幕上移动ACTION_UP
—— 手指从屏幕上松开的一瞬间
正常情况下,触摸屏幕会出现以下两种情况
- 点击屏幕后松开,DOWN -> UP
- 点击屏幕滑动后再松开,DOWN->MOVE->...->MOVE->UP
2.2.3.2 TouchSlop
TouchSlop
是系统所能识别出的被认为是滑动的最小距离,是一个常量。
Q1:怎么获取这个常量?
ViewConfiguration.get(getContext()).getScaledTouchSlop()
。
Q2:这个常量有什么意义?
在处理滑动时,可以利用这个常量来进行过滤,当两次滑动事件的滑动距离小于这个常量时,可以认为它们不是滑动。
2.2.3.3 VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直速度。
Q:怎么使用VelocityTracker
?
1.在View
的onTouchEvent
方法中追踪当前点击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
2.获取当前速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
注意:
获取速度之前需要先计算速度,即
getXVelocity()
和getYVelocity()
方法前必须先调用velocityTracker.computeCurrentVelocity(1000)
;这里的速度指的事一段时间内手滑动的像素数。速度可以为正也可以为负,因为在
Android
坐标系中,手指逆着坐标正方向滑动,速度结果就是负的,这里的正负指的是方向。速度 = (终点位置 - 起点位置)/时间段
3.使用clear
重置并回收内存
当不需要使用velocityTracker
时,需要使用clear
去回收它。
velocityTracker.clear();
velocityTracker.recycle();
2.2.3.4 GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
Q:怎么使用GestureDetector
?
1.创建一个GestureDetector
对象并实现OnGestureDetector
接口
GestureDetector mGestureDetector = new GestureDetector(this,this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
2.在View
的onTouchEvent
方法添加
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
3.有选择的实现OnGestureListener
和OnDoubleTapListener
中的方法
建议:如果只是监听滑动操作,建议在
onTouchEvent
中实现;如果要监听双击这种行为,则使用GestureDetector
。
2.2.3.5 Scroller
弹性滑动对象,用于实现View的弹性滑动
Q:如何使用Scroller
?典型代码固定,如下。
Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX -scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate;
}
@Override
Public void computeScroll(){
if(mScroller.computeScrollOffset()){
ScrollTo(mScroll.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
三、View的滑动
滑动在
Android
开发中具有重要的作用,掌握滑动的方法是实现自定义控件的基础。
View
滑动的基本思想:当触摸事件传到
View
时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并算出偏移量,并通过偏移量来修改View
的坐标。
3.1 View滑动的7种方法
3.2.1 layout()
思路:
view
进行绘制的时候会调用onLayout()
方法来设置显示的位置,因此可以通过修改View
的left
、top
、right
、bottom
这四种属性来控制View
的坐标。
- 使用:
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); //layout()方法
break;
}
return true;
}
3.2.2 offsetLeftAndRight()
与offsetTopAndBottom()
思路:与
layout()
方法思路一样,不同的是offsetLeftAndRight()
与offsetTopAndBottom()
方法设置的是左右和上下的偏离值。
使用:
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;
//对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
break;
}
return true;
}
3.2.3 LayoutParams
(改变布局参数)
思路:
LayoutParams
主要保存了一个View
的布局参数,可以通过LayoutParams
来改变View
的布局的参数从而达到了改变View
的位置的效果。
- 使用:
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;
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
父控件若是
LinearLayout
则按代码上所示,父控件若是RelativeLayout
,则要使用RelativeLayout.LayoutParams
,除了使用布局的LayoutParams
外,也可以用ViewGroup.MarginLayoutParams
来实现
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
3.2.4 动画
思路:通过动画可以让一个
View
进行平移,而平移也就是一种滑动,主要操作的是View
的translationX
和translationY
属性。
Android
内有两种动画可以使用:View动画和属性动画。
-
View
动画使用:1.在res目录新建anim文件夹并创建translate.xml:
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
2.在Java代码中引用:
mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));
注意:
View
动画并不能改变View
的位置参数。如果对一个Button进行如上的平移动画操作,当Button平移300像素停留在当前位置时,我们点击这个Button并不会触发点击事件,但在我们点击这个Button的原始位置时却触发了点击事件。这就是补间动画和属性动画的区别
-
属性动画使用:
CustomView
在1000毫秒内沿着X轴像右平移300像素:
ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();
3.2.5 scrollTo
/scrollBy
思路:
scollTo(x,y)
表示移动到一个具体的坐标点,而scollBy(dx,dy)
则表示移动的增量为dx、dy。其中scollBy
最终也是要调用scollTo
的。scollTo
、scollBy
移动的是View的内容,如果在ViewGroup
中使用则是移动他所有的子View
。
- 使用:
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;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
}
return true;
}
3.2.6 Scroller
scollTo
/scollBy
方法来进行滑动时,这个过程是瞬间完成的,使用Scroller
来实现有过度效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔完成的。Scroller
本身是不能实现View的滑动的,它需要配合View
的computeScroll()
方法才能弹性滑动的效果。
- 使用:
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
//不断的重绘,重复调用computeScroll方法
PostInvalidate();
}
}
//缓慢滚动到指定位置
public void smoothScrollTo(int destX,int destY){
int scrollX=getScrollX();
int delta=destX-scrollX;
//1000秒内滑向destX
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
在View
类中调用
//使用Scroll来进行平滑移动
mCustomView.smoothScrollTo(-400,0);
-
源码分析
当我们构造一个Scroller对象并调用它的startScroll方法时,startScroll保存了传递的几个参数
/** * @param startX 起点的横坐标 * @param startY 起点的纵坐标 * @param dx 水平滑动的距离 * @param dy 竖直滑动的距离 * @param duration 滑动时间 */ public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }
Q1:在
startScroll
方法中,内部并没有做滑动相关的事,那么startScroll是如何让View滑动的?答:使用
invalidate
方法。invalidate
方法会导致View
重绘,重绘过程中View
的draw
方法中又会去调用computeScroll
方法,本来computeScroll
方法在View
中是一个空实现,在上面的代码中我们已经添加代码,通过scrollTo
方法实现滑动,接着使用PostInvalidat
方法第二次重绘,如此反复,知道整个滑动过程结束。在
computeScroll
方法中有使用到computeScrollOffset()
方法,下面看看这个方法的源码public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; .......
这个方法会根据时间的流逝计算当前的
scrollX
和scrollY
的值。返回ture时表示滑动未结束,返回false则表示滑动已结束。
3.2.7 延时策略
核心思想:通过发送一系列延时消息从而达到一种渐进性效果。
使用:
Handle
/View
的postDelayed
/线程的sleep
。注意:无法精确定时,因为系统消息调度也需要时间。
四、事件分发机制
事件分发机制是View体系里学习的核心点,它是解决滑动冲突的理论基础,因此,学习好事件分发机制是非常重要的。
这一部分主要是我对知识的总结概括,看了之后还是对事件分发机制感到模糊的读者,推荐一篇详细的事件分发文章学习 View 事件分发,就像外地人上了黑车!。
Q1:什么是点击事件的事件分发?
当一个点击事件MotionEvent
产生以后,系统把这个事件传递给具体的View
的过程,就是事件分发过程。
4.1 主要方法
-
dispatchTouchEvent
:进行事件的分发(传递)。返回值是boolean
类型,受当前onTouchEvent
和下级view的dispatchTouchEvent
影响 -
onInterceptTouchEvent
:对事件进行拦截。该方法只在ViewGroup
中有,View
(不包含ViewGroup
)是没有的。如果一旦拦截,则执行ViewGroup
的onTouchEvent
,在ViewGroup
中处理事件,而不接着分发给View,且只调用一次,所以后面的事件都会交给ViewGroup
处理。 -
onTouchEvent
:进行事件处理。
三者关系的伪代码:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false; //boolean类型的值表示是否消费事件
if(onInterceptTouchEvent(event)){ //当前View拦截了这个事件
consume = onTouchEvent(event); //执行当前View的onTouchEvent()方法,是否消费由该返回值决定
}else {//当前View没有拦截这个事件
consume = child.dispatchTouchEvent(event); //事件传递给下一层View的dispatchTouchEvent()方法,是否消费由下一层ViewdispatchTouchEvent()方法返回值决定
}
return consume;
}
4.2 事件分发的全流程
Q2: View事件分发的本质是什么?
View事件分发的本质是递归,点击事件自上而下分发的过程是“递”,消耗事件自下而上的过程是“归”。
Q3: “递”和“归”的两个过程分别是什么?
当一个点击事件产生后,它的传递过程会遵循如下顺序:Activity->Window->ViewGroup->...->View。这个自上而下传递的过程就是“递“的过程。
当传递到具体的一个View后,这个View的onTouchEvent
返回false,即不消耗这个事件,那么这个事件则会向上传递,假若所有的元素都不处理这事件,那么这个事件最终会传递给Activity
处理,这个自下而上传递的过程就是“归”的过程。
注意:
在“递”的过程中,
ViewGroup
可以在当前层级,通过设置onInterceptTouchEvent
方法返回 true,来拦截事件的下发,而直接步入“归”流程。在
ViewGroup
可以拦截事件下发的同时,child
也可以通过getParent.requestDisallowInterceptTouchEvent
方法,来阻止上一级的下发拦截。
总结:同样参考上面链接
4.3 源码分析
事件分发的三个过程:
Activity
对事件的分发- 顶级
View
对事件的分发View
对事件的处理这里不列出源码,仅画出流程图,需要查看源码的读者可查看《Android开发艺术探索》相应章节或者在编译器中查看。
五、滑动冲突
在使用滑动的过程中,假设一种情况,一个界面内外两层可以滑动,这个时候你滑动它,这个界面怎么判定你滑动的是内层还是外层?这个时候就会产生滑动冲突,所以在这个部分,我们一起来解决这个滑动冲突。
5.1 场景的滑动冲突场景
- 场景A:外部滑动与内部滑动不一致的滑动冲突,常见于常见于
ScrollView
和Fragment
中LisetView
的使用。 - 场景B:外部滑动与内部滑动一致的滑动冲突,可能出现在自定义
View
与ListView
中,外部可以上下滑动,内部也可以上下滑动。 - 场景C:场景AB的嵌套。
5.2 处理规则
-
场景A的处理规则:当用户左右滑动时,让外部的View拦截点击事件,当用户上下滑动时,让内部的View拦截点击事件。
Q1:如何判断用户是左右滑动还是上下滑动
利用水平偏移量和竖直方向的偏移量进行相减,用是否大于0来判断哪个偏移量大。若偏移量offsetX-offsetY>0,可判断为水平滑动,这时可以由外部拦截,让它来处理点击事件。
-
场景B的处理规则:需要根据业务逻辑来处理,,规定何时让外部View拦截事件何时由内部View拦截事件。
-
场景C的处理规则:同样需要从业务上找突破点
5.3 解决方法
外部拦截法
内部拦截法
5.3.1 外部拦截法
点击事件都先经过父容器的拦截处理,如果父容器需要就拦截,不需要此事件就不拦截。
方法:需要重写父容器的
onInterceptTouchEvent
方法,在内部做出相应的拦截。注意:父容器一旦开始拦截任何一个事件,那么后续的事件都会交给它来处理。
//重写父容器的拦截方法
public boolean onInterceptTouchEvent (MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View
intercepted = false;
break;
case MotionEvent.ACTION_MOVE://对于ACTION_MOVE事件根据需要决定是否拦截
if (父容器需要当前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP://对于ACTION_UP事件必须返回false,一旦拦截子View的onClick事件将不会触发
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
5.3.2 内部拦截法
父容器不拦截任何事件,所有事件都传递个给子元素,如果子元素需要此事件就直接消耗,否则交由父容器处理。
方法:需要配合
requestDisallowInterceptTouchEvent
方法。重写子View
的dispatchTouchEvent()
public boolean dispatchTouchEvent ( MotionEvent event ) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);//为true表示禁止父容器拦截
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);//为fasle表示允许父容器拦截
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
注意:除子容器需要做处理外,父容器也要默认拦截除了
ACTION_DOWN
以外的其他事件,这样当子容器调用parent.requestDisallowInterceptTouchEvent(false)
方法时,父元素才能继续拦截所需的事件。因此,
父View
需要重写onInterceptTouchEvent()
:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
Q1:为什么父容器不能拦截ACTION_DOWN事件?
由于该事件并不受
FLAG_DISALLOW_INTERCEPT
(由requestDisallowInterceptTouchEvent
方法设置)标记位控制,所以一旦父容器拦截了该事件,那么所有的事件都不会传递给子View,内部拦截法也就失效了。
参考自:
-
《Android开发艺术探索》
-
《Android进阶之光》