自定义ScrollView实现弹性效果

来源:互联网 发布:台湾生活水平知乎 编辑:程序博客网 时间:2024/06/11 19:58

    弹性效果包括过度拉伸效果和反弹效果。

    实现思路请看这篇文章,我基于他的实现进行了一些优化。

    一、实现思路

    这个问题的本质是控制ScrollView装载的View(不妨叫它innerView)的显示位置。而innerView显示位置的改变可以通过两种方式实现。

  • 改变ScrollView的mScrollY
  • 调用innerView的layout(l, t, r, b)函数

    前者就是ScrollView实现正常滚动的方式。而后者就是我们拿来实现反弹效果的。也就是说,在ScrollView正常滚动达到极限的时候,我们调用innerView的layout(l, t, r, b)函数继续"滚动"innerView。手指抬起时,意味着一次滑动的结束,这个时候,我们要判断是否要"反弹"。所以这里面有两个关键问题。

  • 如何判断ScrollView的滚动已经达到极限
  • 如何判断是否需要反弹
    对于问题一,参照以下代码。

    public boolean needOverScroll(float deltaY) {        final int offset = innerView.getMeasuredHeight() - getHeight();        final float scrollY = getScrollY();        return (scrollY == 0 && deltaY > 0)|| (scrollY == offset && deltaY < 0);    }

    ScrollView的滚动已经达到极限分两种情况。

    第一种情况是手指往下滑动无法移动innerView了。我们知道,手指往下滑动,ScrollView的mScrollY减小,直至为0。ScrollView的mScrollY减为0之后(scrollY == 0),再向下滑动(deltaY > 0),mScrollY就不再变化了(不会变为负数),innerView也就不会移动了。这个时候,就需要我们调用innerView的layout(l, t, r, b)函数继续"滚动"innerView。

    第二种情况是手指往上滑动无法移动innerView了。我们知道,手指往上滑动,ScrollView的mScrollY增大,直至为innerView.getMeasuredHeight() - getHeight()(即mScrollY最大值为ScrollView装载的View的测量高度与ScrollView提供给其显示的高度的差值)ScrollView的mScrollY增大为innerView.getMeasuredHeight() - getHeight()之后(scrollY == offset),再向上滑动(deltaY < 0),mScrollY就不再变化了,innerView也就不会移动了。这个时候,也需要我们调用innerView的layout(l, t, r, b)函数继续"滚动"innerView。

   这里相对于我参考博文的那种实现,增加了对deltaY的考虑。这么做,可以避免本可以利用ScrollView的本身滚动的时候,使用layout(l, t, r, b)进行滚动。是对初始时就向上滑动的情况的优化。如果不考虑deltaY的话,一开始向上滑动也会调用layout(l, t, r, b)移动布局。虽然根据打印的日志,只移动了几个像素(之后因为mScrollY不再为0就不再调用layout(l, t, r, b)),但是既然能避免还是应该避免。

    对于问题二,参照以下代码。

    public boolean needRebound() {        return getInnerViewActualTop() > 0 || getInnerViewActualBottom() < getHeight();    }    public int getInnerViewActualTop(){        return  innerView.getTop() - getScrollY();    }    public int getInnerViewActualBottom(){        return  innerView.getBottom() - getScrollY();    }
    是否需要反弹也分两种情况

    第一种情况是需要向上反弹,即getInnerViewActualTop() > 0。

    第二种情况是需要向下反弹,即getInnerViewActualBottom() < getHeight()。

    说明一下getInnerViewActualTop()函数。我在调试的时候发现,使用原生的ScrollView时,无论你如何滚动,innerView.getTop()都是0。而视觉上,以ScrollView为参考系,innerView的上边界是不断变化的。(我推想,应该是在onDraw()函数中,getTop()-getScrollY()来确定上边界,所以getTop()可以一直不变,改变mScrollY就行了)所以,我写了getInnerViewActualTop()函数,来获得innerView相对于ScrollView视觉上的上边界。

    这样实现有一个好处,就是当你过度拉伸,不松手,再往回滑动,让innerView重新显示到合理的位置上(没有过度拉伸,没有空白的区域),抬手,innerView不会反弹。而我参考博文的那种实现,只要你过度拉伸过,抬手就会反弹,体验不是太好。

    二、源码

package com.example.ligang.demo_autopullrefreshlistview;import android.content.Context;import android.graphics.Rect;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.animation.TranslateAnimation;import android.widget.ScrollView;public class OverScrollView extends ScrollView {    private static final float OVER_SCROLL_RATIO = 0.5f;    private static final int REBOUND_DURATION = 200;    private View innerView;    private float lastY = -1;    private Rect originalRect = new Rect();    public OverScrollView(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    protected void onFinishInflate() {        if (getChildCount() > 0) {            innerView = getChildAt(0);        }    }    @Override    public boolean onTouchEvent(MotionEvent ev) {        if (innerView != null) {            handleTouchEvent(ev);        }        return super.onTouchEvent(ev);    }    public void handleTouchEvent(MotionEvent ev) {        final float currY = ev.getY();        if (lastY == -1) {            lastY = currY;        }        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                lastY = currY;                break;            case MotionEvent.ACTION_MOVE:                final float deltaY = (currY - lastY) * OVER_SCROLL_RATIO;                lastY = currY;                if (needOverScroll(deltaY)) {                    if (originalRect.isEmpty()) {                        originalRect.set(innerView.getLeft(), innerView.getTop(), innerView.getRight(), innerView.getBottom());                    }                    if (deltaY > 0) {                        innerView.layout(innerView.getLeft(), innerView.getTop() + (int) (Math.ceil(deltaY)), innerView.getRight(), innerView.getBottom() + (int) (Math.ceil(deltaY)));                    } else {                        innerView.layout(innerView.getLeft(), innerView.getTop() + (int) (Math.floor(deltaY)), innerView.getRight(), innerView.getBottom() + (int) (Math.floor(deltaY)));                    }                }                break;            case MotionEvent.ACTION_UP:                if (needRebound()) {                    rebound();                }                reset();                break;            default:                reset();                break;        }    }    private void reset() {        lastY = -1;    }    public void rebound() {        TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, innerView.getTop(), originalRect.top);        translateAnimation.setDuration(REBOUND_DURATION);        innerView.startAnimation(translateAnimation);        innerView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);        originalRect.setEmpty();    }    public boolean needRebound() {        return getInnerViewActualTop() > 0 || getInnerViewActualBottom() < getHeight();    }    public int getInnerViewActualTop(){        return  innerView.getTop() - getScrollY();    }    public int getInnerViewActualBottom(){        return  innerView.getBottom() - getScrollY();    }    public boolean needOverScroll(float deltaY) {        final int offset = innerView.getMeasuredHeight() - getHeight();        final float scrollY = getScrollY();        return (scrollY == 0 && deltaY > 0)|| (scrollY == offset && deltaY < 0);    }}





0 0
原创粉丝点击