View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

时间:2023-03-09 09:41:39
View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

关于View绘制系列的文章已经完成了四篇了,前面四篇文章主要带小伙伴们熟悉一下View的体系的整体框架、View的测量以及布局等过程,从本篇博客开始,我们就来看看View的绘制过程。View的绘制涉及到许多细小的知识点,一篇博客很难全部介绍清楚,所以我打算采用农村包围城市的方式,先把这里边会涉及到的各种琐碎的知识点给小伙伴们介绍一遍,然后我们再来看View绘制的整体流程,及draw方法和onDraw方法到底是怎么回事。OK,那么本篇博客我们就先来看看View绘制过程中涉及到的第一个问题,View的滚动。对了,如果小伙伴们还没看过之前的四篇文章,可以先移步这里,那四篇文章有助于你理解本文:

1.View绘制详解,从LayoutInflater谈起

2.View绘制详解(二),从setContentView谈起

3.View绘制详解(三),扒一扒View的测量过程

4.View绘制详解(四),谝一谝layout过程

好了,废话不多说,来看看今天的话题。

OK,说到View的绘制,我们首先得明白一点,就是我们Android手机中View是没有边界的,View是无限大的,View中的画布Canvas也是无限大的。说到这里有许多小伙伴就有疑问了,我们前面不是刚说了View的测量吗?每一个View都是有大小的,怎么现在又没有大小了呢?其实,我们说的View的大小是指View的父容器分配给它的大小,View的内容只能在父容器分配的大小中显示,如果View的内容显示在父容器分配的区域之外,则用户就看不到这一部分内容,但是并不是说超过的部分就不存在。我们以下图的TextView为例:

View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

TextView显示在任意一个容器中,该容器分配给TextView的大小就是中间黑色框的部分,TextView在绘制它自身的内容的时候只能在中间黑色框中绘制,绘制在黑色框以外的部分将会显示不出来,但并不能意味着这不能绘制。我们在onDraw方法中获取到的canvas,它的大小经过剪裁之后已经是当前控件的大小了,同时,参考的坐标点也变成了当前View的左上角。

OK,为什么要说这个问题呢?因为这里涉及到View绘制时的两个方法ScrollTo和ScrollBy,我们来看看View中draw方法的一小段源码:

@CallSuper
public void draw(Canvas canvas) { ...
... if (!dirtyOpaque) {
drawBackground(canvas);
} ...
...
} private void drawBackground(Canvas canvas) { ...
... final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}

这个是绘制View的背景的一段源码,在View绘制之前,要先将canvas平移,平移完了之后再绘制,绘制完了之后再平移成原来的状态。为什么要这么做呢?我们来看下面一张图:

View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

父容器分配给TextView可显示的区域就是中间的黑色框,但是TextView的内容区域由于滚动已经不处于这个黑色框中了(辛弃疾的词即为TextView的内容区域),那怎么样完成这种滚动效果呢?我们需要通过translate方法来让canvas平移。小伙伴们想象一下,中间的黑色框不动,画布向右下平移才能进入到黑色框中,移到黑色框中之后进行绘制,绘制完成之后再移动回去,这个时候TextView的内容区域不就跑到TextView的左上角去了么。那么在这次的移动操作中,我们的scrollX和scrollY两个参数为正数,但是我们的View的内容却往左上角移动,这就是原因。因为我们移动的不是TextView,我们移动的是canvas。我们来看下面一个简单的例子,效果图如下:

View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

当我点击Button时,TextView的内容自动移动到TextView的左下角,我们来看看Button的点击事件:

tv.scrollTo(-100, -100);

负数是向右下角移动,因为负数表示canvas先向左上角x、y轴各移动100px,然后再向右下角x、y各100px,如下:

View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

说到这里,实名反对泡网这篇文章,View 的scrollTo 和scrollBy,误人子弟。

OK,对于一个View而言,scrollTo移动的是View的内容,对于ViewGroup而言,scrollTo移动的则是ViewGroup中的子控件。OK,我们来看看scrollTo的源码:

public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}

这里就是给mScrollX和mScrollY重新赋值,然会回调onScrollChangeListener重新绘制View。OK,有一个和scrollTo功能相似的方法叫做ScrollBy,我们来看看:

public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

scrollBy中也是调用了scrollTo发那个发,不同的是它没有给mScrollX和mScrollY重新赋值,而是在当前的基础上再偏移多少。这也就是我们常说的scrollTo表示移动到哪里,而scrollBy表示移动多少。OK,基于此,我们来做下面一个小案例:

View绘制详解(五),draw方法细节详解之View的滚动/滑动问题

屏幕中有一个TextView,当我的手指在TextView上拖动的时候,我们的TextView可以*的移动,OK,我们来看看代码:

tv.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = (int) event.getX();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
((ViewGroup) v.getParent()).scrollBy(-(int) event.getX()+downX, -(int) event.getY()+downY);
break;
}
return true;
}
});

TextView想在它自己所在的容器中滚动,但是TextView自己不能滚动,如果调用了TextView的滚动方法实际上滚动的就是TextView中的文本了。所以我们要调用的是TextView的父容器的滚动方法,而且还要将值改为相反数,原因不用我多说了吧。至于要加上downX和downY两个值,是因为我们的手指不一定就按在TextView的左上角,所以要减去手指到TextView左上角这一段距离。

OK,这就是View绘制方法中涉及到的第一个小细节(View无限大,canvas无限大),以及由这个细节牵扯出来的两个方法scrollTo和scrollBy。OK,draw方法的第一次分析就到这里,有问题欢迎留言讨论。

以上。