庖丁解牛之ScrollView

您所在的位置:网站首页 暂停这是怎么了 庖丁解牛之ScrollView

庖丁解牛之ScrollView

2023-12-23 11:43| 来源: 网络整理| 查看: 265

新媒体管家

点击上方“程序员大咖”,选择“置顶公众号”

关键时刻,第一时间送达!

前言

ScrollView可以说是Android里最简单的滑动控件,但是其中也蕴含了很多的知识点。今天尝试通过ScrollView的源码来了解ScrollView内部的细节。本文在介绍ScrollView时会忽略以下内容:嵌套滑动,崩溃保存,Accessibility。 

ScrollView是一种控件,继承自 FrameLayout,他的子控件远远大于ScrollView本身,所以ScrollView展现出来的只有子控件的一部分,通过滑动的形式来呈现出子控件的内容。

基本用法与功能剖析

先来回顾下ScrollView的基本用法,超级简单。我们通常在ScrollView内部放一个LinearLayout,然后在LinearLayout放各种元素,ScrollView滚动时就可以看到这些元素。附带一句,LinearLayout的width通常是match_parent(也可以是warp_content,这里有个坑,我们暂且不管,后面会提)。

   

       

       

   

从测试的角度来看下,ScrollView的功能是怎么样的?

首先滑动的时候有2种情况,如果滑的慢,ScrollView的滑动会随着手指的离开而停止(简单滑动);如果滑的快,在手指离开后,ScrollView还会再滑一段时间(这段时间内的状态我们称为fling)。

第二,fling的时候,手指碰一下,就立刻停止fling 

第三,ScrollView到顶部的时候,下拉有光影效果。底部同理

子窗口大小超出父窗口

我们知道,一般情况下子view都是没有父view大的,因为measure的时候子view的大小会受到父view的制约,那什么情况下,子view会超出父view大小呢?

要想子view超出父view大小,大概有2种方式,一种是父view对子view的要求为MeasureSpec.EXACTLY,子view的size设置为某个固定值,另一种是父view对子view的要求为UNSPECIFIED,然后子view就可以随便搞了。可以参考getChildMeasureSpec代码就能大概看出来。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

        int specMode = MeasureSpec.getMode(spec);

        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;

        int resultMode = 0;

        switch (specMode) {

        // Parent has imposed an exact size on us

        case MeasureSpec.EXACTLY:

            if (childDimension >= 0) {

               //此时为case1,resultSize可能大于specSize

                resultSize = childDimension;

                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                // Child wants to be our size. So be it.

                resultSize = size;

                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                // Child wants to determine its own size. It can't be

                // bigger than us.

                resultSize = size;

                resultMode = MeasureSpec.AT_MOST;

            }

            break;

        // Parent has imposed a maximum size on us

        case MeasureSpec.AT_MOST:

            if (childDimension >= 0) {

                // Child wants a specific size... so be it

                resultSize = childDimension;

                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                // Child wants to be our size, but our size is not fixed.

                // Constrain child to not be bigger than us.

                resultSize = size;

                resultMode = MeasureSpec.AT_MOST;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                // Child wants to determine its own size. It can't be

                // bigger than us.

                resultSize = size;

                resultMode = MeasureSpec.AT_MOST;

            }

            break;

        // Parent asked to see how big we want to be

        case MeasureSpec.UNSPECIFIED:

        //此时为case2,parent不做限制,大小就可以乱来了

            if (childDimension >= 0) {

                // Child wants a specific size... let him have it

                resultSize = childDimension;

                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                // Child wants to be our size... find out how big it should

                // be

                resultSize = 0;

                resultMode = MeasureSpec.UNSPECIFIED;

            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                // Child wants to determine its own size.... find out how

                // big it should be

                resultSize = 0;

                resultMode = MeasureSpec.UNSPECIFIED;

            }

            break;

        }

        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

    }

EXACTLY+固定值

对于case1,我们举个例子,可以这么写

   

此时TextView的就比parent的大,这是一种方式让子view超出了父view的大小。 

ScrollView重写了android.widget.ScrollView#measureChildWithMargins

UNSPECIFIED

而ScrollView的child能比ScrollView本身还大,用的是第二种方法,量的时候把specMode改为UNSPECIFIED,具体代码如下所示,关键看这句 

final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( 

MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED); 

直接把childHeightMeasureSpec变为了MeasureSpec.UNSPECIFIED,此时parent传过来的高度其实已经毫无意义了。而子view的高度一般写为wrap_conten,就可以非常大了。

   @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,

                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

                        + widthUsed, lp.width);

        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(

                MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    }

嵌套滑动(NestedScrolling)

本文虽然不介绍嵌套滑动,但是嵌套滑动的相关代码频繁出现在onTouchevent里面,所以还是要简单说下。

NestedScrolling 提供了一套父 View 和子 View 滑动交互机制。要完成这样的交互,父 View 需要实现 NestedScrollingParent 接口,而子 View 需要实现 NestedScrollingChild 接口。 

更多知识可以参考 

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0822/3342.html 

https://segmentfault.com/a/1190000002873657

ScrollView默认支持了嵌套滑动,既可作为父view,也可作为子view 

我们在看代码的时候暂时忽略和嵌套滑动相关的(带nest的函数)

简单滑动

手指在屏幕上滑动会触发ACTION_DOWN,ACTION_MOVE, ACTION_MOVE没人处理,就交给ScrollView处理。 

这里我们看到个变量mIsBeingDragged,这个代表的是ScrollView是否正在被拖拽,手指抬起,mIsBeingDragged就会变为false,初始化的时候也为false。看L4可知如果deltaY(滑动的距离)超过mTouchSlop,那就表示触发了ScrollView的滑动,mIsBeingDragged 置为true,mTouchSlop是一个固定阈值。然后会执行L17 overScrollBy进行滚动。

            case MotionEvent.ACTION_MOVE:

                 ...

                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 (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)

                            && !hasNestedScrollingParent()) {

                        // Break our velocity if we hit a scroll barrier.

                        mVelocityTracker.clear();

                    }

overScrollBy这是View的方法,会触发onOverScrolled回调。此时只是普通的滑动,所以走L18,就是调super.scrollTo,根据手指滑动的距离进行移动。非常简单。

   @Override

    protected void onOverScrolled(int scrollX, int scrollY,

            boolean clampedX, boolean clampedY) {

        // Treat animating scrolls differently; see #computeScroll() for why.

        if (!mScroller.isFinished()) {

             //fling走这里

            final int oldX = mScrollX;

            final int oldY = mScrollY;

            mScrollX = scrollX;

            mScrollY = scrollY;

            invalidateParentIfNeeded();

            onScrollChanged(mScrollX, mScrollY, oldX, oldY);

            if (clampedY) {

                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());

            }

        } else {

              //普通的滑动走这里

            super.scrollTo(scrollX, scrollY);

        }

        awakenScrollBars();

    }

fling(惯性滑动)

怎么实现手指离开之后,还能滑动一段距离呢? 

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)) {

                        flingWithNestedDispatch(-initialVelocity);

                    } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,

                            getScrollRange())) {

                        postInvalidateOnAnimation();

                    }

                    mActivePointerId = INVALID_POINTER;

                    endDrag();

                }

                break;

只要速度超过mMinimumVelocity,那就会调用flingWithNestedDispatch(),实际上就是调用mScroller.fling()。mScroller.fling是一个OverScroller,OverScroller的相关知识可以参考 View的滚动与Scroller

fling的时候点击一下,立刻停止

这是怎么做到的?总的来说,是通过onInterceptTouchEvent和onTouchEvent的配合,调用 mScroller.abortAnimation();来停止滚动的。 

分2种case来讨论:

case1 ScrollView内部的LinearLayout的width为match_parent

此时随便点一下就点到了LinearLayout内部。 

先来看fling时的状态,此时手指已经抬起,endDrag()被调用,mIsBeingDragged为false。此时点击一下,会到onInterceptTouchEvent()方法。此时在LinearLayout内部,所以inChild返回true,会走到mIsBeingDragged = !mScroller.isFinished();,因为在fling,所以mScroller.isFinished()必定false,所以mIsBeingDragged为true,那么down事件就被拦截起来了。 

下一步会走到onTouchEvent里。

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

                if (mIsBeingDragged && mScrollStrictSpan == null) {

                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");

                }

                startNestedScroll(SCROLL_AXIS_VERTICAL);

                break;

            }

再来看onTouchEvent如何处理down事件,有下面这段代码,如果在fling,那么立刻终止,达到目的。

      /*

                 * 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 = null;

                    }

                }

case2 ScrollView内部的LinearLayout的width较小,点击到LinearLayout外部

此时inChild返回false,那么onInterceptTouchEvent返回false,不拦截。但是注意,此时点到了LinearLayout外部,那么这个down事件,没有child去处理,所以还是交给ScrollView来处理,还是会走到onTouchEvent内,一样会调用mScroller.abortAnimation();方法

R.attr.scrollViewStyle是什么

在构造函数里,我们可以看到这么一段代码,默认给ScrollView,配置了scrollViewStyle,这有什么意义呢?其实就是设置了scrollbars和fadingEdge为vertical。看下边代码

  public ScrollView(Context context, AttributeSet attrs) {

        this(context, attrs, com.android.internal.R.attr.scrollViewStyle);

    }

attrs.xml内有

themes.xml内有

@style/Widget.ScrollView

styles.xml内有

   

        vertical

        vertical

   

其他

因为用了OverScroller,所以mScrollY可能是负值;

Scrollview到顶部的时候下拉的晕影效果,主要是用EdgeEffect实现;

来自:CSDN-litefish

http://blog.csdn.net/litefish/article/details/52127930

程序员大咖整理发布,转载请联系作者获得授权

【点击成为Python大神】



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3