前言
学习清单:
- View是什么
- View的位置参数
- View的触控
- View的滑动
- View的动画
- View的事件分发机制
- View的工作原理
- View的自定义方式
一.为什么要学习View?
View
,是Android
中十分重要的一个知识点,是所有控件的基类,尽管View
不属于四大组件,但是它的作用堪比四大组件,甚至重要性大于ContentProvider
和Broadcast Receivers
。
ViewGroup
是View
的继承,它的内部包含了一组View。
很多时候,面对产品经理的各种奇葩的需求,仅仅使用系统提供的控件是不能满足需求的,因此,我们就需要自定义特定的控件,而自定义控件就需要对View
体系有一定程度的理解;有时候,涉及到滑动事件的自定义View的时候,难免会出现各种各样的滑动冲突,而要解决滑动冲突的话,还需要对View
的事件分发机制了然于心。
综上,掌握好View
这方面的知识,不仅可以让你在日常开发中对自定义View的各种场景胸有成竹,还可以让你在面试官的重重追问(ai hu)下游刃有余(xin tai bao zha)。
二.核心知识点归纳
2.1 View
的位置参数
Q1:Android坐标系是怎样的呢?
Q2:View的位置怎么确定?
- 由四个顶点确定,分别对应四个属性:top、left、right、bottom。
Q3:View偏移量translation
需要注意的是,在onCreate()
方法里无法获取到View的坐标参数,这是因为此时View还未开始绘制,全部坐标参数将都是0。
2.2 View
的触控
2.2.1 MotionEvent
它是手指触摸屏幕所产生的一系列事件。典型事件有:
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上滑动
- ACTION_UP:手指在屏幕上松开的一瞬间
通过MotionEvent 对象可以得到触摸事件的x、y坐标。其中通过
getX()
、getY()
可获取相对于当前view(注意:不是父容器)左上角的x、y坐标(相对坐标);通过
getRawX()
、getRawY()
可获取相对于手机屏幕左上角的x,y坐标(绝对坐标)。具体关系见下图:
2.2.2 TouchSlop
- 系统所能识别的被认为是滑动的最小距离。即当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
- 该常量和设备有关,可用它来判断用户的滑动是否达到阈值
- 获取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
2.2.3 VelocityTracker
使用过程:
在view的
onTouchEvent
方法中追踪当前单击事件的速度:VelocityTracker velocityTracker = VelocityTracker.obtain();//实例化一个VelocityTracker 对象
velocityTracker.addMovement(event);//添加追踪事件
在
ACTION_UP
事件中获取当前的速度velocityTracker .computeCurrentVelocity(1000);//获取速度前先计算速度,这里计算的是在1000ms内
float xVelocity = velocityTracker .getXVelocity();//得到的是1000ms内手指在水平方向从左向右滑过的像素数,即水平速度
float yVelocity = velocityTracker .getYVelocity();//得到的是1000ms内手指在水平方向从上向下滑过的像素数,垂直速度
当不需要使用它的时候,需要调用
clear
方法来重置并回收内存velocityTracker.clear();
velocityTracker.recycle();
2.2.4 GestureDetector
使用过程:
创建一个GestureDetecor对象并实现OnGestureListener接口,根据需要实现单击等方法
GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象
mGestureDetector.setIsLongpressEnabled(false);// 解决长按屏幕后无法拖动的现象
接管目标view的onTouchEvent方法,在待监听view的onTouchEvent方法中添加如下实现
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
有选择的实现OnGestureListener和OnDoubleTapListener中的方法
2.3 View
的滑动
2.3.1 View
滑动的七种方式
1. scrollTo
/scollBy
2. LayoutParams
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求重新对View进行measure、layout
3. 动画
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms内使得View从原始位置向右平移100像素
4. layout()
public boolean onTouchEvent(MotionEvent event) {
//获取到手指处的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//lastX是存储上一次的x
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;
return true;
}
5. offsetLeftAndRight()
与offsetTopAndBottom()
// 对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
6. Scroller
Scroller
惯用代码:
Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象
private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离
int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离
int deltaX = dstX - scrollX;//x方向滑动的位移量
int deltaY = dstY - scrollY;//y方向滑动的位移量
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
invalidate(); //刷新界面
}
//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();//通过不断的重绘不断的调用computeScroll方法
}
}
startScroll()
的源码:
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;//滑动时间
mStartTime = AnimationUtils.currentAminationTimeMills();//开始时间
mStartX = startX;//滑动起点
mStartY = startY;//滑动起点
mFinalX = startX + dx;//滑动终点
mFinalY = startY + dy;//滑动终点
mDeltaX = dx;//滑动距离
mDeltaY = dy;//滑动距离
mDurationReciprocal = 1.0f / (float)mDuration;
}
7. 延时策略
2.3.2 滑动冲突
Q1:产生原因:
Q2:出现的场景:
- 场景一:外部滑动和内部滑动方向不一致:如ViewPager嵌套ListView(实际这么用没问题,因为ViewPager内部已处理过)。
- 场景二:外部滑动方向和内部滑动方向一致:如ScrollView嵌套ListView。
- 场景三:上面两种情况的嵌套
Q3:处理规则:
- 对场景一:当用户左右/上下滑动时让外部View拦截点击事件,当用户上下/左右滑动时让内部View拦截点击事件。即根据滑动的方向判断谁来拦截事件。关于判断是上下滑动还是左右滑动,可根据滑动的距离或者滑动的角度去判断。
- 对场景二:一般从业务上找突破点。即根据业务需求,规定何时让外部View拦截事件何时由内部View拦截事件。
- 对场景三:相对复杂,可同样根据需求在业务上找到突破点。
Q4:解决方式:
A1:外部拦截法
- 含义:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。
- 方法:需要重写父容器的
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;
}
A2:内部拦截法
含义:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。
方法:需要配合
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);
}
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
2.4 View的事件分发机制
Q1:了解setContentView()
Q2:事件分发本质是什么:
Q3:事件分发需要的主要方法是什么:
dispatchTouchEvent
:进行事件的分发(传递)。返回值是boolean
类型,受当前onTouchEvent
和下级view的dispatchTouchEvent
影响onInterceptTouchEvent
:对事件进行拦截。该方法只在ViewGroup
中有,View
(不包含ViewGroup
)是没有的。如果一旦拦截,则执行ViewGroup
的onTouchEvent
,在ViewGroup
中处理事件,而不接着分发给View,且只调用一次,所以后面的事件都会交给ViewGroup
处理。onTouchEvent
:进行事件处理
2.5 View的工作原理
2.5.1 View工作流程
measure
确定View的测量宽高layout
确定View的最终宽高和四个顶点的位置draw
将View 绘制到屏幕上- 对应
onMeasure()
、onLayout()
、onDraw()
三个方法。
2.5.2 measure
现在,分别讨论两种measure
:
View的measure:只有一个原始的View,通过measure()即可完成测量。
方法一:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//分析模式,根据不同的模式来设置
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
方法二:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int width=resolveSize(mWidth, widthMeasureSpec);
int height=resolveSize(mHeight, heightMeasureSpec);
setMeasuredDimension(width,height);
}
- ViewGroup的
measure
:除了完成ViewGroup自身的测量外,还会遍历去调用所有子元素的measure
方法。
2.5.3 layout
- 确定View的最终宽高和四个顶点的位置
2.5.4 draw
- 绘制到屏幕
2.6 自定义View
Q1:自定义View的类型有哪些?
特别提醒:
三.课堂小测试
Q1:View
的测量宽高和最终宽高有什么区别?
答案揭晓:
在
View
的默认实现中,测量宽高和最终宽高相等,但是测量宽高的赋值时机比最终宽高的赋值时机稍微早一点,测量宽高形成于measure
过程,最终宽高形成于View的layout
过程。
Q2:什么情况下测量宽高和最终宽高不一致呢?
重写了View的
layout
方法public void layout(int l,int t,int r, int b){
super.layout(l,t,r+100,b+100);
}
在某些情况下,
View
需要多次measure
才能确定自己的测量宽高,在前几次的测量过程中,得出的测量宽高有可能和最终宽高不一致,但最终两者还是一致的。
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接: