前言

学习清单:

  • View是什么
  • View的位置参数
  • View的触控
  • View的滑动
  • View的动画
  • View的事件分发机制
  • View的工作原理
  • View的自定义方式

一.为什么要学习View?

View,是Android中十分重要的一个知识点,是所有控件的基类,尽管View不属于四大组件,但是它的作用堪比四大组件,甚至重要性大于ContentProviderBroadcast Receivers

ViewGroupView的继承,它的内部包含了一组View。

很多时候,面对产品经理的各种奇葩的需求,仅仅使用系统提供的控件是不能满足需求的,因此,我们就需要自定义特定的控件,而自定义控件就需要对View体系有一定程度的理解;有时候,涉及到滑动事件的自定义View的时候,难免会出现各种各样的滑动冲突,而要解决滑动冲突的话,还需要对View的事件分发机制了然于心。

综上,掌握好View这方面的知识,不仅可以让你在日常开发中对自定义View的各种场景胸有成竹,还可以让你在面试官的重重追问(ai hu)下游刃有余(xin tai bao zha)。

进阶之路 | 奇妙的View之旅-LMLPHP

二.核心知识点归纳

2.1 View的位置参数

Q1:Android坐标系是怎样的呢?

进阶之路 | 奇妙的View之旅-LMLPHP

Q2:View的位置怎么确定?

  • 由四个顶点确定,分别对应四个属性:top、left、right、bottom

进阶之路 | 奇妙的View之旅-LMLPHP

Q3:View偏移量translation

进阶之路 | 奇妙的View之旅-LMLPHP

需要注意的是,在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坐标(绝对坐标)。

    具体关系见下图:

进阶之路 | 奇妙的View之旅-LMLPHP

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

进阶之路 | 奇妙的View之旅-LMLPHP

2. LayoutParams
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求重新对View进行measure、layout
3. 动画

进阶之路 | 奇妙的View之旅-LMLPHP

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;
}

进阶之路 | 奇妙的View之旅-LMLPHP

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方法。重写子ViewdispatchTouchEvent()

    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()

进阶之路 | 奇妙的View之旅-LMLPHP

进阶之路 | 奇妙的View之旅-LMLPHP

Q2:事件分发本质是什么:

Q3:事件分发需要的主要方法是什么

  • dispatchTouchEvent:进行事件的分发(传递)。返回值是 boolean 类型,受当前onTouchEvent下级viewdispatchTouchEvent影响
  • onInterceptTouchEvent:对事件进行拦截。该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。如果一旦拦截,则执行ViewGrouponTouchEvent,在ViewGroup中处理事件,而不接着分发给View,且只调用一次,所以后面的事件都会交给ViewGroup处理。
  • onTouchEvent:进行事件处理

进阶之路 | 奇妙的View之旅-LMLPHP

2.5 View的工作原理

2.5.1 View工作流程

  • measure确定View的测量宽高
  • layout确定View的最终宽高四个顶点的位置
  • draw将View 绘制到屏幕
  • 对应onMeasure()onLayout()onDraw()三个方法。

进阶之路 | 奇妙的View之旅-LMLPHP

2.5.2 measure

进阶之路 | 奇妙的View之旅-LMLPHP

现在,分别讨论两种measure

  • View的measure:只有一个原始的View,通过measure()即可完成测量。

    进阶之路 | 奇妙的View之旅-LMLPHP

方法一:

	@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方法。

进阶之路 | 奇妙的View之旅-LMLPHP

2.5.3 layout

  • 确定View的最终宽高和四个顶点的位置

进阶之路 | 奇妙的View之旅-LMLPHP

2.5.4 draw

  • 绘制到屏幕

进阶之路 | 奇妙的View之旅-LMLPHP

2.6 自定义View

Q1:自定义View的类型有哪些

进阶之路 | 奇妙的View之旅-LMLPHP

特别提醒

进阶之路 | 奇妙的View之旅-LMLPHP

三.课堂小测试

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才能确定自己的测量宽高,在前几次的测量过程中,得出的测量宽高有可能和最终宽高不一致,但最终两者还是一致的。


如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

05-11 22:23