ScrollView、ListView剖析 - 上下拉伸回弹阻尼效果

0.881字数 581阅读 9044

实现上下回弹阻尼效果

通过继承 ScrollView,重新写其方法overScrollBy ,可轻松实现ScrollView的上下回弹效果,类似iOS系统中的阻尼拉伸效果。

public class OverScrollView extends ScrollView {
  
    public OverScrollView(Context context) {
        super(context);
    }

    public OverScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected boolean overScrollBy(
            int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {

        return super.overScrollBy(
                deltaX, deltaY, scrollX, scrollY,
                scrollRangeX, scrollRangeY, maxOverScrollX, 150,
                isTouchEvent);
    }
}

以上实现了垂直方向上的回弹拉伸效果,如果是水平ScrollView, 只需修改maxOverScrollX的值即可。

ScrollView有三个overscroll mode属性,表示允许回弹拉伸的时机:

/**
     * Always allow a user to over-scroll this view, provided it is a
     * view that can scroll.
     *
     * @see #getOverScrollMode()
     * @see #setOverScrollMode(int)
     */
    public static final int OVER_SCROLL_ALWAYS = 0;

    /**
     * Allow a user to over-scroll this view only if the content is large
     * enough to meaningfully scroll, provided it is a view that can scroll.
     *
     * @see #getOverScrollMode()
     * @see #setOverScrollMode(int)
     */
    public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1;

    /**
     * Never allow a user to over-scroll this view.
     *
     * @see #getOverScrollMode()
     * @see #setOverScrollMode(int)
     */
    public static final int OVER_SCROLL_NEVER = 2;

默认为OVER_SCROLL_IF_CONTENT_SCROLLS,您可根据需求在布局文件中声明overscroll mode:

<com.example.demo.demo.OverScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#e7e7e7"
    android:overScrollMode="always">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#352344">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="1920px"
            android:gravity="center"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:text="OverScrollView"/>

    </LinearLayout>

</com.example.demo.demo.OverScrollView>
上下拉伸回弹效果

实现top、bottom不同的拉伸距离

我们从overScrollBy方法下手,来看下ScrollView各层级之间的继承关系:

ViewViewGroupFrameLayoutScrollView

ViewGroupFrameLayoutScrollView并未重写overScrollBy方法,而是直接继承自ViewoverScrollBy方法。

下面我们在ScrollView的子类OverScrollView中重写overScrollBy方法,直接复制View中的overScrollBy方法内的实现:

package com.example.demo.demo;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.ScrollView;

public class OverScrollView extends ScrollView {

    private int mTopOverScrollDistance = 150;
    private int mBottomOverScrollDistance = 90;

    @Override
    protected boolean overScrollBy(
            int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {

        final int overScrollMode = getOverScrollMode();
        final boolean canScrollHorizontal =
                computeHorizontalScrollRange() > computeHorizontalScrollExtent();
        final boolean canScrollVertical =
                computeVerticalScrollRange() > computeVerticalScrollExtent();
        final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
        final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

        int newScrollX = scrollX + deltaX;
        if (!overScrollHorizontal) {
            maxOverScrollX = 0;
        }

        int newScrollY = scrollY + deltaY;
        if (!overScrollVertical) {
            mTopOverScrollDistance = 0;
            mBottomOverScrollDistance = 0;
        }

        // Clamp values if at the limits and record
        final int left = -maxOverScrollX;
        final int right = maxOverScrollX + scrollRangeX;
        final int top = -mTopOverScrollDistance;
        final int bottom = mBottomOverScrollDistance + scrollRangeY;

        boolean clampedX = false;
        if (newScrollX > right) {
            newScrollX = right;
            clampedX = true;
        } else if (newScrollX < left) {
            newScrollX = left;
            clampedX = true;
        }

        boolean clampedY = false;
        if (newScrollY > bottom) {
            newScrollY = bottom;
            clampedY = true;
        } else if (newScrollY < top) {
            newScrollY = top;
            clampedY = true;
        }

        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

        return clampedX || clampedY;
    }


    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
    }
}

但是需要将maxOverScrollY用声明的top拉伸大小mTopOverScrollDistance和bottom的拉伸大小mBottomOverScrollDistance做如上替换。

设置top、bottom仅其一支持拉伸回弹

我们知道,一旦设置了overScrollMode,上下回弹就会一起生效,可能有些需求只需保持top回弹拉伸效果,而bottom不需要,这里有几种实现方式:

  • mTopOverScrollDistancemBottomOverScrollDistance的值为0,这是一种简单粗暴的实现方式。
  • 修改overScrollBy方法,判断是否超过底部和顶部,并做逻辑处理。
  • 修改ScrollViewonTouchEvent方法源码。

在这里简单介绍下第二种方式的实现,其他方式可以自行尝试实现:

public class OverScrollView extends ScrollView {

    private int mTopOverScrollDistance = 150;
    private int mBottomOverScrollDistance = 90;

    private boolean mTopOverScrollEnable;
    private boolean mBottomOverScrollEnable;

    public OverScrollView(Context context) {
        super(context);
    }

    public OverScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public OverScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected boolean overScrollBy(
            int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {

        final int overScrollMode = getOverScrollMode();
        final boolean canScrollHorizontal =
                computeHorizontalScrollRange() > computeHorizontalScrollExtent();
        final boolean canScrollVertical =
                computeVerticalScrollRange() > computeVerticalScrollExtent();
        final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
        final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
                (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

        int newScrollX = scrollX + deltaX;
        if (!overScrollHorizontal) {
            maxOverScrollX = 0;
        }

        int newScrollY = scrollY + deltaY;
        if (!overScrollVertical) {
            mTopOverScrollDistance = 0;
            mBottomOverScrollDistance = 0;
        }

        // Clamp values if at the limits and record
        final int left = -maxOverScrollX;
        final int right = maxOverScrollX + scrollRangeX;
        final int top = -mTopOverScrollDistance;
        final int bottom = mBottomOverScrollDistance + scrollRangeY;

        boolean clampedX = false;
        if (newScrollX > right) {
            newScrollX = right;
            clampedX = true;
        } else if (newScrollX < left) {
            newScrollX = left;
            clampedX = true;
        }

        boolean clampedY = false;
        if (newScrollY > scrollRangeY) {    //判断是否超过底部
            if (!mBottomOverScrollEnable) {
                newScrollY = scrollRangeY;
                clampedY = true;
            }
        } else if (newScrollY < 0) {    //判断是否超过顶部
            if (!mTopOverScrollEnable) {
                newScrollY = 0;
                clampedY = true;
            }
        }

        if (newScrollY > bottom) {
            newScrollY = bottom;
            clampedY = true;
        } else if (newScrollY < top) {
            newScrollY = top;
            clampedY = true;
        }

        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

        return clampedX || clampedY;
    }
}

通过对ScrollView进行滑动到底部和顶部的逻辑判断,然后修改newScrollY的值,来屏蔽top或bottom的回弹拉伸,上述只实现了垂直方式的逻辑,水平方式需另行实现。

ListView拉伸回弹效果

ListView也可以实现上述效果,实现方式与ScrollView大同小异。

延伸

通过对ScrollViewListView的子类实现自定义属性,对mTopOverScrollDistancemBottomOverScrollDistancemTopOverScrollEnablemBottomOverScrollEnable提供单独的set方法,可动态修改拉伸回弹效果。

PS:本文系本人原创,未经许可,禁止转载。

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
  • 我作为新妈妈的生活又幸福了一点,小夏天基本稳定睡整觉了。 我的天使小夏天在睡眠上不是个天使宝宝。她的整觉磨合经历了...
  • 文、图/江南海北的雪 最近看到一本书《驴行北京》,这是户外徒步的亲身体验之作,是学者行者的跨界力作!也是第一本以驴...
  • 前端学习一个月,利用Html和CSS,以及JQuery完成了一家美容企业官网的制作,基本的网页布局和框架很快搭建起...
  • 清晨,我怀揣着晨练的梦想出发了。 记得附近的学校对外开放的,所以就想享受一下塑胶跑道。结果二个学校都是大门紧闭,只...