安卓基础知识系列旨在简明扼要地提供面试或工作中常用的基础知识,让对安卓还不太熟悉的小伙伴更快地入门。同时自己在工作中,也没法完全记住所有的基础细节,写这样的系列文章,可以让自己形成一个更完备的知识体系,同时给自己日后留个知识参考。
开始的开始
View 是除了安卓四大组件以外用的最多的控件,我们需要利用 Android 内置的 View 控件(TextView、LinearLayout等)完成业务需求,有时甚至需要自定义控件来满足特殊的业务需求。View 还负责与用户的交互(点击、滑动、长按等操作),掌握 View 知识体系,对我们理解 View 与用户的交互过程非常有帮助,同时也是我们自定义 View 控件所必须掌握的基础知识。
关于 View 知识体系内容,我觉得安卓开发艺术探索这本书讲的十分经典,所以本篇大部分内容都参考于它。文章筛选了书中比较常用的内容,有书的朋友可以直接看书即可。
正文
本篇内容主要向大家介绍 View 的一些基础知识。
View 的位置参数
View的位置主要由它的四个顶点来决定,分别对应于View的四个属性
-
left:View 左上角横坐标
-
top:View 左上角纵坐标
-
right:View 右下角横坐标
-
bottom:View 右下角纵坐标
值得注意的是,这些坐标都是相对于该 View 的父容器来说的,因此它是一种相对坐标。
在 Android 中,x 轴 和 y 轴的正方向分别为右和下。事实上,不仅仅是安卓,大部分显示系统都是按照这个标准来定义坐标系的:
根据上图,我们比较容易算出一个 View 的宽、高:
val widght = right - left
val height = bottom - top
此外,View除了上面四个位置参数以外,还有几个参数:x、y、translationX 和 translationY。其中 x 和 y 代表的是 View 左上角的坐标,而 translationX 和 translationY 是 View 左上角相对于父容器的偏移量。
这几个参数都是相对于父容器的坐标,并且 translationX 和 translationY 默认值为 0。
以上参数的换算关系如下:
val x = left + translationX
val y = top + translationY
值得一提的是,View在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值不会改变。会发生变化的是 x、y、translationX、translationY。
可以通过修改View的translation值,来修改其在ViewGroup中的布局位置。View在ViewGroup中的布局位置改变了,意味着View接收触摸事件(TouchEvent)的位置也会改变。后文会提到一个View的内容位置,移动View的内容位置并不会影响该View接收触摸事件的范围。
MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的时间类型有如下三种:
- ACTION_DOWN:手指刚接触屏幕的一瞬间。
- ACTION_MOVE:手指在屏幕上移动。
- ACTION_UP:手指从屏幕上松开的一瞬间。
正常情况下,一次手指接触屏幕的行为会触发一系列点击事件,考虑如下几种情况;
- 点击屏幕后松开,事件序列为 DOWN -> UP。
- 点击屏幕滑动一会再松开,事件序列为 DOWN -> MOVE -> … -> MOVE -> UP。
上述三中情况时典型的事件序列,通过 MotionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。为此,系统在 MotionEvent 中提供了两组方法:getX() / getY() 和 getRawX() / getRawY()。它俩的区别如下:
方法 | 描述 |
---|---|
getX() / getY() | 返回相对于当前 View 的左上角 x 和 y 坐标。 |
getRawX() / getRawY() | 返回相对于手机屏幕左上角的 x 和 y 坐标。 |
TouchSlop
TouchSlop 是系统所能识别出的被认为是滑动的最小距离。换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
TouchSlop 是一个常量,它跟设备有关,不同设备商这个值可能是不同的,通过如下方式即可获取这个常量:
ViewConfiguration.get(context).getScaledTouchSlop()
这个常量有什么意义呢?当我们在处理滑动的时候,可以利用这个常量来做一些过滤。比如当两次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值。因此就可以认为它们不是滑动,这样做可以有更好的用户体验。
该常量值的定义可以在源码中找到,在 frameworks/base/core/res/res/values/config.xml
文件中,如下所示:
<!-- Base "touch slop" value used by ViewConfiguration as a
movement threshold where scrolling should begin. -->
<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
VelocityTracker
VelocityTracker 用于做手指在滑动过程中的速度追踪,包括水平和竖直方向的速度。**速度追踪可用于手指快速滑动后抬起时,模拟 View 的惯性滑动。**一般情况下,用户快速滑动屏幕后抬起时,View 的滑动事件随之停止,这种体验不太好,如果 View 能在用户抬起手指后,保持一定的惯性,减速滑动直到停止,那用户体验明显会高很多。像 ScrollView 就是通过 VelocityTracker 来实现惯性滑动。
它的使用过程很简单,首先,在 View 的 onTouchEvent 方法中追踪当前单击事件的速度:
val velocityTracker = VelocityTracker.obtain()
velocityTracker.addMovement(event)
接着,当我们想知道当前的滑动速度时,这个时候可以采用如下方法来获得当前速度:
// 计算滑动速度,计算的时间间隔为 1000ms
// 也即,计算 1000ms 内手指所滑动的像素数
velocityTracker.computeCurrentVelocity(1000)
val xVelocity = velocityTracker.getXVelocity()
val yVelocity = velocityTracker.getYVelocity()
比如将时间间隔设为 1000ms,在 1s 内手指在水平方向从左向右滑动 100 像素,那么水平速度即为 100 px/s。
速度也可以是负值,当手指从右往左滑动时,水平速度即为负值,速度的计算可以遵循如下公式:
速度 = (终点位置 - 起点位置) / 时间间隔
根据上面的公式再加上 Android 系统的位置坐标系理解,可以知道,手指逆着 X 轴的坐标系时,其水平方向的滑动速度为负值,Y 轴同理。
最后,在不需要使用它时,需要调用clear()
方法来重置并回收内存
velocityTracker.clear()
velocityTracker.recycle()
GestureDetector
用于辅助检测用户的单击、滑动、长按、双击等行为。一般情况下我们需要处理用户手指的单击、长按和滑动操作,可以直接在 onClick()、onLongClick() 和 onTouchEvent() 中处理即可,如果还需要 View 处理用户的双击操作,则需要借助 GestureDetector。
scrollTo和scrollBy
为了实现 View 的滑动,View 提供了专门的方法来实现这个功能,那就是 scrollTo() 和 scrollBy(),
/**
* 设置视图的滚动位置. 这将导致调用 {@link #onScrollChanged(int, int, int, int)}
* 然后 View 会失效并被重构
* @param x 要滚动到的x坐标位置
* @param y 要滚动到的y坐标位置
*/
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();
}
}
}
/**
* 移动视图的滚动位置. 这将导致调用 {@link #onScrollChanged(int, int, int, int)}
* 然后 View 会失效并被重构
* @param x 水平滚动的像素数
* @param y 竖直滚动的像素数
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
从源码可以看出,scrollBy() 实际上也是调用了 scrollTo() 方法,它实现了基于当前位置的相对滑动,而 scrollTo() 实现了基于所传递参数的绝对滑动。
这里又出现了 View 的两个位置属性,mScrollX 和 mScrollY,这两个属性的改变规则简要概括一下:
- mScrollX:在滑动过程中 mScrollX 的值总是等于 View 左边缘和内容左边缘的在水平方向的距离
- mScrollY:在滑动过程中 mScrollY 的值总是等于 View 上边缘和内容上边缘的在水平方向的距离
View 边缘指的是 View 的位置,由四个顶点(left,top,right,bottom)组成,View 内容边缘是指 View 中内容的边缘。
mScrollX 和 mScrollY的单位是像素,当 View 左边缘在 View 的内容左边缘右边时,mScrollX 为正值,反之为负值。当 View 上边缘在 View 内容上边缘下边时,mScrollY 为正值,反之为负值
换句话说,如果从左向右滑动,那么 mScrollX 为正值,反之为负值,如果从下往上滑动,mScrollY 为正值,反之为负值。
scrollTo() 和 scrollBy() 只能改变 View 的内容位置而不能改变 View 在布局中的位置,正如前文所说,View 的 left、top、right、bottom 位置是不跟随手势滑动改变的。也就是说,不管怎么滑动,也不可能将当前 View 滑动到附近 View 所在的区域,这个需要仔细体会一下。
Scroller
Scroller 是一个对象,用于实现 View 的弹性滑动。
使用 View 的 scrollTo() 和 scrollBy() 方法来进行滑动时,其过程是瞬间完成的,这个没有过渡效果的滑动用户体验不好。这时候就可以使用 Scroller 来实现有过渡效果的滑动来增加用户体验,其过程不是瞬间完成的,而是在一定的时间间隔内完成。
Scroller本身无法让 View 进行弹性滑动,它需要借助 View 的 computeScroll() 方法,要使用 Scroller,有一套固定的实现:
val scroller = Scroller(mContext)
private fun smoothScrollTo(destX: Int, dest: Int) {
val scrollX = getScrollX()
val deltaX = destX - scrollX
// 1000ms 内滑向 destX
mScroller.startScroll(scrollX, 0, deltaX, 0, 1000)
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY())
postInvalidate()
}
}
上面是 Scroller 的典型使用方法,这里先描述它的工作原理:当我们构造一个 Scroller 对象并且调用它的 startScroll 方法时,Scroller 内部其实什么都没做,它只是保存了我们传递的几个参数:
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
可以看到,仅仅调用 startScroll() 是无法让 View 滑动的,因为它内部没有做滑动相关的事,那 Scroller 到底是如何让 View 弹性滑动的呢?
是通过 startScroll() 方法下面的 invalidate() 方法。
invalidate() 方法会导致 View 重绘,在 View 的 draw() 方法又会去调用 computeScroll() 方法,computeScroll() 方法在 View 中是一个空实现,需要我们自己去实现,上面的代码已经实现了 computeScroll() 方法,正是因为这个方法,View 才能实现弹性滑动。在 computeScroll() 方法中,又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo() 方法实现滑动,接着又调用 postInvalidate() 方法来进行第二次重绘,这一次重绘过程和第一次重绘过程一样,还是会调用 computeScroll() 方法,然后返回上面的过程,直到整个滑动过程结束。
看看 computeScrollOffset() 内部实现:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
// 计算已经流逝的时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
// 计算时间流逝的百分比
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
...
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
这个方法会根据时间的流逝来计算出当前的 scrollX 和 scrollY 的值。
这个方法的返回值也很重要,它返回 true 表示滑动还未结束,反之表示滑动已经结束。
看到这里相信你已经明白了 Scroller的工作原理,将以上内容简单总结一下:Scroller 并不能实现 View 的滑动,它需要配合 View 的 computeScroll 方法才能完成弹性滑动的效果。它不断让 View 重绘,每一次重绘距离滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,从而通过 scrollTo 来一点点地滑动 View 的内容位置(View 的位置不变,滑动的始终是 View 的内容位置)。
是的没有错,所谓的弹性滑动,就是一段连续的移动操作所呈现的视觉效果,动画的执行也是一种视觉效果。
用一张图来说明上面的过程:
最后的最后
View 基础知识常用的内容差不多就是这些了。上文介绍了不少内容,都有哪些呢?
-
View 的位置参数,android 屏幕的坐标系。View 有四个位置参数:left、top、right、bottom,这些位置参数都是相对于父容器的,其值不会随着 View 的滑动改变
-
View 的 translationX 和 translationY 属性,指的是 View 左上角相对于父容器的偏移量,其值会跟着 View 的滑动改变,默认值为0。
-
与 translationX、translationY 类似的位置属性还有:
mScrollX:指的是在滑动过程中 mScrollX 的值总是等于 View 左边缘和内容左边缘的在水平方向的距离
mScrollY:指的是在滑动过程中 mScrollY 的值总是等于 View 上边缘和内容上边缘的在水平方向的距离
-
MotionEvent 事件对象,手指触碰屏幕移动一段距离后抬起时,会触发 ACTION_DOWN -> ACTION_MOVE… -> ACTION_MOVE -> ACTION_UP 一系列事件,可以从 MotionEvent 对象中获取手指此时的位置信息(getX()、getY() 获取相对于当前 View 左上角的位置信息,getRawX()、getRawY() 获取相对于屏幕左上角的位置信息)。
-
VelocityTracker 用于做手指在滑动过程中的速度追踪,包括水平和竖直方向的速度。速度追踪可用于手指快速滑动后抬起时,模拟 View 的惯性滑动。
-
scrollTo() 和 scrollBy() 方法,可以用来进行 View 内容的瞬间滑动,中间没有过渡效果。
-
可以借助 Scroller 对象实现 View 内容的弹性滑动,这种弹性滑动有过度效果,用户体验会好一些。其原理是借助 View 的 computeScroll() 方法,根据时间流逝的百分比,拿到 View 的下一滑动位置,传入 scrollTo() 完成一小段距离的滑动,不断移动一小段距离,最终在规定时间间隔内完成 View 内容的滑动。
本文的内容到这里就结束啦,下一篇内容将会介绍 View的事件传递机制,希望文章内容对你有所帮助~
文章内容参考
兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
- 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!
拜托拜托,谢谢各位同学!