ListView和ScrollView的下拉和上拉的回弹效果

Android中是没有下拉和上拉的回弹效果的,但是有滚动到顶部和底部的光影效果,由EdgeEffect类来操作的。但是我们发现QQ,微信,支付宝等在下拉和上拉的都有一定的回弹效果,用户操作起来感觉蛮流畅的~

源码链接:https://github.com/chuwuwang/ZhouTools

效果

最近仿照着写来一个ListView和ScrollView的相同效果,用起来也是蛮方便的,先看下效果。

ListView和ScrollView的下拉和上拉的回弹效果_第1张图片ListView和ScrollView的下拉和上拉的回弹效果_第2张图片

原理

在网上搜索了一下,也有不少的实现思路。

  • 一种是不停的改变ListView或者ScrollView的paddingTop和paddingBottom的值。
  • 一种是不停的改变ListView或者ScrollView的布局,也就是改变距离上面的值和下面的值。
  • 还有一种就是通过重写官方自带的overScrollBy()方法。

关于overScrollBy()方法,只要改变返回中的maxOverScrollY的值。比如,你改成200,那下拉和上拉就有200距离的回弹效果。经过测试,发现好像在4.4以上版本均有bug(4.4以下版本没有测试机,无法考证),需要我们修改,遂放弃用此方法。

    @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, maxOverScrollY,
                isTouchEvent);
    }

实现

下面讲讲我的实现。

对于ListView的回弹效果,我用了改变padding值来实现的。也没有什么实现技巧,各位一看代码就明白了。说一点,当我们有回弹效果的时候,就应该不需要官方自带的光影效果了。

 // 禁用下拉到两端发荧光的效果
 setOverScrollMode(OVER_SCROLL_NEVER);

还有一点,如果当前正在回弹,这时候我们又来滑动,这时候需要处理下。因为我是用Handler来实现,只要清空所有的消息队列即可。

    ...
    case MotionEvent.ACTION_MOVE:
         // 清空所有消息队列
         mHandler.removeCallbacksAndMessages(null);
         ...

对于ScrollView的回弹效果,我用了改变布局大小来实现的。主要需要判断ScrollView是滑动到顶部还是底部,ScrollView有onOverScrolled()和onScrollChanged()这两个方法,分别都可以判断是否滑动到顶部和底部,但是稍微有点区别,这里就不细说了。

如果不通过调用上面方法,手动计算是否滑动到顶部和底部的话。需要明白ScrollView的高度是怎样计算的。

ListView和ScrollView的下拉和上拉的回弹效果_第3张图片

无padding的情况可以转换为有padding的情况,即tp,bp=0。

mScrollY + H – tp – bp = h ===> mScrollY + H = h

大多数情况下都可以这样计算,但是有个别及其特殊的机器,该方法是不准确的。(少数,我测试过,大部分的小米,华为,魅族,锤子都是可以的,我们公司自己生产的机器不行,NND)

主要是因为对于底部的计算和ScrollView自己的padding和子View的margin有关。

万一遇到这种奇葩机型,我们还需要去除子View的margin值。

不管是ListView还是ScrollView,都有一个方法,设置回弹速度的。

    /**
     * 设置回弹的速度。值越大,速度越快。默认为30。
     */
    public void setSpringBackSpeed(int speed) {
        if (speed <= 0) {
            throw new RuntimeException("speed 不能小于或者等于0");
        }
        this.speed = speed;
    }

代码

SpringBackListView.java

package com.geek.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ListView;

/**
 * 有回弹效果的ListView
 *
 * @author leeshenzhou on 2016/12/01.
 */
public class SpringBackListView extends ListView implements OnScrollListener {

    private int paddingLeft;
    private int paddingRight;
    private int paddingTop;
    private int paddingBottom;

    // true下拉.false上拉
    private boolean isPull;
    // 是否滚动到第一行
    private boolean isTop;
    // 是否滚动到最后一行
    private boolean isBottom;

    private int scrollState;

    private float mDownY;
    private float mLastY;

    private Handler mHandler = new Handler();

    // 回弹的速度
    private int speed = 20;

    /**
     * 设置回弹的速度。值越大,速度越快。默认为20。
     */
    public void setSpringBackSpeed(int speed) {
        if (speed <= 0) {
            throw new RuntimeException("speed 不能小于或者等于0");
        }
        this.speed = speed;
    }

    public SpringBackListView(Context context) {
        super(context);
        init();
    }

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

    public SpringBackListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        // 初始化padding的值
        paddingLeft = getPaddingLeft();
        paddingRight = getPaddingRight();
        paddingTop = getPaddingTop();
        paddingBottom = getPaddingBottom();

        setOnScrollListener(this);

        // 禁用下拉到两端发荧光的效果
        setOverScrollMode(OVER_SCROLL_NEVER);

    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = ev.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                // 清空所有消息队列
                mHandler.removeCallbacksAndMessages(null);

                mLastY = ev.getY();
                int offset = (int) ((mLastY - mDownY) / 2.5);
                isPull = offset > 0;

                if (isPull) { // 下拉操作
                    if (isTop && scrollState != SCROLL_STATE_FLING) {
                        offset += paddingTop;
                        setPadding(paddingLeft, offset, paddingRight, paddingBottom);
                        setSelection(0); // 选中第一个item.不然没有下拉效果
                    }

                } else { // 上拉
                    if (isBottom && scrollState != SCROLL_STATE_FLING) {
                        offset -= paddingBottom;
                        setPadding(paddingLeft, paddingTop, paddingRight, -offset);
                        setSelection(getCount() - 1); // 选中最后一个item.不然没有上拉效果
                    }
                }
                break;

            case MotionEvent.ACTION_UP:
                if (isPull) { // 下拉操作
                    int top = getPaddingTop();
                    int duration = 0;

                    while (top > paddingTop) {
                        top -= speed;
                        duration += 10;
                        final int pt = top;

                        mHandler.postDelayed(new Runnable() {

                            @Override
                            public void run() {
                                if (pt < paddingTop) {
                                    // 如果回弹的距离小于初始的paddingTop值,则恢复原始状态
                                    setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
                                } else {
                                    setPadding(paddingLeft, pt, paddingRight, paddingBottom);
                                }
                            }

                        }, duration);

                    }

                } else { // 上拉
                    int bottom = getPaddingBottom();
                    int duration = 0;

                    while (bottom > paddingBottom) {
                        bottom -= speed;
                        duration += 10;
                        final int pb = bottom;

                        mHandler.postDelayed(new Runnable() {

                            @Override
                            public void run() {
                                if (pb < paddingBottom) {
                                    // 如果回弹的距离小于初始的paddingBottom值,则恢复原始状态
                                    setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
                                } else {
                                    setPadding(paddingLeft, paddingTop, paddingRight, pb);
                                }
                            }

                        }, duration);

                    }

                }
                break;
        }

        return super.onTouchEvent(ev);

    }

    @Override
    public void onScroll(AbsListView lv, int firstVisibleItem,
                         int visibleItemCount, int totalItemCount) {
        isTop = firstVisibleItem == 0;
        isBottom = firstVisibleItem + visibleItemCount == totalItemCount;
    }

    @Override
    public void onScrollStateChanged(AbsListView lv, int scrollState) {
        this.scrollState = scrollState;
    }

    /**
     * 这是一个很奇怪的方法。哈哈
     */
    @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, maxOverScrollY,
                isTouchEvent);
    }

}

SpringBackScrollView.java

package com.geek.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;

/**
 * 有回弹效果的ScrollView
 * 
 * @author lee.shenzhou
 *
 */
public class SpringBackScrollView extends ScrollView {

    private float mDownY;
    private float mFirstY;

    // 子View
    private View childView;

    // 初始的位置
    private Rect normal = new Rect();

    private Handler mHandler = new Handler();

    private int speed = 30;

    private boolean isPull;

    /**
     * 设置回弹的速度。值越大,速度越快。默认为30。
     */
    public void setSpringBackSpeed(int speed) {
        if (speed <= 0) {
            throw new RuntimeException("speed 不能小于或者等于0");
        }
        this.speed = speed;
    }

    public SpringBackScrollView(Context context) {
        super(context);
        init();
    }

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

    private void init() {
        // 禁用下拉到两端发荧光的效果
        setOverScrollMode(OVER_SCROLL_NEVER);
    }

    @Override
    protected void onFinishInflate() {
        childView = getChildAt(0);
        if (childView != null) {
            normal.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (childView != null) {
            handleScrollTouchEvent(ev);
        }
        return super.onTouchEvent(ev);
    }

    public void handleScrollTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {

        case MotionEvent.ACTION_DOWN:
            mDownY = ev.getY();
            mFirstY = ev.getY();
            break;

        case MotionEvent.ACTION_UP:
            springBackLocation();
            break;

        case MotionEvent.ACTION_MOVE:
            // 移除滑动的消息队列
            mHandler.removeCallbacksAndMessages(null);

            final float preY = mDownY;
            final float nowY = ev.getY();

            isPull = nowY - mFirstY > 0;

            int deltaY = (int) ((preY - nowY) / 2.5);

            mDownY = nowY;

            // 当滚动到最上或者最下时就不会再滚动,这时移动布局
            if (isNeedMove()) {
                // 保存正常的布局位置
                if (normal.isEmpty()) {
                    normal.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
                    return;
                }
                // 移动布局
                childView.layout(childView.getLeft(), childView.getTop() - deltaY, 
                        childView.getRight(), childView.getBottom() - deltaY);
            } 

            break;

        default:
            break;
        }

    }

    /**
     * 回弹到原始位置
     */
    public void springBackLocation() {
        final int nowTop = childView.getTop();
        final int nowBottom = childView.getBottom();
        final int originTop = normal.top;
        final int originBottom = normal.bottom;

        Log.i("nsz", "nowTop:" + nowTop + " nowBottom:" + nowBottom
                + " originTop:" + originTop + " originBottom:" + originBottom);

        // 下拉回弹
        if (isPull) {
            int moveTop = nowTop;
            int moveBottom = nowBottom;
            int duration = 0;

            while (moveTop >= originTop) {
                moveTop -= speed;
                moveBottom -= speed;
                duration += 10;
                final int offTop = moveTop;
                final int offBottom = moveBottom;

                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (offTop <= originTop || offBottom <= originBottom) {
                            childView.layout(normal.left, normal.top, normal.right, normal.bottom);
                        } else {
                            childView.layout(normal.left, offTop, normal.right, offBottom);
                        }
                    }

                }, duration);
            }

        }

        // 上拉回弹
        else {
            int moveTop = nowTop;
            int moveBottom = nowBottom;
            int duration = 0;

            while (moveTop <= originTop) {
                moveTop += speed;
                moveBottom += speed;
                duration += 10;
                final int offTop = moveTop;
                final int offBottom = moveBottom;

                mHandler.postDelayed(new Runnable() {

                    @Override
                    public void run() {
                        if (offTop >= originTop || offBottom >= originBottom) {
                            childView.layout(normal.left, normal.top, normal.right, normal.bottom);
                        } else {
                            childView.layout(normal.left, offTop, normal.right, offBottom);
                        }
                    }

                }, duration);

            }

        }

    }

    /**
     * 是否需要移动布局
     */
    public boolean isNeedMove() {
        // 注意:慎重选择
        // 子View的margin和自己的padding对移动有影响,所以子View最好不要设置marginTop和marginBottom。
        // 如果设置了,对判断滑动到底部有些不准确,需要加上下面注释掉margin值,但是不同的机器,测试出有点不一样。

        // 获取到子View的margin值
        // LayoutParams params = (LayoutParams) childView.getLayoutParams();
        // int topMargin = params.topMargin;
        // int bottomMargin = params.bottomMargin;
        // int offset = childView.getHeight() - getHeight() + getPaddingBottom() + getPaddingTop() + topMargin + bottomMargin;
        int offset = childView.getHeight() - getHeight() + getPaddingBottom() + getPaddingTop();
        int scrollY = getScrollY();

        if (scrollY == 0) {
            return true;
        } else if (scrollY == offset) {
            return true;
        }

        return false;
    }

}

你可能感兴趣的:(Android,开发记录,android,listview,支付宝,微信)