Android粘性容器ScrollView的简单实现方式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/MonaLisaTearr/article/details/80354400

      之前一直在QQ或者很多腾讯的游戏上看到过一个滚动的容器,滚到低或者滚到顶部后还可以往下拉一小短距离(一种粘性的效果),松开手后会自动弹回去,最近终于有这样的需求了,其实网上也有很多实现方案,直接copy过来用也是没有问题的,但是总得自己搞清楚原理,这样就算出了bug自己也能解决掉,下面分享下自己在实现过程中学到的一些东西!

一、scrollTo(),scrollBy(), getScrollX() 和 getScrollY()

        在开始写ScrollView之前,我们先了解View的这几个方法,大家可别小看这几个东西,如果之前没有了解过的话,第一次理解起来还是蛮绕的,感觉有点反人类思维,我们先看个小例子。

1、我们先看ScrollTo(),首先我们先正常去显示一张图片,然后再去调用ScrollTo()方法,看看前后的效果

初始化效果:


调用scrollTo(100,0)后:


        首先,我们先说一下,scrollTo(x,y)的第一个参数是作用于x轴上的,而第二个参数书是作用于y轴上的,这里很容易产生一个疑问,就是我们调用scrollTo(100,0)之后,感觉图片应该向x轴的正方向移动才对,为什么反了?

        其实scrollTo()方法并不是对View本身进行移动,而是对View中的内容进行移动,说白了就是对画布的移动,这里要说明的一点就是,我们的画布是无限大的,并不只我们看到的区域,也就是说,View限制了我们看到的区域,而我们的scrollTo()跟scrollBy()方法就是专门做把那些隐藏的区域显示在View中,或者说将那些显示在View中的区域隐藏起来。

        大家应该用过canvas的平移的方法,那么大家有没有想过,既然是平移,偏移量自然要有,有了偏移量那么自然就有参考点,所以说,canvas本身就拥有自己的坐标系,而这坐标系的原点刚好就是跟View初始化视图后的坐标原点是重合的。而scrollTo(100,0)的意义就是指将canvas上坐标为(100,0)的点显示到View的坐标原点上来,这就是为什么反方向移动了!

2、那么getScrollX()跟scrollTo()又有什么关系呢?

看scrollTo(),getScrollX的源码:

   public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
    public final int getScrollX() {
        return mScrollX;
    }

我们看到,x,y两个值会被保存到mScrollX、mScrollY中,而getScrollX()返回的就是他们。。关系不言而喻

3、那么接下来我们看下scrollBy()

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

这就清楚了吧,内部还是调用了scrollTo(),只不过每次都加上上一次保存的值,也就是说每掉一次都会在上一次的基础上进行移动

二、好了,我们为什么要说scrollTo()?因为这就是ScrollView的滚动原理,后面对临界值的判断需要用到,下面我们分析下实现思路

1、我们的需求是在ScrollView滚动到顶部的时候还能继续往下拉,或者滚动到底部后还能继续往上拉一段距离,那么这里就有两个问题第一个就是如何判断已经到顶部或者底部,第二个问题就是如何继续往下或者往下移动。

第一个问题:
我们前面已经知道,ScrollView的滚动原理是scrollTo()那么我们可以在getScrollY()上做手脚
getScrollY()==0//证明滚动到了顶部
getScrollY()==child.getMesureHeight() - getHeight()//当mScrollY的值等于孩子的高跟ScrollView的高的差值是,证明已经滚动到底部

第二个问题:
其实我们依然可以使用scrollTo()的方式让内容继续移动,但是那样的话会影响第一个问题中临界点的判断,无疑会增加很多判断,那么我们可以换一个方法,那就是使用子View的layout()方法对子View重新布局,这样也是能达到效果的。

2、至于复原,使用TranslateAnimation动画即可

三、下面是完整的代码,注释都有了

**
 * Created by server on 18-5-10.
 */

public class LazyScrollView extends ScrollView {
    /**
     * 子View
     */
    private View mChild;

    /**
     * 触摸点在Y轴方向上的坐标
     */
    private float mTouchY;

    /**
     * 用于保存子View开始调用layout重新布局前的位置信息
     * 确保松手后能恢复原来的位置
     */
    private Rect mOldRect = new Rect();

    /**
     * 用于标识当前动画是否正在执行
     */
    private boolean mAnimation = false;

    /**
     * 动画执行时间
     */
    private static final int DURATION = 200;


    private int mOffest;

    public LazyScrollView(Context context) {
        this(context, null);
    }

    public LazyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LazyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() > 0) {
            mChild = getChildAt(0);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mOffest = getMeasuredHeight()/2;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mChild == null) {
            return super.onTouchEvent(ev);
        } else {
            if (mAnimation) {
                return super.onTouchEvent(ev);
            }

            int action = ev.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mTouchY = ev.getY();
                    break;
                case MotionEvent.ACTION_UP:
                    mTouchY = 0;
                    if (!mOldRect.isEmpty()) {
                        runAnimation();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    //此处为了防止子View还没完全复原,又快速触摸屏幕时出现跳屏现象
                    final float preY = mTouchY == 0 ? ev.getY() : mTouchY;
                    //获取滑动后的位置
                    float moveY = ev.getY();

                    //计算当前位置与上一个位置的差值
                    int diffY = (int) (preY - moveY + 0.5f);

                    //更新位置信息
                    mTouchY = moveY;

                    /**
                     * 这里获取ScrollView的底部与子View底部的差值
                     * 如果这个差值等与getScrollY()的是那就证明已经滚动到了底部临界点
                     */
                    int offset = mChild.getMeasuredHeight() - getHeight();

                    /**
                     * 该值等于0,证明滚动到了顶部
                     */
                    int scrollY = getScrollY();
                    if (scrollY == 0 || scrollY == offset) {
                        if (mOldRect.isEmpty()) {
                            /**
                             * 保存原有的位置信息
                             */
                            mOldRect.set(mChild.getLeft(), mChild.getTop(), mChild.getRight(), mChild.getBottom());
                        }


                        //随着移动的增加,逐渐增加粘性效果
                        int df = 2;
                        if (Math.abs(mChild.getTop())>mOffest/3){
                            df = 3;
                        }else if (Math.abs(mChild.getTop())>mOffest/2){
                            df = 4;
                        }else if (Math.abs(mChild.getTop())>mOffest){
                            df = 5;
                        }

                        /**
                         * 重新进行布局操作
                         */
                        mChild.layout(mChild.getLeft(), mChild.getTop() - diffY/df, mChild.getRight(), mChild.getBottom() - diffY/df);
                    } else {
                        super.onTouchEvent(ev);
                    }
                    break;
                default:
                    break;
            }

        }
        return super.onTouchEvent(ev);
    }


    /**
     * 恢复View的位置时的动画实现
     */
    public void runAnimation() {
        TranslateAnimation ta = new TranslateAnimation(0, 0, 0, mOldRect.top - mChild.getTop());
        ta.setDuration(DURATION);
        ta.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                mAnimation = true;
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                mChild.clearAnimation();
                mChild.layout(mOldRect.left, mOldRect.top, mOldRect.right, mOldRect.bottom);
                mOldRect.setEmpty();//清除位置信息
                mAnimation = false;
            }
        });
        mChild.startAnimation(ta);
    }
}
ok下面看下效果图





猜你喜欢

转载自blog.csdn.net/MonaLisaTearr/article/details/80354400