ScrollView
是 Android 中用于实现单向滚动功能的布局容器。它只能容纳一个子视图,并且能够使这个子视图在垂直方向(默认)或水平方向上滚动。下面我们将结合源码来分析 ScrollView
的实现原理。
1. ScrollView 类定义
ScrollView
继承自 FrameLayout
,这意味着它本身是一个布局容器,可以包含一个子视图,并且提供了滚动功能。
1public class ScrollView extends FrameLayout implements NestedScrollingParent {
2
3 private static final String TAG = "ScrollView";
4
5 // 滚动状态
6 private static final int SCROLL_STATE_IDLE = 0;
7 private static final int SCROLL_STATE_DRAGGING = 1;
8 private static final int SCROLL_STATE_SETTLING = 2;
9
10 private static final int MAX_SCROLL_DURATION = 250; // 最大滚动持续时间
11
12 // 内部状态
13 private int mScrollState = SCROLL_STATE_IDLE;
14
15 // 滚动监听器
16 private OnScrollChangeListener mOnScrollChangeListener;
17
18 // 构造函数
19 public ScrollView(Context context) {
20 super(context);
21 initScrollView();
22 }
23
24 public ScrollView(Context context, AttributeSet attrs) {
25 super(context, attrs);
26 initScrollView();
27 }
28
29 public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
30 super(context, attrs, defStyleAttr);
31 initScrollView();
32 }
33
34 private void initScrollView() {
35 // 初始化操作
36 setFillViewport(true); // 填充整个视口
37 setFocusable(true); // 可聚焦
38 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // 子视图优先获得焦点
39 }
40
41 // 设置滚动监听器
42 public void setOnScrollChangeListener(OnScrollChangeListener l) {
43 mOnScrollChangeListener = l;
44 }
45
46 // 获取当前滚动位置
47 public int getScrollY() {
48 return getScrollY();
49 }
50
51 // 滚动到指定位置
52 public void scrollTo(int x, int y) {
53 super.scrollTo(x, y);
54 }
55
56 // 平滑滚动到指定位置
57 public void smoothScrollTo(int destX, int destY) {
58 AutoScrollHelper.getInstance(this).ensureTarget(this);
59 AutoScrollHelper.getInstance(this).startAnimation(destX, destY);
60 }
61
62 // 其他方法...
63}
2. 测量过程(onMeasure)
ScrollView
的 onMeasure
方法负责测量其子视图,并根据子视图的大小和父容器的约束来确定 ScrollView
自身的大小。
1@Override
2protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
4
5 int childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
6 int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
7
8 // 创建子视图的测量规格
9 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
10 int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.UNSPECIFIED);
11
12 // 测量子视图
13 View child = getChildAt(0);
14 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
15
16 // 设置测量结果
17 setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
18}
3. 布局过程(onLayout)
ScrollView
的 onLayout
方法负责根据子视图的大小和滚动位置来确定子视图的位置。
1@Override
2protected void onLayout(boolean changed, int l, int t, int r, int b) {
3 final int width = r - l;
4 final int height = b - t;
5 final int paddingLeft = getPaddingLeft();
6 final int paddingTop = getPaddingTop();
7 final int paddingRight = getPaddingRight();
8 final int paddingBottom = getPaddingBottom();
9
10 final int childWidth = width - paddingLeft - paddingRight;
11 final int childHeight = height - paddingTop - paddingBottom;
12
13 final View child = getChildAt(0);
14 if (child != null) {
15 final int childLeft = paddingLeft;
16 final int childTop = paddingTop;
17 child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
18 }
19}
4. 滚动处理
ScrollView
通过重写 onTouchEvent
方法来处理触摸事件,并实现滚动功能。
1@Override
2public boolean onTouchEvent(MotionEvent ev) {
3 // ...
4 switch (ev.getActionMasked()) {
5 case MotionEvent.ACTION_DOWN:
6 // ...
7 break;
8 case MotionEvent.ACTION_MOVE:
9 // ...
10 break;
11 case MotionEvent.ACTION_UP:
12 // ...
13 break;
14 case MotionEvent.ACTION_CANCEL:
15 // ...
16 break;
17 }
18 return true;
19}
5. 滚动逻辑
滚动逻辑主要在 computeScrollDeltaToGetChildRectOnScreen
方法中实现,该方法计算滚动距离以确保指定的矩形可见。
1int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
2 // ...
3 // 计算需要滚动的距离
4 int scrollDelta;
5 if (rect.top < 0) {
6 scrollDelta = -rect.top;
7 } else if (rect.bottom > getHeight()) {
8 scrollDelta = getHeight() - rect.bottom;
9 } else {
10 scrollDelta = 0;
11 }
12 // 返回滚动距离
13 return scrollDelta;
14}
6. 平滑滚动
ScrollView
使用 AutoScrollHelper
类来实现平滑滚动动画。
1public void smoothScrollTo(int destX, int destY) {
2 AutoScrollHelper.getInstance(this).ensureTarget(this);
3 AutoScrollHelper.getInstance(this).startAnimation(destX, destY);
4}
7. 回调方法
ScrollView
提供了 OnScrollChangeListener
接口,允许开发者监听滚动事件。
1public interface OnScrollChangeListener {
2 void onScrollChange(ScrollView who, int x, int y, int oldx, int oldy);
3}
8. NestedScrollingParent 支持
ScrollView
实现了 NestedScrollingParent
接口,支持嵌套滚动功能。
1@Override
2public boolean startNestedScroll(int axes) {
3 // ...
4 return true;
5}
6
7@Override
8public void stopNestedScroll() {
9 // ...
10}
11
12@Override
13public boolean hasNestedScrollingParent() {
14 // ...
15 return false;
16}
17
18@Override
19public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
20 // ...
21}
22
23@Override
24public void dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
25 // ...
26}
27
28@Override
29public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
30 // ...
31 return false;
32}
33
34@Override
35public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
36 // ...
37 return false;
38}
总结
ScrollView
的实现原理涉及测量和布局其内部的子视图,以及处理触摸事件来实现滚动功能。通过重写 onMeasure
和 onLayout
方法,ScrollView
确保子视图正确地显示在屏幕上。同时,它还通过处理触摸事件来响应用户的滚动操作,并提供了平滑滚动和滚动监听的支持。此外,ScrollView
还实现了 NestedScrollingParent
接口,以支持嵌套滚动场景。