从0開始写MyScrollView

上篇文章对ScrollView的详细实现进行了分析。本文依据上篇分析的结果。自己动手写一个ScrollView。

step1 尾随手指滑动,非常easy。重写2个函数就好了

简单的滑动,仅仅要重写onTouchEvent就能够了。然后我们须要内部的LinearLayout高度能够超出MyScrollView,那就在measure过程中进行处理,重写measureChildWithMargins就能够了。


/**
* Created by fish on 16/8/2.
*/
public class MyScrollView extends FrameLayout { private boolean mIsBeingDragged = false;
/**
* Position of the last motion event.
*/
private int mLastMotionY;
private int mTouchSlop; public MyScrollView(Context context) {
this(context, null);
} public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
} public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initScrollView();
} private void initScrollView() {
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
} //让内部的LinearLayout高度能够非常大非常大
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int delta = (int) (event.getY() - mLastMotionY);
if (mIsBeingDragged) {
scrollBy(0, -delta);
mLastMotionY= (int) event.getY();
} else if (Math.abs(delta) > mTouchSlop) {
mIsBeingDragged = true;
mLastMotionY= (int) event.getY();
scrollBy(0, -delta);
}
break; case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
break;
} return true;
}
}

step2 增加scrollbar

When you create a custom view you need to do the following to support

scrollbars:

- Enable the scrollbars

- Override the various compute*ScrollOffset, compute*ScrollRange(), etc. to

return sensible values

- Call awakenScrollbars() when you want to display the scrollbars (this is

called by the scroll methods in View as well)

http://markmail.org/thread/n7wv2rvgre3talba

要重写computeVerticalScrollOffset。computeVerticalScrollRange,初始化的时候调用setWillNotDraw(false);(为什么要setWillNotDraw(false)呢。由于默认ViewGroup是不绘制的,仅仅是个容器,可是这里要画滑块。所以得setWillNotDraw(false))

以上几点还不够。还得配置view的style属性。

从上篇文章我们知道ScrollView还配置了com.android.internal.R.attr.scrollViewStyle。 那我们怎样增加这个默认的style呢?我们知道这个style本质上是Widget.ScrollView,所以能够这样, style=”@android:style/Widget.ScrollView”非常关键,直接把style指定。

跟自己定义属性相关的知识能够參考http://blog.csdn.net/lmj623565791/article/details/45022631。写的非常好。

    <com.fish.myscrollviewpractise.MyScrollView
style="@android:style/Widget.ScrollView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"> <LinearLayout
android:id="@+id/linear1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"> </LinearLayout>
</com.fish.myscrollviewpractise.MyScrollView>

好了,此时scrollbar已经有了

话说回来。我们有必要搞清楚,为什么这样子就有scrollbar了

先看下scrollbar是什么时候调用的,调用图例如以下

从0開始写MyScrollView-LMLPHP

//View#onDrawScrollBars
scrollBar.setParameters(computeVerticalScrollRange(),
computeVerticalScrollOffset(),
computeVerticalScrollExtent(), true);

在view的onDrawScrollBars内部。须要setParameters。此时调用computeVerticalScrollRange和computeVerticalScrollOffset。这2个函数,我们进行重写。

    @Override
protected int computeVerticalScrollOffset() {
// LogUtil.fish("computeVerticalScrollOffset");
//这么写是考虑了OverScroller的情况
return Math.max(0, super.computeVerticalScrollOffset());
} @Override
protected int computeVerticalScrollRange() {
// LogUtil.fish("computeVerticalScrollRange");
final int count = getChildCount();
final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
if (count == 0) {
return contentHeight;
} int scrollRange = getChildAt(0).getBottom();
final int scrollY = getScrollY();
final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
// if (scrollY < 0) {
// scrollRange -= scrollY;
// } else if (scrollY > overscrollBottom) {
// scrollRange += scrollY - overscrollBottom;
// } return overscrollBottom;
}

此时有一个问题不太理解,为什么滚动停止了。滚动栏就消失了?答案在下边,state会变为ScrollabilityCache.OFF,就不会仅仅滚动栏了。


protected final void onDrawScrollBars(Canvas canvas) {
// scrollbars are drawn only when the animation is running
final ScrollabilityCache cache = mScrollCache;
if (cache != null) { int state = cache.state; if (state == ScrollabilityCache.OFF) {
//滚好了就会走到这里。那就不调用 onDrawVerticalScrollBar,所以不绘制滚动栏
return;
}
。。。
scrollBar.setParameters(computeHorizontalScrollRange(),
![]() computeHorizontalScrollOffset(),
computeHorizontalScrollExtent(), false);
。 。 。
onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom);
。。。

step3 滚完不要立马停下来,依据惯性再滚一会

速度达到一定程度。才会有惯性滚动,所以我们要检測速度,增加VelocityTracker。假设不熟悉VelocityTracker能够參考VelocityTracker

我们增加了

private VelocityTracker mVelocityTracker;

private Scroller mScroller;

在onTouchevent内有例如以下代码

     case MotionEvent.ACTION_UP:

                if (mIsBeingDragged) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, initialVelocity > 0 ? -300 : 300, 4000);
invalidate();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
    private void endDrag() {
mIsBeingDragged = false;
recycleVelocityTracker();
}

step4 Scroller改为OverScroller

依据官方建议把Scroller改为OverScroller,增加fling代码。

看下边代码,把overY设置为height / 2。

overY代表能够超出边界多大距离,height / 2事实上这是比較大的一个值,滑的时候会导致超过边界较多距离。而原生是ScrollView不会超过边界非常多距离,这是为什么?

假设我们想要超过边界的距离小一点全然能够把这个值改小,比方改为100,这个地方写height / 2我也认为非常奇怪,暂且无论。

 mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height / 2);

step5 滚动的时候考虑边界,增加onScrollChanged

之前,我们直接用scrollTo,没有考虑边界的问题。

此时其有用overScrollBy比較合适。overScrollBy()会考虑边界以及over区域。

overScrollBy()是view的方法。会回调onOverScrolled()。所以我们还须要重写onOverScrolled().onOverScrolled(int scrollX, int scrollY,

boolean clampedX, boolean clampedY)这个函数是在overScrollBy内部调用的,overScrollBy会依据边界值以及over值计算出合适的scrollX和scrollY,而clampedX和clampedY代表着scrollX和scrollY的值是否被裁剪过(超出上下限就会被裁剪),假设被裁剪过overScrollBy的返回值就是true。否则就是false。

主要代码例如以下所看到的:

   @Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) { int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY(); if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); } postInvalidate();
}
} @Override
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY) {
// Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {
final int oldX = getScrollX();
final int oldY = getScrollY();
setScrollX(scrollX);
setScrollY(scrollY);
// invalidateParentIfNeeded();
//源代码里有这句,可是我认为不是必需写。 onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
if (clampedY) {
mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange());
}
} else {
super.scrollTo(scrollX, scrollY);
} awakenScrollBars();
}

主要解释3点,

第一。onOverScrolled()的2个分支是怎么回事?普通滑动调用的是下边super.scrollTo(scrollX, scrollY);fling走的是上边,假设超出边界须要用mScroller.springBack来复位。

第二,onOverScrolled里面为什么调用awakenScrollBars(),这句话的作用是要求绘制的时候加上scrollBar,曾经我们不写这句话是由于scrollTo()方法内部包括了这句话

第三,onOverScrolled里面有这句话onScrollChanged。事实上是不是必需的,由于在computeScroll是会调用的。所以反复了。可是呢,写这个也有一点优点,那就是我们监控onScrollChanged的时候,假设发现同样的值出现了2次,那我们就知道这是出于惯性滑动的状态(fling)

step6 move事件也用overScrollBy处理

这是为了解决一个问题,曾经拉到顶部了,还能够继续下拉

            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 (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true; //把deltaY弄小一点,这事实上无所谓的
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y; // final int oldY = getScrollY();
final int range = getScrollRange();
// final int overscrollMode = getOverScrollMode(); // Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollBy(0, deltaY, 0, getScrollY(), 0, range, 0, mOverscrollDistance, true)) {
//被裁剪了说明滑到头了。此时清除mVelocityTracker,是为了up的时候计算不出速度。速度为0,就没有fling了
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
} }

step7 边缘拉的时候增加晕影效果

ScrollView边缘拉的时候有晕影效果。这是怎么做到的呢?

EdgeEffect。增加此效果,主要四步

第一步,在View初始化的时候。会调用setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS);

我们重写此函数,在内部构造mEdgeGlowTop和mEdgeGlowTop

  //在view的init里面被调用
@Override
public void setOverScrollMode(int mode) {
if (mode != OVER_SCROLL_NEVER) {
if (mEdgeGlowTop == null) {
Context context = getContext();
mEdgeGlowTop = new EdgeEffect(context);
mEdgeGlowBottom = new EdgeEffect(context);
}
} else {
mEdgeGlowTop = null;
mEdgeGlowBottom = null;
}
super.setOverScrollMode(mode);
}

第二步,在computeScroll内增加mEdgeGlowTop.onAbsorb。onAbsorb是初始化一堆參数为后面的draw做准备

    @Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) { int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY(); if (oldX != x || oldY != y) {
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); if (canOverscroll) {
if (y < 0 && oldY >= 0) {
mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
} else if (y > range && oldY <= range) {
mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
}
} } postInvalidate();
}
}

第三步,重写onDraw()。增加绘制mEdgeGlowTop和mEdgeGlowBottom的代码。此处代码抄自ScrollView。

第四步,在endDrag的时候进行release。这是和onAbsorb相应的。清除各种数据

        if (mEdgeGlowTop != null) {
mEdgeGlowTop.onRelease();
mEdgeGlowBottom.onRelease();
}

第五步,在onTouchevent的move事件里,对下拉。上拉做响应,调用mEdgeGlowTop.onPull,呈现出拖拽效果

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();
}
}

step8 增加onInterceptTouchEvent

这部分代码不难理解,可是实际调用的机会比較少,主要实现2个功能。child处理了down,我能够抢个move(假设够大的话);配合onTouchevent实现fling时点击停止。

  @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/ /*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
} /*
* Don't try to intercept touch if we can't scroll anyway.
*/
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
} switch (action & MotionEvent.ACTION_MASK) {
//down事件child处理的。我有权截获move事件
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/ /*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
} final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
} final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev); final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
} //配合完毕fling时。点击停止滚动
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
} /*
* Remember location of down touch.
* ACTION_DOWN always refers to pointer index 0.
*/
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged.
*/
mIsBeingDragged = !mScroller.isFinished(); break;
} case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
break; } /*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged; }

step9 增加cancel事件处理。增加requestDisallowInterceptTouchEvent

cancel事件。就是收到前驱事件,后边的事件被parent抢走了,此时触发cancel,进行重置处理。

requestDisallowInterceptTouchEvent就是请求parent放过事件,都给我吧。

相关代码例如以下

    @Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept) {
recycleVelocityTracker();
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
          //onTouchEvent
//假设cancel了就结束滚动
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged && getChildCount() > 0) {
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
mActivePointerId = INVALID_POINTER;
endDrag();
}

OK。此时大功告成,一个可用的ScrollView已经完毕了,功能有滚时显示滑块。普通滑动。惯性滑动,fling时点击停止,滚动能够超出边界并回弹,到达边界是有晕影效果等功能。

github地址

05-11 13:15