首页 > 代码库 > 领悟自定义风采,ScrollView源码完全解析
领悟自定义风采,ScrollView源码完全解析
今天给大家带来这篇源码解读,首先很感谢大家能"赏脸。本文秉着思路清晰,细致分析源码脉络。从根本上带领大家学会自定义控件和分析源码,学会举一反三。"自定义,何等熟悉的名词。到底,它有多深奥,其实不然,咱们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对象回收 }
关于4个参数,在这给大家解释一下。
- Context:上下文,用来获取当前的主题和资源。
- AttributeSet :属性集接口对象。在Attributeset接口里可以含有各种获取资源的方法。
- defStyleAttr:属性样式XML文件。通过这个文件可以设置该控件各种自定义属性。
- defStyleRes:资源标志,当XML样式文件找不到或者传入的是第三个参数defStyleAttr传入的是0的时候,将会使用默认资源。我们可以设置为0,这样就不会去寻找默认的XML文件了。
在构造方法里面,首先通过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));
接下来,我们看下TypedArray属性集对象中的getBoolean方法是怎么样的。我们抽丝剥茧一层一层地来看。这个方法主要是为了检测布局文件是否超出屏幕
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 = http://www.mamicode.com/mData;>
先解释一下传入的2个参数。第一个是属性索引地址,意思为重新加载时的索引地址,第二个参数是属性找不到或者属性的地址不能强制为整型时候将会默认返回false,该方法在重新加载XML资源文件的时候会被调用。实质上传入的 R.styleable.ScrollView_fillViewport 的索引是-2001703,所以在返回的时候是返回的true。我们看到上面对于type的值的判断。当type等于TYPE_NULL(值为0)的时候返回false。当type的值在16到31之间,返回索引是否等于data[index]的值,AssetManager.STYLE_DATA等于0。mDate数组是TypedArray的构造方法的传入参数之一。type元素就是数组data中的某个元素。其元素位置就是索引位置的数组元素。接下来,我们看到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中主要对子控件进行了精确测量。而自身的高宽不做太多处理。
- MeausureSpec.UNSPECIFIED(未指定模式,父类不指定子布局的宽高)
- MeasureSpec.EXACTLY(精确模式,在不指定确切宽高时,按照父布局进行宽高设置)
- MeasureSpec.AT_MOST(至多模式,子类宽高最多达到父类指定宽高,较为少用)
在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的宽高减去子控件的内边距。这一点需要我们注意。五:初始布局
初始化ScrollView的位置布局。在该方法中咱们应该注意一个变量mIsLayoutDirty ,它的值初始化时true的,为什么呢。这个变量的字面意思是是否弄脏,其实就是标志布局是否发送变化。这个变化有很多,比如位置变化,状态变化,焦点变化。但是这些变化有hierarchy会自动监控并跟踪它们。@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; } }
我们看到上面的代码。mChildToScrollTo是获取了焦点状态的子View。对于mChildToScrollTo这个View,意思是存在焦点状态的子view,什么意思呢?意思就是该View获取了焦点。我们可以看到初始化是null的,也就是初始化的子View是不存在焦点事件的。。在初始化布局的时候会进行检查子View是不是占据着焦点状态。如果存在并且占用而且该View是ScrollView的子View,就会滚动到此时占据焦点的View。再判断了是否存在占据焦点的子View后,再判断是否已经布局完成。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]; } }; }
因为其超类实现了序列化接口。所以可以实现对mScrollY变量的存储。所以我们实现当前属性的序列化的步骤为:
- 新建一个类继承BaseSavedInstance类,在其构造方法中进行反序列话读取属性,重写其writeToParcel方法,写入属性。
- 重写当前View的onSaveInstanceState方法。当退出当前界面时候,保存此时属性
- 重写当前View的onRestoreInstanceState方法。在恢复的时候进行得到当前属性。
@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; }
因为SavedState构造方法传入的是序列化接口对象。所以可以通过调用父类的onSaveInstanceState方法获取接口对象。
六:事件监听
@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; }
上面我们可以看到首先,我们需要得到计算速度的VelocityTracker对象,该对象可以用来跟踪和计算触摸屏事件。如何计算的呢?
- 创建initVelocityTrackerIfNotExists方法,通过obtain方法复制得到新的VelocityTracker对象
- 通过addMovement方法把MotionEvent对象添加到VelocityTracker中。
- 通过computeCurrentVelocity方法计算当前的速率
private void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } }上面我们说过,通过obtain会将已经存在的VelocityTracker对象复制一个。
private void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } }
接下来我们看到computeCurrentVelocity方法。
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
触屏事件之按下时处理(ACTION_DOWN):
{ 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; }
上面我们可以看到首先通过OverScroller的isFinish方法判断是否滑动完毕。OverScroller类是一个滚动封装类,它包含了滚动事件的各种操作。它比Scroller类更完善,所以我建议大家写自定义控件的时候都采用OverScroller类。这个类在哪里得到的呢?我们可以看到ScrollView中的初始化ScrollView的方法。
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(); }
通过new一个OverScroller的方法就可以得到该对象了。在ScrollView还没有滚动完的时候,允许父节点拦截事件。当滑动完毕之后,然后设置为不允许拦截。
*/ 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); }
解释下,velocityX和velocityY分别是X和Y滑动的速度。为什么fling滑动会产生非平滑的效果呢?原因就在于设置了滑动的最大范围和最终位置。这样就会产生一个缓冲的效果。
通过下面代码我们可以得到该对象。StrictMode是性能诊断类,是从Android2.3开始引入的类。
mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
触屏事件之移动时处理(ACTION_MOVE):
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;
领悟自定义风采,ScrollView源码完全解析