今天给大家带来这篇源码解读,首先很感谢大家能"赏脸。本文秉着思路清晰,细致分析源码脉络。从根本上带领大家学会自定义控件和分析源码,学会举一反三。"自定义,何等熟悉的名词。到底,它有多深奥,其实不然,咱们github千万自定义控件让人眼花缭乱,先克服恐惧。拒绝一味"承袭"人家控件。毕竟,人家的始终是人家的,自己的才是根本。好了,开篇不说多,咱们进入正题吧!分析源码第一要点,分析继承关系(快捷键F4)。我们通过图3可以看见ScrollView继承自FrameLayout布局。
然后我们先大致了解一下ScrollView中所有的方法。该部分为读者自己回顾整体查询,为节省篇幅,所以比较紧凑。先请大家快速地大致预览一下方法名,有个大致过目。为后面的讲解做准备。
首先,我们看到4个构造方法。由上往下互相调用,最终调用的是含有4个参数的构造方法。第四个构造方法的最后一个参数传入的是0。调用了父类的构造方法。而该构造方法
由View的构造方法继承而来。第四个构造方法里看似非常简单,首先得到属性集对象TypedArray,然后通过其getBoolean方法判断是否进行请求重新布局。
public ScrollView(Context context) {
this(context, null);
}
public ScrollView(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
}
public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
//调用第四个构造方法
public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initScrollView();//初始化ScrollView的一些参数,后面会讲
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);//获取属性集对象
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));//根据布局文件判断是否布局是否溢出当前窗口
a.recycle();//TypedArray对象回收
}在构造方法里面,首先通过obtainStyleAttributes方法去得到属性集对象TypeArray。里面的四个参数同上面的解释。
final TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
接下来调用了setFillViewport方法,这个方法是用来设置是否需要重新请求布局。在什么时候会调用requestLayout进行布局请求呢?秘法技就隐藏在TypeArray里的getBoolean方法里。该方法能判断传入的布局的是否溢出父布局。先声明:requestLayout方法不一样会执行,视XML布局文件而定。
private boolean mFillViewport;
public void setFillViewport(boolean fillViewport) {
if (fillViewport != mFillViewport) {
mFillViewport = fillViewport;//当布局文件满足溢出情况的时候,fillViewport为true
requestLayout();//请求布局
}
}
在这里传入的是a.getBoolean(R.styleable.ScrollView_fillViewport, false)的值。如果得到的值是true,那么就需要扩展。会调用requestLayout方法,请求布局来充满全屏。
setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
public boolean getBoolean(@StyleableRes int index, boolean defValue) {
if (mRecycled) {//如果结果集对象不存在
throw new RuntimeException("Cannot make calls to a recycled instance!");
}
index *= AssetManager.STYLE_NUM_ENTRIES;
final int[] data = mData;
final int type = data[index+AssetManager.STYLE_TYPE];
if (type == TypedValue.TYPE_NULL) {
return defValue;
} else if (type >= TypedValue.TYPE_FIRST_INT
&& type <= TypedValue.TYPE_LAST_INT) {
return data[index+AssetManager.STYLE_DATA] != 0;
}
final TypedValue v = mValue;
if (getValueAt(index, v)) {
StrictMode.noteResourceMismatch(v);
return XmlUtils.convertValueToBoolean(v.coerceToString(), defValue);
}
// type的值不会等于0,这个已经检测过了
throw new RuntimeException("getBoolean of bad type: 0x" + Integer.toHexString(type));
}接下来,我们看到requestLayout方法。调用了父类的请求布局方法。
@Override
public void requestLayout() {
mIsLayoutDirty = true;
super.requestLayout();
}
调用了父类的requestLayout方法。在这里我们知道了大致的原理。首先通过属性集的getBoolean方法获取值来判断是否需要进行请求内容扩展。如果为true,就进行请求更新
布局。在requestLayout方法里,利用一个标志来判断是否改变了当前的状态。
控件测量由onMeasure方法负责,此方法中传入的2个分别为widthMeasureSpec和widthMeasureSpec。在该方法中分别需要对三种模式进行测量。首先,我们来了解一下这三种模式。在三种模式下,我们测得的宽高最终会调用measure(int width,int height)设定最终测量结果。该三种模式为View类中的静态内部类MeasureSpec类的静态成员。标志着三种模式。ScrollView中主要对子控件进行了精确测量。而自身的高宽不做太多处理。
在UNSPECIFIED(未指定模式),父布局传递下来的宽高传入多少就是多少,也可以自行设置,设置多少就是多少,不受父类影响。而在EXACTLY(精确模式),当设置为wrap_contenxt的时候,它会默认设置为父布局传递下来的宽高。所以在此时需要重新测量。而在AT_MOST(至多模式),一般不考虑。所以我们只需要对精确模式下进行重新测量即可。我们看到源码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//溢出与否,就要通过之前我们的TypedArray里面的getBoolean里参数的布局文件来判断了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {//如果mFillViewport为true,则子布局充满当前可见区域,宽高即不需要重新测量。
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
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 desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}如果没有溢出,ScrollView自身的宽高即按照父布局来处理,子类按照父类宽高进行处理,但是当为精确模式下,子类设置高度为wrap_content不会生效,需要指定其宽高。此时ScrollView的高度始终和父布局高度一致。当溢出的时候,对子类布局进行测量过程中,首先判断ScrollView中是否包含子布局,如果包含的话,ScrollView子布局的宽高在精确模式下进行测量。此时子布局的宽度等于ScrollView的宽度减去ScrollView的padding左右内边距,高度等于ScrollView的高度减去padding的上下内边距。在这里需要注意的一点是,在SDK版本大于18的时候,子控件的宽高和原来的测量方式不一样了。在此之后,子控件的宽高等于ScrollView的宽高减去ScrollView本身的padding内边距再减去子控件自身的外边距。在SDK版本小于等于18的时候,子控件的测量宽高就是ScrollView的宽高减去子控件的内边距。这一点需要我们注意。 @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;
if (!isLaidOut()) {//检测是否超出屏幕
if (mSavedState != null) { //mSavedState对象为SavedState实例对象,而SavedState类为ScrollView的内部类。SavedState继承自BaseSavedState类。
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
if (mScrollY > scrollRange) {
mScrollY = scrollRange;
} else if (mScrollY < 0) {
mScrollY = 0;
}
}mSavedState对象为SavedState实例对象,而SavedState类为ScrollView的内部类。SavedState继承自BaseSavedState类。SavedState对象可以重写其writeToParcel方法实现序列化。具体代码如下:
static class SavedState extends BaseSavedState {
public int scrollPosition;
SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source) {
super(source);
scrollPosition = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(scrollPosition);
}
@Override
public String toString() {
return "HorizontalScrollView.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " scrollPosition=" + scrollPosition + "}";
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}所以我们实现当前属性的序列化的步骤为:
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Some old apps reused IDs in ways they shouldn't have.
// Don't break them, but they don't get scroll state restoration.
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mSavedState = ss;
requestLayout();
} @Override
protected Parcelable onSaveInstanceState() {
if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Some old apps reused IDs in ways they shouldn't have.
// Don't break them, but they don't get scroll state restoration.
return super.onSaveInstanceState();
}
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.scrollPosition = mScrollY;
return ss;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();//判断VelocityTracker对象是否存在,该对象用来计算速度
MotionEvent vtev = MotionEvent.obtain(ev);//复制已经存在的MotionEvent对象
final int actionMasked = ev.getActionMasked();//得到具体的手势事件行为。此处有三种手势行为的表示。
if (actionMasked == MotionEvent.ACTION_DOWN) {
mNestedYOffset = 0;
}
vtev.offsetLocation(0, mNestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
...
}
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_POINTER_DOWN: {
...
break;
}
case MotionEvent.ACTION_POINTER_UP:
...
break;
}
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}上面我们说过,通过obtain会将已经存在的VelocityTracker对象复制一个。 private void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
{
if (getChildCount() == 0) {//没有子View时候
return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {//通过OverScroller对象的isFinish方法判断是否滑动完毕
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
if (mFlingStrictSpan != null) {
mFlingStrictSpan.finish();//结束mFlingStrictSpan
mFlingStrictSpan = null;
}
}
// Remember where the motion event started
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
break;
}private void initScrollView() {
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();
} */
public void fling(int velocityY) {
if (getChildCount() > 0) {
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
if (mFlingStrictSpan == null) {
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");//获取StrictMode.Span类对象,Span为StrictMode的内部类。
}
postInvalidateOnAnimation();
}
} public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
// Continue a scroll or fling in progress
if (mFlywheel && !isFinished()) {
float oldVelocityX = mScrollerX.mCurrVelocity;
float oldVelocityY = mScrollerY.mCurrVelocity;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mScrollerX.fling(startX, velocityX, minX, maxX, overX);
mScrollerY.fling(startY, velocityY, minY, maxY, overY);
}mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling"); case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
deltaY -= mScrollConsumed[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y - mScrollOffset[1];
final int oldY = mScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
&& !hasNestedScrollingParent()) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
}
final int scrolledDeltaY = mScrollY - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
mLastMotionY -= mScrollOffset[1];
vtev.offsetLocation(0, mScrollOffset[1]);
mNestedYOffset += mScrollOffset[1];
} else if (canOverscroll) {
final int pulledToY = oldY + deltaY;
if (pulledToY < 0) {
mEdgeGlowTop.onPull((float) deltaY / getHeight(),
ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
} else if (pulledToY > range) {
mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
1.f - ev.getX(activePointerIndex) / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
}
if (mEdgeGlowTop != null
&& (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
postInvalidateOnAnimation();
}
}
}
break;原文:http://blog.csdn.net/qq_21004057/article/details/52985299