进阶之路 | 巧妙的View之旅
Go1.14发布了,快来围观新的特性啦
媒介
本文已收录到我的Github个人博客,迎接大佬们莅临舍下:
进修清单:
- View是什么
- 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坐标系是如何的呢?
以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向
Q2:View的位置怎样肯定?
- 由四个极点肯定,离别对应四个属性:top、left、right、bottom。
- left是左上角的横坐标,left =
getLeft()
- right是右下角的横坐标,right =
getRight()
- top是左上角的纵坐标,top =
getTop()
- bottom是右下角的纵坐标,bottom=
getBottom()
注重:这些坐标是相关于父容器而言的,属于相对坐标;假如想要获得相对坐标,须要挪用
getRawX()
,相对坐标的学问在下文将会细致解说。
因而,View的宽高和坐标关联:
- width = right - left,可直接经由历程
getWidth()
获得- height = bottom - top,可直接经由历程
getHeight()
获得
Q3:View偏移量translation
translationX
和translationY
是View 左上角相对父容器左上角的偏移量,它们默许值是0。这些参数也是相关于View父容器。
- 存在关联:x = left + translationX,y = top + translationY
- 因而可知,x和left差别体如今:
- left是View的初始坐标,在绘制终了后就不会再转变;
- 而x是View偏移后的及时坐标,是现实坐标。y和top的区分同理。
须要注重的是,在onCreate()
要领里没法猎取到View的坐标参数,这是因为此时View还未入手下手绘制,悉数坐标参数将都是0。
2.2 View
的触控
2.2.1 MotionEvent
它是手指触摸屏幕所发生的一系列事宜。典范事宜有:
- ACTION_DOWN:手指刚打仗屏幕
- ACTION_MOVE:手指在屏幕上滑动
- ACTION_UP:手指在屏幕上松开的一霎时
事宜列:从手指打仗屏幕至手指脱离屏幕,这个历程发生的一系列事宜,任何事宜列都是以DOWN事宜入手下手,UP事宜完毕,中心有无数的MOVE事宜
- 经由历程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内手指在程度方向从上向下滑过的像素数,垂直速率
注重速率方向,这个速率方向和下面的mScrollX的方向相反
- 当不须要运用它的时刻,须要挪用
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中的要领
发起:假如只是监听滑动操纵,发起在
onTouchEvent
中完成;假如要监听双击这类行动,则运用GestureDetector
。
2.3 View
的滑动
2.3.1 View
滑动的七种体式格局
1. scrollTo
/scollBy
- 区分:
scrollBy
是内部挪用了scrollTo
的,它是基于当前位置的相对滑动;而scrollTo
是相对滑动,因而假如应用雷同输入参数屡次挪用scrollTo()
要领,因为View初始位置是稳定只会涌现一次View转动的结果而不是屡次。- 注重:二者都只能对view内容举行滑动,而不能使view本身滑动。
- 方向:手指从右向左滑动,mScrollX为正值,反之为负值;手指从下往上滑动,mScrollY为正值,反之为负值。(更直观感觉:检察下一张照片或许检察长图时手指滑动方向为正)
- 滑动范例:非弹性滑动
2. LayoutParams
- 道理:经由历程转变View的LayoutParams使得View从新规划:比方将一个View向右挪动100像素,向右,只须要把它的marginLeft参数增大即可
- 滑动范例:非弹性滑动
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求从新对View举行measure、layout
3. 动画
- 动画分为View动画和属性动画,View动画又分为帧动画和补间动画
- 假如运用属性动画的话,为了可以兼容3.0以下版本,须要采纳开源动画库nineoldandroids。
- 属于弹性滑动
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms内使得View从原始位置向右平移100像素
想要相识动画细致内容的读者,可以看一下笔者这篇文章:
4. layout()
- 基本思想:记下触摸点的坐标挪动今后,记下挪动后的坐标算出偏移量
- 运用体式格局:在
onTouchEvent
中猎取到手指的横纵坐标,在ACTION_DOWN
中存储上次的x,在ACTION_MOVE
中盘算挪动的距离,末了挪用layout
要领从新安排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
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()
运用体式格局相似于
layout()
,将layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX , getBottom()+offsetY)
换成offsetLeftAndRight(offsetX)
与offsetTopAndBottom(offsetY)
即可
// 对left和right举行偏移
offsetLeftAndRight(offsetX);
//对top和bottom举行偏移
offsetTopAndBottom(offsetY);
6. Scroller
- 与scrollTo/scrollBy差别:scrollTo/scrollBy历程是霎时完成的,非腻滑;而Scroller则有过渡滑动的结果
- 注重:Scoller本身没法让View弹性滑动,它须要和View的
computeScroll
要领合营运用。- 道理:Scoller的
computeScrollOffset()
依据时候的流逝动态盘算一小段时候里View滑动的距离,并获得当前View位置,再经由历程scrollTo继承滑动。即把一次滑动拆分红无数次小距离滑动从而完成弹性滑动。
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()
的源码:
只是举行前期的准备事情,并没有举行现实的滑动操纵,而是经由历程后续
invalidate()
要领去做滑动行动。
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. 延时战略
- 经由历程发送一系列延时信息从而到达一种渐近式的结果,详细可以经由历程
Handler
/View
的postDelayed
,也可运用线程的sleep
要领。- 瑕玷:没法准确地定时;缘由:系统的音讯调理也须要时候
2.3.2 滑动争执
Q1:发生缘由:
平常状况下,在一个界面里存在表里两层可同时滑动的状况时,会涌现滑动争执征象。
Q2:涌现的场景:
- 场景一:外部滑动和内部滑动方向不一致:如ViewPager嵌套ListView(现实这么用没问题,因为ViewPager内部已处置惩罚过)。
- 场景二:外部滑动方向和内部滑动方向一致:如ScrollView嵌套ListView。
读者假如想要相识涌现缘由以及处置惩罚体式格局,笔者引荐一篇文章:
- 场景三:上面两种状况的嵌套
Q3:处置惩罚划定规矩:
- 对场景一:当用户摆布/高低滑动时让外部View阻拦点击事宜,当用户高低/摆布滑动时让内部View阻拦点击事宜。即依据滑动的方向推断谁来阻拦事宜。关于推断是高低滑动照样摆布滑动,可依据滑动的距离或许滑动的角度去推断。
- 对场景二:平常从营业上找突破点。即依据营业需求,划定什么时候让外部View阻拦事宜什么时候由内部View阻拦事宜。
- 对场景三:相对庞杂,可一样依据需求在营业上找到突破点。
Q4:处置惩罚体式格局:
这里的
onInterceptTouchEvent
,dispatchTouchEvent
,requestDisallowInterceptTouchEvent
等要领在View的事宜分发机制会细致申明
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); }
除子容器须要做处置惩罚外,父容器也要默许阻拦除了
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; } }
内部阻拦法请求父容器不能阻拦
ACTION_DOWN
的缘由:因为该事宜并不受
FLAG_DISALLOW_INTERCEPT
(由requestDisallowInterceptTouchEvent
要领设置)标记位掌握,一旦ACTION_DOWN
事宜到来,该标记位会被重置。所以一旦父容器阻拦了该事宜,那末一切的事宜都不会通报给子View,内部阻拦法也就失效了。
2.4 View的事宜分发机制
读者看完本篇对事宜分发机制还有些隐约的话,笔者墙裂引荐一篇浅显易懂的文章:
Q1:相识setContentView()
我们将从源码的角度,一步步带人人深切
setContentView()
的实质,为背面事宜分发机制的相识打好基本
因而,我们可以获得Activity的构成,以下图所示
Q2:事宜分发实质是什么:
就是对
MotionEvent
事宜分发的历程。即当一个MotionEvent
发生了今后,系统须要将这个点击事宜通报到一个详细的View
上。(关于MotionEvent引见见本篇2.2.1)
Q3:事宜分发须要的重要要领是什么:
dispatchTouchEvent
:举行事宜的分发(通报)。返回值是boolean
范例,受当前onTouchEvent
和下级view的dispatchTouchEvent
影响onInterceptTouchEvent
:对事宜举行阻拦。该要领只在ViewGroup
中有,View
(不包括ViewGroup
)是没有的。假如一旦阻拦,则实行ViewGroup
的onTouchEvent
,在ViewGroup
中处置惩罚事宜,而不接着分发给View,且只挪用一次,所以背面的事宜都邑交给ViewGroup
处置惩罚。onTouchEvent
:举行事宜处置惩罚
- 事宜分发是逐级下发的,目标是将事宜通报给一个View。
- ViewGroup一旦阻拦事宜,就不往下分发,同时挪用onTouchEvent处置惩罚事宜。
2.5 View的事情道理
2.5.1 View事情流程
measure
丈量->layout
规划->draw
绘制
measure
肯定View的丈量宽高layout
肯定View的终究宽高和四个极点的位置draw
将View 绘制到屏幕上- 对应
onMeasure()
、onLayout()
、onDraw()
三个要领。
详细历程:
ViewRoot
对应于ViewRootImpl
类,它是衔接WindowManager
和DecorView
的纽带- View的绘制流程是从
ViewRoot.performTraversals
入手下手。performTraversals()
顺次挪用performMeasure()
、performLayout()
和performDraw()
三个要领,完成顶级 View的绘制。- 个中,
performMeasure()
会挪用measure()
,measure()
中又挪用onMeasure()
,完成对其一切子元素的measure历程,如许就完成了一次measure历程;接着子元素会重复父容器的measure历程,云云重复至完成全部View树的遍历。layout和draw同理。历程图以下:
2.5.2 measure
先来明白
MeasureSpec
:
- 作用:经由历程宽丈量值
widthMeasureSpec
和高丈量值heightMeasureSpec
决议View的大小- 构成:一个32位int值,高2位代表SpecMode(丈量形式),低30位代表SpecSize( 某种丈量形式下的规格大小)。
- 三种形式:
a.
UNSPECIFIED
: 父容器不对View有任何限定,要多大有多大。常用于系统内部。b.
EXACTLY
(准确形式): 父视图为子视图指定一个确实的尺寸SpecSize
。对应LayoutParams
中的match_parent
或详细数值。c.
AT_MOST
(最大形式): 父容器为子视图指定一个最大尺寸SpecSize
,View的大小不能大于这个值。对应LayoutParams
中的wrap_content
。- 决议因素:由
子View
的规划参数LayoutParams
和父容器的MeasureSpec
值配合决议。
如今,离别议论两种measure
:
- View的measure:只要一个原始的View,经由历程measure()即可完成丈量。
从
getDefaultSize()
中可以看出,直接继承View的自定义View须要重写onMeasure()
并设置wrap_content
时的本身大小,不然结果相当于macth_parent
。处置惩罚上述问题的典范代码:
要领一:
@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);
}
}
要领二:
- 道理实在和要领一相似,就是
resolveSize
封装了要领一的一系列操纵- 想探讨
resolveSize
源码的可以看一下这篇文章:
@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
要领。
ViewGroup中没有重写
onMeasure()
,而是供应measureChildren()
。
假如读者对onMeasure的细致重写例子感兴趣的话,笔者引荐一篇文章:
2.5.3 layout
- 肯定View的终究宽高和四个极点的位置
大抵流程:从顶级View入手下手顺次挪用
layout()
,个中子View的layout()
会挪用setFrame()
来设定本身的四个极点(mLeft、mRight、mTop、mBottom),接着挪用onLayout()
来肯定其坐标,注重该要领是空要领,因为差别的ViewGroup对其子View的规划是不雷同的。
假如读者对
onLayout()
的细致重写例子感兴趣的话,笔者引荐一篇文章:
2.5.4 draw
引荐浏览:
- 绘制到屏幕
绘制次序:
- 绘制背景:
background.draw(canvas)
- 绘制本身:
onDraw(canvas)
- 绘制children:
dispatchDraw(canvas)
- 绘制装潢:
onDrawScrollBars(canvas)
注重:View有一个迥殊的要领
setWillNotDraw()
,该要领用于设置WILL_NOT_DRAW
标记位(其作用是当一个View不须要绘制内容时,系统可举行响应优化)。默许状况下View是没有这个优化标志的(设为true)。
2.6 自定义View
假如想相识自定义View实例的读者,笔者引荐一篇文章:
Q1:自定义View的范例有哪些?
迥殊提示:
三.教室小测试
祝贺你!已看完了前面的文章,相信你对
View
已有肯定深度的相识,下面,举行一下教室小测试,考证一下本身的进修效果吧!
Q1:View
的丈量宽高和终究宽高有什么区分?
这个问题详细为
View
的getMeasuredWidth
和getWidth
有什么区分?
- 答案发表:
在
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
才肯定本身的丈量宽高,在前频频的丈量历程当中,得出的丈量宽高有大概和终究宽高不一致,但终究二者照样一致的。
假如文章对您有一点协助的话,愿望您能点一下赞,您的点赞,是我行进的动力
本文参考链接:
- 《Android 进阶之光》
- 《Android 开发艺术探究》
腾讯抢金达人项目中的前后端协作