本文分析版本: Android API 23
1.简介
ScrollView
是我们在开发中经常使用的控件。当我们需要展示的内容比较多但并不是重复的item
时,我们就会使用ScrollView
使内容可以在垂直方向滚动显示防止显示不全。ScrollView
使用起来非常简单,大多数情况下你甚至都不用写一行Java
代码就能使用ScrollView
了。但是要注意的是ScrollView
中只能添加一个子View
。今天我们就来看看ScrollView
到底是如何实现的。以及最后会教大家一行代码实现类似IOS
上的弹性ScrollView
。_
2.源码分析
2.1 继承关系
2.2 主要辅助类
//用来计算滑动位置
private OverScroller mScroller;
//用来绘制边缘阴影
private EdgeEffect mEdgeGlowTop;
private EdgeEffect mEdgeGlowBottom;
//用于计算滑动时的加速度
private VelocityTracker mVelocityTracker;
2.3 构造方法
ScrollView
的构造方法如下:
public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initScrollView();
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
a.recycle();
}
在构造方法中分别调用了initScrollView()
与setFillViewport()
方法,代码如下:
private void initScrollView() {
//初始化OverScroller
mScroller = new OverScroller(getContext());
setFocusable(true);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
setWillNotDraw(false);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
//被认为是滑动操作的最小距离
mTouchSlop = configuration.getScaledTouchSlop();
//最小加速度
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
//最大加速度
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
//用手指拖动超过边缘的最大距离
mOverscrollDistance = configuration.getScaledOverscrollDistance();
//滑动超过边缘的最大距离
mOverflingDistance = configuration.getScaledOverflingDistance();
}
可以看到是初始化了一些类与参数,继续看看setFillViewport()
:
public void setFillViewport(boolean fillViewport) {
if (fillViewport != mFillViewport) {
mFillViewport = fillViewport;
requestLayout();
}
}
只是根据布局文件中的fillViewport
属性来给mFillViewport
赋值并调用requestLayout()
方法。mFillViewport
如果为true
则表示:将子View
的高度延伸到和视图高度一致,即充满整个视图。初始化结束之后,会进入到绘制流程。下面我们按照Measure
-> Layout
-> Draw
的绘制流程来分析ScrollView
中的实现。
2.4 Measure、Layout与Draw
2.4.1 onMeasure方法的实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
// 获取子View
final View child = getChildAt(0);
// 获取ScrollView的高度
final int height = getMeasuredHeight();
if (child.getMeasuredHeight() < height) {
final int widthPadding;
final int heightPadding;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
// 获取ScrollView的padding
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height - heightPadding, MeasureSpec.EXACTLY);
//根据新的高度重新measure子View
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
从代码中可以看到首先调用了super.onMeasure(widthMeasureSpec, heightMeasureSpec);
即父类FrameLayout
的onMeasure()
方法。如果我们将mFillViewport
设置为false
的话将会直接return
。当为true
时才会继续执行,会根据子View
的高度和ScrollView
本身的高度决定是否重新measure
子View
使其充满ScrollView
。ScrollView
的onMeasure()
其实就是处理了mFillViewport
。
2.4.1 onLayout方法的实现
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mIsLayoutDirty = false;
// Give a child focus if it needs it
if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
scrollToChild(mChildToScrollTo);
}
mChildToScrollTo = null;
//是否还未添加过window中去
if (!isLaidOut()) {
if (mSavedState != null) {
mScrollY = mSavedState.scrollPosition;
mSavedState = null;
} // mScrollY default value is "0"
final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
final int scrollRange = Math.max(0,
childHeight - (b - t - mPaddingBottom - mPaddingTop));
// Don't forget to clamp