View基础(25题)
- 什么是View
- View的位置参数
- MotionEvent
- ViewRoot
- DecorView
- MeasureSpec
View三大流程(28题)
- measure过程
- View
- ViewGroup
- layout过程
- draw过程
- 获取View的宽高
- Activity启动到加载ViewRoot的流程
自定义View(26题)
- 四种实现方法
- 直接继承View
- 自定义属性
- 直接继承ViewGroup
- 性能优化
- 硬件加速
事件分发机制(22题)
- 三个重要方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
- 事件传递规则与要点
- 事件传递规则
- Activity的事件分发
- Window的事件分发
- DecorView的事件分发
- 根View的事件分发
- ViewGroup的事件分发
- View的事件分发和事件处理
滑动冲突(8题)
- 滑动冲突的三种场景
- 滑动冲突处理原则和解决办法
- 外部拦截
- 内部拦截
滑动(39题)
- 滑动的7种实现方法
- 弹性滑动
- Scroller
- 动画
- 延时策略
- 侧滑菜单
- DraweLayout
- SlidingPanelLayout
- NavigationView
- ViewDragHelper
- ViewDragHelper.Callback
- GestureDetector
- OnGestureListener
- OnDoubleTapListener
- OnContextClickListener
- SimpleOnGestureListener
辅助类
- ViewConfiguration
- VelocityTracker
如需完整版 Android面试锦集 请点击免费领取
部分答案展示:
View基础(25题)
1、简述View的绘制流程
onMeasure-测量
:从顶层View到子View递归调用measure()
方法,measure()内部调用onMeasure()
, 在onMeasure()
中完成测量工作
onLayout-布局
:从顶层View到子View递归调用layout()
方法,layout调用onLayout()
,会根据测量返回的视图大小
和布局参数将View
放置到合适位置。onDraw-绘制
: ViewRoot会创建Canvas,然后执行onDraw()
进行绘制。
2、onDraw()的绘制顺序
绘制背景
绘制View内容
绘制子View
绘制滚动条
3、requestLayout()的作用
- 请求重新测量、布局
View
(requestLayout)->ViewGroup
(requestLayout)->DecorView
(requestLayout)->ViewRootImpl
(requestLayout)。- 最终会触发
ViewRootImpl
的performTraversals()
, 会触发onMeasure()
和onLayout()
,不一定会触发onDraw()
4、requestLayout在什么情况下只会触发测量和布局
,而不会触发绘制
?
如果没有改变控件的left\right\top\bottom就不会触发
onDraw()
5、invalidate()的作用
- 请求重新绘制
- 会递归调用
父View的invalidateChildInParent
->ViewRootImpl
的invalidateChildInparent()
- 最终会执行
ViewRootImpl
的performTraversals()
, 不会会触发onMeasure()
和onLayout()
,会触发onDraw()
也可能不触发onDraw()
View三大流程(28题)
1、ViewRoot如何完成View的三大流程?
- ViewRoot的
performTraversals()
开始View的绘制流程,依次调用performMeasure()
、performLayout()
和performDraw()
- performMeasure()最终执行父容器的measure()方法,并依此执行所有子View的measure方法。
- performLayout()和performDraw()同理
2、View三大流程的作用?(3)
- measure决定了View的宽/高,测量后可以通过
getMeasuredWidth/Height
来获得View测量后的宽/高,除特殊情况外该值等于View最终的宽/高- layout决定了View的顶点坐标以及实际View的宽/高:完成后可以通过
getTop/Bottom/Left/Right
获取顶点坐标,并通过getWidth/Height()
获得View的最终宽/高- draw决定了View的显示,最终将View显示出来
3、什么时候测量宽高不等于实际宽高?
MeasuredWidth/height != getWidth/Height()
的场景:更改View的布局参数并进行重新布局后,就会导致测量宽高 != 实际宽高
measure过程
View
4、View的measure方法的特点?
- View的measure方法是final类型方法——表明该方法无法被重载
- View的measure方法会调用onMeasure方法,onMeasure会调用setMeasuredDimension方法设置View宽/高的测量值
5、View的onMeasure源码要点
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1. setMeasuredDimension方法设置View宽/高的测量值
setMeasuredDimension(
//2. 第一个参数是获得的测量宽/高(通过getDefaultSize获取)
getDefaultSize(getSuggestedMinimumWidth(), //3. 获取的建议最小的宽/高
widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(),
heightMeasureSpec));
}
- setMeasuredDimension方法设置View宽/高的测量值(测量值通过getDefaultSize获取)
- getDefaultSize用于获取View的测量宽/高
自定义View(26题)
四种实现方法
1、自定义View实现方法的分类?
分类 | 注意点1 | 注意点2 | 注意点3 |
---|---|---|---|
1.继承View | 重写onDraw()—绘制和支持padding | 重写onMeasure()—解决wrap_content问题 | |
2.继承ViewGroup | 重写onMesaure()—测量子元素,测量自身,并且需要处理子View的margin和自身的padding | 必须实现onLayout()—布局子元素,并且处理子View的margin和自身的padding属性 | 实现自身的LayoutParams并且重写LayoutParmas相关的3个方法—让子View的Margin属性生效 |
3.继承特定的View(TextView等) | 扩展较容易实现 | 不需要额外支持wrap_content 和padding | |
4.继承特定的ViewGroup(LinearLayout等) | 方法2能实现的效果方法4都能实现 |
2、自定义View的注意点?(5)
- View需要支持wrap_content、padding
- ViewGroup需要支持子View的margin和自身的padding
- 尽量不要在View中使用Handler,View已经有post系列方法
- View如果有线程或者动画,需要及时停止(onDetachedFromWindow会在View被remove时调用)——避免内存泄露
- View如果有滑动嵌套情形,需要处理好滑动冲突
直接继承View
3、直接继承自View的实现步骤和方法:
- 重写onDraw,在onDraw中处理
padding
- 重写onMeasure,额外处理
wrap_content
的情况- 设定自定义属性attrs(属性相关xml文件,以及在onDraw中进行处理)
class CustomViewByView(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int):
View(context, attrs, defStyleAttr, defStyleRes){
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int):this(context, attrs, defStyleAttr, 0)
constructor(context: Context, attrs: AttributeSet):this(context, attrs, 0, 0)
constructor(context: Context): this(context, null, 0, 0)
var mColor = Color.RED
init {
//3. 自定义attrs中属性的获取
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewByView)
mColor = typedArray.getColor(R.styleable.CustomViewByView_circle_color, Color.RED)
typedArray.recycle()
}
//1. 重写onDraw方法
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = mColor //属性attrs给定的颜色
//2. 需要处理padding
val width = width - paddingLeft - paddingRight
val height = height - paddingTop - paddingBottom
canvas.drawCircle(paddingLeft + width.toFloat() / 2, paddingTop + height.toFloat() / 2,
Math.min(width, height).toFloat() / 2, paint)
}
//3. 特别处理wrap_content的情况,给定一个最小值
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
when{
// 为wrap_content的边均使用最小值mMinWidth/mMinHeight
widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST -> {
setMeasuredDimension(minimumWidth, minimumHeight)
}
widthSpecMode == MeasureSpec.AT_MOST -> {
setMeasuredDimension(minimumWidth, heightSpecSize)
}
heightSpecMode == MeasureSpec.AT_MOST -> {
setMeasuredDimension(widthSpecSize, minimumHeight)
}
}
}
}
事件分发机制(22题)
1、事件分发
- 点击事件的对象就是MotionEvent,因此事件的分发,就是MotionEvent的分发过程,
- 点击事件有三个重要方法来完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent
三个重要方法
2、简述Android的事件分发机制
事件分发顺序:
Activty->ViewGroup->View
主要方法:dispatchTouchEvent-分发事件
、onInterceptTouchEvent-当前View是否拦截该事件
、onTouchEvent-处理事件
1. 父View调用dispatchTouchEvent
开启事件分发。
2. 父View调用onInterceptTouchEvent
判断是否拦截该事件,一旦拦截后该事件的后续事件(如DOWN之后的MOVE和UP)都直接拦截,不会再进行判断。
3. 如果父View进行拦截,父View调用onTouchEvent
进行处理。
4. 如果父View不进行拦截,会调用子View
的dispatchTouchEvent
进行事件的层层分发。
dispatchTouchEvent
3、dispatchTouchEvent的作用
- 用于进行事件的分发
- 只要事件传给当前View,该方法一定会被调用
- 返回结果受到当前View的onTouchEvent和下级View的dispatchTouchEvent影响
- 表示是否消耗当前事件
4、ViewGroup事件分发伪代码:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
boolean intercepted = false;
intercepted = onInterceptTouchEvent(ev);
// 1、没有被拦截,分发给子View
if(intercepted == false){
consume = child.dispatchTouchEvent(ev);
}
// 2、事件被拦截因此自己进行处理 || 子View没有消耗该事件因此自己进行处理
if(intercepted == true || consume == false){
// 3、交给当前View进行处理(调用的是View的dispatchTouchEvent,该方法就是处理事件,等效于onTouchEvent)
consume = super.dispacthTouchEvent(ev);
}
return consume;
}
super.dispacthTouchEvent(ev): 就是调用ViewGroup父类View的dispacthTouchEvent方法。该方法是直接对事件的处理。
5、View事件分发伪代码:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 1. 判断是否有OnTouchListener,返回true,则处理完成
if (mOnTouchListener != null){
result = mOnTouchListener.onTouch(this, event);
}
// 2. 事件没有被消耗。并且,如果有代理,会执行代理的onTouchEvent方法
if (result == false && mTouchDelegate != null) {
result = mTouchDelegate.onTouchEvent(event);
}
// 3. 事件没有被消耗。才会调用onTouchEvent
if (result == false) {
result = onTouchEvent(event);
// 4. 接收到UP事件,就会执行OnClickListener的onClick方法
if(MotionEvent.ACTION_UP == action && mOnClickListener != null){
mOnClickListener.onClick(event);
}
}
// 5. 返回事件处理的结果(是否消耗该事件)
return result;
}
- mTouchDelegate和mOnClickListener本质都是在onTouchEvent中执行的,作为伪代码就忽视这些细节了。并不影响整个流程的层级。
6、View和ViewGroup在dispatchTouchEvent上的区别
- ViewGroup在dispatchTouchEvent()中会进行事件的分发。
- View在dispatchTouchEvent()中会对该事件进行处理。
onInterceptTouchEvent
滑动冲突(8题)
滑动冲突的三种场景
1、滑动冲突的三种场景
- 内层和外层滑动方向不一致:一个垂直,一个水平
- 内存和外层滑动方向一致:均垂直or水平
- 前两者层层嵌套
滑动冲突处理原则和解决办法
2、 滑动冲突处理原则
- 对于内外层滑动方向不同,只需要根据滑动方向来给相应控件拦截
- 对于内外层滑动方向相同,需要根据业务来进行事件拦截
- 前两者嵌套的情况,根据前两种原则层层处理即可。
3、 滑动冲突解决办法
- 外部拦截:在父容器进行拦截处理,需要重写父容器的onInterceptTouchEvent方法
- 内部拦截:父容器不拦截任何事件,事件都传递给子元素。子元素需要就处理,否则给父容器处理。需要配合
requestDisallowInterceprtTouchEvent
方法。
外部拦截
4、外部拦截法要点
- 父容器的
onInterceptTouchEvent
方法中处理- ACTION_DOWN不拦截,一旦拦截会导致后续事件都直接交给父容器处理。
- ACTION_MOVE中根据情况进行拦截,拦截:return true,不拦截:return false(外部拦截核心)
- ACTION_UP不拦截,如果父控件拦截UP,会导致子元素接收不到UP进一步会让onClick方法无法触发。此外UP拦截也没什么用。
5、onClick方法生效的两个条件?
View可以点击
接收到了DOWN和UP事件
6、外部拦截,自定义ScrollView
//Kotlin
class CustomScrollView(context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int): ScrollView(context, attrs, defStyleAttr, defStyleRes) {
constructor(context: Context) : this(context, null, 0, 0)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)
var lastX: Int = 0
var lastY: Int = 0
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
val curX = ev.x.toInt()
val curY = ev.y.toInt()
when(ev.action){
ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
}
ACTION_MOVE -> {
//如果是水平滑动则交给父容器处理
if(Math.abs(curX - lastX) > Math.abs(curY - lastY)){
parent.requestDisallowInterceptTouchEvent(false)
}
}
ACTION_UP -> null
else -> null
}
lastX = curX
lastY = curY
return super.dispatchTouchEvent(ev)
}
}
滑动(39题)
滑动的7种实现方法
1、View滑动的7种方法:
- layout:对View进行重新布局定位。在onTouchEvent()方法中获得控件滑动前后的偏移。然后通过layout方法重新设置。
- offsetLeftAndRight和offsetTopAndBottom:系统提供上下/左右同时偏移的API。onTouchEvent()中调用
- LayoutParams: 更改自身布局参数
- scrollTo/scrollBy: 本质是移动View的内容,需要通过父容器的该方法来滑动当前View
- Scroller: 平滑滑动,通过重载
computeScroll()
,使用scrollTo/scrollBy
完成滑动效果。- 属性动画: 动画对View进行滑动
- ViewDragHelper: 谷歌提供的辅助类,用于完成各种拖拽效果。
2、Layout实现滑动
/*================================*
* onTouchEvent-进行偏移计算,之后调用layout
*================================*/
public boolean onTouchEvent(MotionEvent event) {
float curX = event.getX(); //手指实时位置的X
float curY = event.getY(); //Y
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
int offsetX = (int)(curX - downX); //X偏移
int offsetY = (int)(curY - downY); //Y偏移
/**=============================================
* 变化后的距离=getLeft(当前控件距离父控件左边的距离)+偏移量——调用layout重新布局
*============================================*/
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
case MotionEvent.ACTION_DOWN:
downX = curX; //按下时的坐标
downY = curY;
break;
}
return true;
}
3、offsetLeftAndRight和offsetTopAndBottom实现滑动
/*================================*
* onTouchEvent-进行偏移计算,直接调用
*================================*/
public boolean onTouchEvent(MotionEvent event) {
float curX = event.getX(); //手指实时位置的X
float curY = event.getY(); //Y
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
int offsetX = (int)(curX - downX); //X偏移
int offsetY = (int)(curY - downY); //Y偏移
/**=============================================
* 对left和right, top和bottom同时偏移
*============================================*/
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
case MotionEvent.ACTION_DOWN:
downX = curX; //按下时的坐标
downY = curY;
break;
}
return true;
}
4、LayoutParams实现滑动:
- 通过父控件设置View在父控件的位置,但需要指定父布局的类型,不好
- 用ViewGroup的MariginLayoutParams的方法去设置margin
//方法一:通过布局设置在父控件的位置。但是必须要有父控件, 而且要指定父布局的类型,不好的方法。
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
/**===============================================
* 方法二:用ViewGroup的MarginLayoutParams的方法去设置marign
* 优点:相比于上面方法, 就不需要知道父布局的类型。
* 缺点:滑动到右侧控件会缩小
*===============================================*/
ViewGroup.MarginLayoutParams mlayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
mlayoutParams.leftMargin = getLeft() + offsetX;
mlayoutParams.topMargin = getTop() + offsetY;
setLayoutParams(mlayoutParams);
5、scrollTo\scrollBy实现滑动
- 都是View提供的方法。
- scrollTo-直接到新的x,y坐标处。
- scrollBy-基于当前位置的相对滑动。
- scrollBy-内部是调用scrollTo.
scrollTo\scrollBy
, 效果是移动View的内容,因此需要在View的父控件中调用。
// 1、移动到目标位置
((View)getParent()).scrollTo(dstX, dstY);
// 2、相对滑动:且scrollBy是父容器进行滑动,因此偏移量需要取负
((View)getParent()).scrollBy(-offsetX, -offsetY);
6、scrollTo/By内部的mScrollX和mScrollY的意义
- mScrollX的值,相当于手机屏幕相对于View左边缘向右移动的距离,手机屏幕向右移动时,mScrollX的值为正;手机屏幕向左移动(等价于View向右移动),mScrollX的值为负。
- mScrollY和X的情况相似,手机屏幕向下移动,mScrollY为+正值;手机屏幕向上移动,mScrollY为-负值。
- mScrollX/Y是根据第一次滑动前的位置来获得的,例如:第一次向左滑动200(等于手机屏幕向右滑动200),mScrollX = 200;第二次向右滑动50, mScrollX = 200 + (-50)= 150,而不是(-50)。
7、动画实现滑动的方法
- 可以通过传统动画或者属性动画的方式实现
- 传统动画需要通过设置fillAfter为true来保留动画后的状态(但是无法在动画后的位置进行点击操作,这方面还是属性动画好)
- 属性动画会保留动画后的状态,能够点击。
8、ViewDragHelper
- 通过
ViewDragHelper
去自定义ViewGroup
让其子View
具有滑动效果。