文档章节

Android 自定义View EdgeEffect效果 ----ScrollView部分源代码剖析

adgkns
 adgkns
发布于 2014/02/25 00:51
字数 1399
阅读 1648
收藏 44

EdgeEffect 提供了一种方式去画可滑动View组件的过度滑动效果。EdgeEffect的接口不多,只有6个接口。下面我们用ScrollView源码来分析一下如何实现过度滑动的晕影效果.

ScrollView实现晕影效果,实际上是通过下面两个EdgeEffect

private EdgeEffect mEdgeGlowTop; //滑动到顶时,出现的晕影效果
    private EdgeEffect mEdgeGlowBottom; //滑动到底时,出现的晕影效果
从ScrollView的代码中可以看到OverScrollMode会对是否有EdgeEffect有影响,当OverScrollMode为OVER_SCROLL_NEVER的时候,是没有EdgeEffect效果的。
@Override
    public void setOverScrollMode(int mode) {
        if (mode != OVER_SCROLL_NEVER) {//当mode不为OVER_SCROLL_NEVER的时候,创建EdgeEffect实例。
            if (mEdgeGlowTop == null) {
                Context context = getContext();
                mEdgeGlowTop = new EdgeEffect(context); //创建EdgeEffect实例
                mEdgeGlowBottom = new EdgeEffect(context);//创建EdgeEffect实例
            }
        } else {
            mEdgeGlowTop = null;
            mEdgeGlowBottom = null;
        }
        super.setOverScrollMode(mode);
    }

每次画的时候,每次画的时候都会调用draw。

@Override
    public void draw(Canvas canvas) {
        super.draw(canvas); //画ScrollView
        if (mEdgeGlowTop != null) {
            final int scrollY = mScrollY;
            if (!mEdgeGlowTop.isFinished()) {//画滑到顶的晕影效果
                final int restoreCount = canvas.save();
                final int width = getWidth() - mPaddingLeft - mPaddingRight;

                canvas.translate(mPaddingLeft, Math.min(0, scrollY));
                mEdgeGlowTop.setSize(width, getHeight());
                if (mEdgeGlowTop.draw(canvas)) {
                    postInvalidateOnAnimation();
                }
                canvas.restoreToCount(restoreCount);
            }
            if (!mEdgeGlowBottom.isFinished()) {//画滑动到底的晕影效果
                final int restoreCount = canvas.save();
                final int width = getWidth() - mPaddingLeft - mPaddingRight;
                final int height = getHeight();

                canvas.translate(-width + mPaddingLeft,
                        Math.max(getScrollRange(), scrollY) + height);
                canvas.rotate(180, width, 0);
                mEdgeGlowBottom.setSize(width, height);
                if (mEdgeGlowBottom.draw(canvas)) {
                    postInvalidateOnAnimation();
                }
                canvas.restoreToCount(restoreCount);
            }
        }
    }

但仅仅于此还是不行的,怎么判断滑动到边上呢?


@Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);

        final int action = ev.getAction();

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    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 = null;
                    }
                }

                // Remember where the motion event started
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                break;
            }
            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; //sh 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;

                    final int oldX = mScrollX;
                    final int oldY = mScrollY;
                    final int range = getScrollRange();//ScrollView的高度
                    final int overscrollMode = getOverScrollMode();
                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                    if (overScrollBy(0, deltaY, 0, mScrollY,
                            0, range, 0, mOverscrollDistance, true)) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }
                    onScrollChanged(mScrollX, mScrollY, oldX, oldY);

                    if (canOverscroll) {
                        final int pulledToY = oldY + deltaY;
                        if (pulledToY < 0) {
                            mEdgeGlowTop.onPull((float) deltaY / getHeight()); //ScrollView滑动到顶 if (!mEdgeGlowBottom.isFinished()) {
                                mEdgeGlowBottom.onRelease();
                            }
                        } else if (pulledToY > range) {//ScrollView滑动到底
                            mEdgeGlowBottom.onPull((float) deltaY / getHeight());
                            if (!mEdgeGlowTop.isFinished()) {
                                mEdgeGlowTop.onRelease();
                            }
                        }
                        if (mEdgeGlowTop != null
                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                            postInvalidateOnAnimation();//使ViewRoot重新去画
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);

                    if (getChildCount() > 0) {
                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                            fling(-initialVelocity);
                        } else {
                            if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                                    getScrollRange())) {
                                postInvalidateOnAnimation();
                            }
                        }
                    }

                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;//重置EdgeEffect
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0) {
                    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
                        postInvalidateOnAnimation();
                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();//重置EdgeEffect
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN: {
                final int index = ev.getActionIndex();
                mLastMotionY = (int) ev.getY(index);
                mActivePointerId = ev.getPointerId(index);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }
        return true;
    }

从上面可以看出,ScrollView计算滑动到顶部,实际上是计算之前的mScrollY+移动距离是否大于0.

计算滑动到底部,实际上是计算之前的mScrollY + 移动距离是否大于ScrollView的滑动到底时Child的偏移量

private int getScrollRange() {
        int scrollRange = 0;
        if (getChildCount() > 0) {
            View child = getChildAt(0);//ScrollView仅有一个Child
            //计算滑动到底时候Child的偏移量
            scrollRange = Math.max(0,
                    child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
        }
        return scrollRange;
    }

画了个简单的图示,方便理解。
private void endDrag() {
        mIsBeingDragged = false;

        recycleVelocityTracker();

        if (mEdgeGlowTop != null) {
            mEdgeGlowTop.onRelease(); //重置滑到顶的EdgeEffect
            mEdgeGlowBottom.onRelease();//重置滑到底的EdgeEffect
        }

        if (mScrollStrictSpan != null) {
            mScrollStrictSpan.finish();
            mScrollStrictSpan = null;
        }
    }

上面基本已经实现了EdgeEffect效果,以及缓慢拖动的时候,晕影的渐变效果。为了使晕影效果更加平滑,Android在computeScroll中做了一些处理。


@Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            // This is called at drawing time by ViewGroup.  We don't want to
            // re-show the scrollbars at this point, which scrollTo will do,
            // so we replicate most of scrollTo here.
            //
            //         It's a little odd to call onScrollChanged from inside the drawing.
            //
            //         It is, except when you remember that computeScroll() is used to
            //         animate scrolling. So unless we want to defer the onScrollChanged()
            //         until the end of the animated scrolling, we don't really have a
            //         choice here.
            //
            //         I agree.  The alternative, which I think would be worse, is to post
            //         something and tell the subclasses later.  This is bad because there
            //         will be a window where mScrollX/Y is different from what the app
            //         thinks it is.
            //
            int oldX = mScrollX;
            int oldY = mScrollY;
            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(mScrollX, mScrollY, 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());
                    }
                }
            }

            if (!awakenScrollBars()) {
                // Keep on drawing until the animation has finished.
                postInvalidateOnAnimation();
            }
        } else {
            if (mFlingStrictSpan != null) {
                mFlingStrictSpan.finish();
                mFlingStrictSpan = null;
            }
        }
    }


到此就已经结束了。

总结一下:

1.对于每一个需要画OverScroll晕影效果的边,都需要定义自己的EdgeEffect

2.在接收到ACTION_MOVE的event时,判断是否已经滑动到边上,如果是就调用EdgeEffect的onPull方法。

如果调用了onPull,调用invalidate()或者postInvalidateOnAnimation()去触发重新去画。

3.在收到ACTION_MOVE或者ACITION_CANCEL的时候,调用EdgeEffect的onRelease方法重置。

在调用onRelease方法后,调用invalidate()或者postInvalidateOnAnimation()去触发重新去画。

4.重写draw方法,在super.draw(canvas)之后调用EdgeEffect的draw方法。如果EdgeEffect没有finish.做旋转和平移的变换,然后调用EdgeEffect的setSize和draw方法。如果EdgeEffect的draw方法返回ture,调用invalidate()或者postInvalidateOnAnimation()去触发重新去画。

5.在失去Window Focus的时候,调用EdgeEffect的finish方法(AbsListView.java)。对于EdgeEffect的onAbsorb方法一般是在computeScroll中调用的。但具体还不是特别清楚,有知道的告诉我一下。









© 著作权归作者所有

adgkns
粉丝 15
博文 47
码字总数 16222
作品 0
广州
程序员
私信 提问
加载中

评论(2)

敲代码的NY
敲代码的NY
受教了,这两天我也在看ScrollView的源码,看了你的这边文章,我对ScrollView的源码又有了新的理解,感谢,另外想问一下你的图是用什么来画?
adgkns
adgkns
另外说明一下,前几天遇到一个问题,在有的机子上不会调用到draw方法,需要在构造函数中加setWillNotDraw(false),就可以了。
高仿美团app,浮动layout滑动到顶部悬停效果

做了个类似美团app的一个效果 当一个浮动layout的滑动到顶部时,这个浮动layout就悬停下来,当屏幕往下滑动时,浮动layout也跟着往下移动。 因此,我特意也写了一个:浮动layuot滑动到顶部悬...

谁带我去看看世界
2015/06/16
0
4
android常用组件之ScrollView

ScrollView在android中可以实现滚动视图,手机屏幕大小有限,当显示的内容较多时,滚动视图就派上用场了。 该实例主要是通过布局文件生成视图,只是演示ScrollView的使用,没有考虑整体美观效...

sometimesno1
2014/12/27
0
0
android 如何解决scrollTo无法执行

在Activity 的 onCreate() 方法(貌似在onStart和onResume也一样)中, 调用 mScrollView.scrollTo(0, 100); 是无效, 没有效果的. 找了半天, 终于在 http://stackoverflow.com/questions/32632...

西门好又多
2012/06/06
0
0
Android有用代码片段(三)

前两个已经到第四十个了,所以还得再开一篇,用于记录,以前文章:Android有用代码片段(二)、android有用代码片段,有需要的朋友可以去看一下。 四十一、数据库写入图片信息: 四十二、lis...

迷途d书童
2012/03/30
680
0
滚动到底部或顶部响应的ScrollView使用

本文介绍 滚动到底部或顶部响应(如加载更多)的ScrollView的使用 。下面介绍使用自定义的ScrollView来完成该功能的实例。 关于实现原理可见: 滑动到底部或顶部响应的ScrollView实现 本文可运...

Trinea
2013/05/30
1K
2

没有更多内容

加载失败,请刷新页面

加载更多

利用mybatis generator生成实体类、Mapper接口以及对应的XML文件

项目中通常会遇到数据的持久化,如果是采用mybatis的orm,就会涉及到生成xml的问题,刚好mybatis官网提供了这么个插件MyBatis Generator,效果简直是棒呆。 1. 首先需要在build.gradle文件中...

啊哈关关
今天
2
0
SpringSocial相关的知识点

使用SprigSocial开发第三方登录 核心类 ServiceProvider(AbstractOauth2ServiceProvider):主要负责实现server提供商(例如QQ,微信等共有的东西),默认实现类是AbstractOauth2ServiceProvider...

chendom
今天
4
0
Java并发之AQS详解

一、概述   谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!   类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源...

群星纪元
昨天
4
0
Fabric-sdk-java最新教程

Fabric Java SDK是Fabric区块链官方提供的用于Java应用开发的SDK,全称为Fabric-sdk-java,网上可用资料不多,本文列出了精心整理的针对Fabric Java SDK的最新精选教程。 如果希望快速掌握F...

汇智网教程
昨天
3
0
react 子组件监听props 变化

componentWillReceiveProps //已经被废弃 getDerivedStateFromProps// 推荐使用//如果条件不存在必须要返回null static getDerivedStateFromProps(props, current_stat...

一箭落旄头
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部