ListView 回弹/阻尼效果

网上例子比较多, 找着试了好几个不是真的行. 所以这必须是真的能用的, 而且效果要棒棒的. 下面例出好几种方案

...

方案一[推荐]: 这是在 github上找的一个star并不高的代码, 但是这效果是我花了大半天时间趴的最棒的! 不多说, 上代码

package com.jhcp.lottery.ui.wedgit;

/**
 * Created by suliyea on 16/4/14.
 */
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.RelativeLayout;

public class BounceScroller extends RelativeLayout {
    public static final String TAG = "BounceScroller";

    public static final int DEFALUT_DURATION = 400;

    public static enum State {
        STATE_FIT_CONTENT, STATE_SHOW, STATE_OVER, STATE_FIT_EXTRAS
    };

    protected State mState = State.STATE_FIT_CONTENT;
    private Bouncer mBouncer = new Bouncer();
    private BounceListener mListener;
    private View mContentView;

    private int mLastEventY;
    private int mLastTargetTop;

    private View mHeaderView;
    protected int mHeaderHeight;

    private View mFooterView;
    private int mFooterHeight;

    private boolean overScrolled;

    private boolean pullingHeader;
    private boolean pullingFooter;

    private boolean headerBounce = true;
    private boolean footerBounce = true;

    private int remainOffset;

    private View mTargetView;
    private TimeInterpolator mInterpolator;
    private long mTimeBase = 0;

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

    public BounceScroller(Context context, AttributeSet attrs) {
        super(context, attrs);

        mInterpolator = new DecelerateInterpolator();
        remainOffset = 0;
    }

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mContentView == null && !eventInView(event, mContentView)) {
            return super.dispatchTouchEvent(event);
        }

        int action = event.getAction();

        if (takeTouchEvent(event)) {
            Log.d(TAG, System.currentTimeMillis() + " takeTouchEvent " + action);
        } else {
            boolean result = super.dispatchTouchEvent(event);
            Log.d(TAG, System.currentTimeMillis() + " dispatchTouchEvent "
                    + result);
        }

        if (action == MotionEvent.ACTION_CANCEL
                || action == MotionEvent.ACTION_UP) {
            overScrolled = false;
            mTimeBase = 0;
            pullingHeader = false;
            pullingFooter = false;
            mLastEventY = 0;
            mLastTargetTop = 0;
            mTargetView = null;
            return true;
        } else if (action == MotionEvent.ACTION_DOWN) {
            // cancel bounce if exists
            mBouncer.cancel();

            mTargetView = getTargetView(mContentView, event);
            mTimeBase = 0;
        } else if (action == MotionEvent.ACTION_MOVE) {
            int eventOffset = (int) event.getY() - mLastEventY;
            if (!overScrolled) {
                if (mHeaderView != null && mHeaderView.getBottom() > 0
                        && eventOffset < 0) {
                    overScrolled = true;
                } else if (mFooterView != null
                        && mFooterView.getTop() < getBottom()
                        && eventOffset > 0) {
                    overScrolled = true;
                } else {
                    overScrolled = false;
                }
            }

            if (mTargetView != null
                    && mTargetView.getVisibility() != View.VISIBLE) {
                mTargetView = getTargetView(mContentView, event);
                Log.d(TAG, "update mTargetView " + mTargetView.getId());
                mTimeBase = 0;
                overScrolled = false;
            } else {
                int targetTop = getViewTop(mTargetView);
                int viewOffset = targetTop - mLastTargetTop;
                Log.d(TAG, "targetTop " + targetTop + " viewOffset "
                        + viewOffset + " eventOffset " + eventOffset
                        + " mTimeBase " + mTimeBase);
                if (eventOffset != 0 && viewOffset == 0 && !overScrolled) {
                    long currentTime = System.currentTimeMillis();
                    remainOffset += eventOffset;
                    if (mTimeBase == 0) {
                        mTimeBase = currentTime;
                    } else if (currentTime - mTimeBase > 50) {
                        overScrolled = true;
                        mTimeBase = 0;
                    }
                } else if (eventOffset != 0 && viewOffset != 0) {
                    mTimeBase = 0;
                }

                if (remainOffset != 0 && overScrolled) {
                    Log.d(TAG, "do remainOffset " + remainOffset);
                    onOffset(remainOffset);
                    remainOffset = 0;
                }
            }
        }

        mLastTargetTop = getViewTop(mTargetView);
        mLastEventY = (int) event.getY();

        return true;
    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(TAG, System.currentTimeMillis() + " onDraw");
    }

    private int getViewTop(View view) {
        if (view == null) {
            return 0;
        }
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        return location[1];
    }

    private boolean takeTouchEvent(MotionEvent event) {
        if (!headerBounce && !footerBounce) {
            return false;
        }

        int action = event.getAction();
        int contentTop = mContentView.getTop();

        if (action == MotionEvent.ACTION_UP
                || action == MotionEvent.ACTION_CANCEL) {
            if (contentTop > 0) {
                int offset = contentTop;
                if (mHeaderView != null && offset > mHeaderHeight / 2) {
                    offset = offset - mHeaderHeight;
                    mBouncer.recover(true, offset, State.STATE_FIT_EXTRAS);
                } else {
                    mBouncer.recover(true, offset, State.STATE_FIT_CONTENT);
                }
            } else if (contentTop < 0) {
                int offset = mContentView.getBottom() - getBottom();
                if (mFooterView != null && (offset + mFooterHeight / 2) < 0) {
                    offset = offset + mFooterHeight;
                    mBouncer.recover(false, offset, State.STATE_FIT_EXTRAS);
                } else {
                    // add by suliyea 防止上拉回弹时候不能回复到原位.
                    if (offset != contentTop) {
                        offset = contentTop;
                    }
                    mBouncer.recover(false, offset, State.STATE_FIT_CONTENT);
                }
            }
        } else if (action == MotionEvent.ACTION_MOVE) {
            int offset = (int) (event.getY() - mLastEventY);
            return onOffset(offset);
        }
        return false;
    }

    private boolean onOffset(int offset) {
        offset = offset / 2;
        boolean handled = false;
        int contentTop = mContentView.getTop();
        if (headerBounce && !handled && contentTop >= 0 && !pullingFooter) {
            handled |= pullHeader(offset);
        }

        if (footerBounce && !handled && contentTop <= 0 && !pullingHeader) {
            handled |= pullFooter(offset);
        }
        return handled;
    }

    private void setState(boolean header, State newState) {
        String position = header ? "header" : "footer";

        if (newState == mState) {
            return;
        }

        Log.d(TAG, position + " setState " + mState + " -> " + newState);

        mState = newState;

        if (mListener != null) {
            mListener.onState(header, newState);
        }
    }

    private boolean pullHeader(int offset) {
        int scrollY = mContentView.getScrollY();
        int curTop = mContentView.getTop();

        // pull header
        if (!overScrolled || scrollY > 0
                || (offset < 0 && scrollY == 0 && curTop <= 0)) {
            return false;
        }

        pullingHeader = true;

        int nextTop = curTop + offset;
        if (nextTop <= 0) {
            offset = -curTop;
            overScrolled = false;
            mTimeBase = 0;
            nextTop = 0;
            pullingHeader = false;
            if (mState != State.STATE_FIT_CONTENT) {
                setState(true, State.STATE_FIT_CONTENT);
            }
        } else if (nextTop > 0 && nextTop <= mHeaderHeight) {
            if ((mState != State.STATE_SHOW)) {
                setState(true, State.STATE_SHOW);
            }
        } else if (nextTop > mHeaderHeight) {
            if (mState != State.STATE_OVER) {
                setState(true, State.STATE_OVER);
            }
        }

        Log.d(TAG, "pullHeader " + offset + " nextTop " + nextTop);
        offsetContent(offset);
        return true;
    }

    private boolean pullFooter(int offset) {
        int curBottom = mContentView.getBottom();
        int conBottom = this.getBottom();

        // pull footer
        if (!overScrolled || (offset > 0 && conBottom <= curBottom)) {
            return false;
        }

        pullingFooter = true;

        int nextBottom = curBottom + offset;
        if (nextBottom >= conBottom) {
            offset = conBottom - curBottom;
            overScrolled = false;
            mTimeBase = 0;
            nextBottom = conBottom;
            pullingFooter = false;
            if (mState != State.STATE_FIT_CONTENT) {
                setState(false, State.STATE_FIT_CONTENT);
            }
        } else if (nextBottom < conBottom
                && nextBottom >= (conBottom - mFooterHeight)) {
            if ((mState != State.STATE_SHOW)) {
                setState(false, State.STATE_SHOW);
            }
        } else if (nextBottom < (conBottom - mFooterHeight)) {
            if (mState != State.STATE_OVER) {
                setState(false, State.STATE_OVER);
            }
        }

        Log.d(TAG, "pullFooter " + offset + " nextBottom " + nextBottom);
        offsetContent(offset);
        return true;
    }

    private class Bouncer implements AnimatorUpdateListener, AnimatorListener {
        private ValueAnimator mAnimator;
        private int mLastOffset;
        private boolean isHeader;
        private State mTargetState;
        private boolean mCanceled;

        public void recover(boolean header, int offset, State state) {
            cancel();
            Log.d(TAG, "recover offset " + offset);
            mCanceled = false;
            isHeader = header;
            mTargetState = state;
            mAnimator = new ValueAnimator();
            mAnimator.setIntValues(0, offset);
            mLastOffset = 0;
            mAnimator.setDuration(500);
            mAnimator.setRepeatCount(0);
            if (mInterpolator == null) {
                mInterpolator = new DecelerateInterpolator();
            }
            mAnimator.setInterpolator(mInterpolator);
            mAnimator.addListener(this);
            mAnimator.addUpdateListener(this);
            mAnimator.start();
        }

        public void cancel() {
            if (mAnimator != null && mAnimator.isRunning()) {
                mAnimator.cancel();
            }
            mAnimator = null;
        }

        @Override
        public void onAnimationUpdate(ValueAnimator va) {
            int currentOffset = (Integer) va.getAnimatedValue();
            int delta = mLastOffset - currentOffset;
            Log.d(TAG, "recover delta " + delta + " currentOffset "
                    + currentOffset);
            offsetContent(delta);
            mLastOffset = currentOffset;

            if (mListener != null) {
                int contentOffset = mContentView.getTop();
                mListener.onOffset(isHeader, contentOffset);
            }
        }

        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d(TAG, "onAnimationEnd");
            mAnimator = null;
            if (!mCanceled) {
                setState(isHeader, mTargetState);
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            Log.d(TAG, "onAnimationCancel");
            mCanceled = true;
        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
                            int bottom) {
        int contentTop = 0;
        int contentBottom = 0;

        if (mContentView != null) {
            contentTop = mContentView.getTop();
            contentBottom = contentTop + this.getMeasuredHeight();
            mContentView.layout(0, contentTop, right, contentBottom);
        }

        if (mHeaderView != null) {
            int headerTop = contentTop - mHeaderHeight;
            mHeaderView.layout(0, headerTop, right, headerTop + mHeaderHeight);
        }

        if (mFooterView != null) {
            int footerTop = contentBottom;
            mFooterView.layout(0, footerTop, right, footerTop + mFooterHeight);
        }
    }

    private boolean offsetContent(int offset) {
        if (mContentView != null) {
            mContentView.offsetTopAndBottom(offset);
        }

        if (mHeaderView != null) {
            mHeaderView.offsetTopAndBottom(offset);
        }

        if (mFooterView != null) {
            mFooterView.offsetTopAndBottom(offset);
        }

        if (mListener != null) {
            int contentOffset = mContentView.getTop();
            boolean header = contentOffset > 0;
            mListener.onOffset(header, contentOffset);
        }

        invalidate();
        return true;
    }

    public boolean attach(View view) {
        if (view == null) {
            return false;
        }
        ViewGroup parent = (ViewGroup) view.getParent();
        ViewGroup.LayoutParams params = parent.getLayoutParams();
        int index = parent.indexOfChild(view);
        parent.removeView(view);

        parent.addView(this, index, params);

        index = 0;
        if (mHeaderView != null) {
            index = 1;
        }
        params = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        addView(view, index, params);
        this.mContentView = view;
        return true;
    }

    public void resetState() {
        if (mState != State.STATE_FIT_EXTRAS) {
            return;
        }

        if (mContentView == null) {
            return;
        }

        int offset = mContentView.getTop();
        if (offset != 0) {
            boolean header = true;
            if (offset < 0) {
                header = false;
            }
            mBouncer.recover(header, offset, State.STATE_FIT_CONTENT);
        }
    }

    public BounceScroller enableHeader(boolean bounce) {
        this.headerBounce = bounce;
        return this;
    }

    public BounceScroller enableFooter(boolean bounce) {
        this.footerBounce = bounce;
        return this;
    }

    public BounceScroller setHeaderView(View view) {
        if (mHeaderView != null) {
            removeView(mHeaderView);
            mHeaderView = null;
        }

        mHeaderView = view;
        if (mHeaderView != null) {
            mHeaderView.measure(MeasureSpec.UNSPECIFIED,
                    MeasureSpec.UNSPECIFIED);
            mHeaderHeight = mHeaderView.getMeasuredHeight();
            LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
                    mHeaderHeight);
            addView(mHeaderView, 0, params);
        }
        return this;
    }

    public View getHeaderView() {
        return mHeaderView;
    }

    public BounceScroller setFooterView(View view) {
        if (mFooterView != null) {
            removeView(mFooterView);
            mFooterView = null;
        }

        mFooterView = view;
        if (mFooterView != null) {
            mFooterView.measure(MeasureSpec.UNSPECIFIED,
                    MeasureSpec.UNSPECIFIED);
            mFooterHeight = mFooterView.getMeasuredHeight();
            LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
                    mHeaderHeight);
            addView(mFooterView, 0, params);
        }
        return this;
    }

    public View getFooterView() {
        return mFooterView;
    }

    public BounceScroller setListener(BounceListener listener) {
        this.mListener = listener;
        return this;
    }

    public BounceScroller setInterpolator(TimeInterpolator interpolator) {
        this.mInterpolator = interpolator;
        return this;
    }

    private View getTargetView(View target, MotionEvent event) {
        View view = null;
        if (target == null) {
            return view;
        }

        if (!eventInView(event, target)) {
            return view;
        }

        if (!(target instanceof ViewGroup)) {
            view = target;
            return view;
        }

        if (target instanceof AdapterView) {
            AdapterView parent = (AdapterView) target;
            int first = parent.getFirstVisiblePosition();
            int last = parent.getLastVisiblePosition();
            for (int index = 0; index <= (last - first); ++index) {
                View child = parent.getChildAt(index);
                if (!eventInView(event, child)) {
                    continue;
                }
                if (!(child instanceof ViewGroup)) {
                    view = child;
                    return view;
                }

                view = getTargetView(child, event);
                // stop search in current view group
                return view;
            }
        } else if (target instanceof ViewGroup) {
            ViewGroup parent = (ViewGroup) target;
            int childCount = parent.getChildCount();

            // with z-order
            for (int index = childCount - 1; index >= 0; --index) {
                View child = parent.getChildAt(index);
                if (!eventInView(event, child)) {
                    continue;
                }

                if (!(child instanceof ViewGroup)) {
                    view = child;
                    return view;
                }

                view = getTargetView(child, event);
                // stop search in current view group
                return view;
            }
        }

        // set view as group self
        view = target;
        return view;
    }

    private boolean eventInView(MotionEvent event, View view) {
        if (event == null || view == null) {
            return false;
        }

        int eventX = (int) event.getRawX();
        int eventY = (int) event.getRawY();

        int[] location = new int[2];
        view.getLocationOnScreen(location);

        int width = view.getWidth();
        int height = view.getHeight();
        int left = location[0];
        int top = location[1];
        int right = left + width;
        int bottom = top + height;

        Rect rect = new Rect(left, top, right, bottom);
        boolean contains = rect.contains(eventX, eventY);
        return contains;
    }



    public interface BounceListener {

        public void onState(boolean header, BounceScroller.State state);

        public void onOffset(boolean header, int offset);
    }

}

用例




    

        

            
        
    




方案二: 这个是 github上还不错的, 代码很大神, 使用很方便, 但是在测试使用 listview阻尼效果发现, 会有点击效果(selecter)的事件冲突, 没有找到好的解决办法, 怎么用自己看https://github.com/EverythingMe/overscroll-decor

...

方案三: 下面这种是网上流传的重写overScrollBy方式, 体验不好的地方是在快递下拉时, 会出现停止不回弹的 bug

我们使用ListView将位置拖到顶部和底部默认是没有回弹效果的,为了增加这个效果,方法如下:
1、开启overScrollMode为always在布局中android:overScrollMode="always"或在代码中setOverScrollMode(View.OVER_SCROLL_ALWAYS);
2、继承listview 覆盖overScrollBy方法,并且利用反射机制修改阴影效果为透明



import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;

import java.lang.reflect.Field;

/**
 * Created by suliyea on 16/4/12.
 */


public class BounceListView extends ListView {
    private static final int MAX_Y_OVERSCROLL_DISTANCE = 100;

    private Context mContext;
    private int mMaxYOverscrollDistance;

    public BounceListView(Context context) {
        super(context);
        mContext = context;
        initBounceListView();
    }

    public BounceListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initBounceListView();
    }

    public BounceListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mContext = context;
        initBounceListView();
    }

    private void initBounceListView() {
        // get the density of the screen and do some maths with it on the max
        // overscroll distance
        // variable so that you get similar behaviors no matter what the screen
        // size

        final DisplayMetrics metrics = mContext.getResources()
                .getDisplayMetrics();
        final float density = metrics.density;
        mMaxYOverscrollDistance = (int) (density * MAX_Y_OVERSCROLL_DISTANCE);

        this.setOverScrollMode(View.OVER_SCROLL_ALWAYS);

        try {
            Class c = (Class) Class.forName(AbsListView.class.getName());
            Field egtField = c.getDeclaredField("mEdgeGlowTop");
            Field egbBottom = c.getDeclaredField("mEdgeGlowBottom");
            egtField.setAccessible(true);
            egbBottom.setAccessible(true);
            Object egtObject = egtField.get(this); // this 指的是ListiVew实例
            Object egbObject = egbBottom.get(this);

            // egtObject.getClass() 实际上是一个 EdgeEffect 其中有两个重要属性 mGlow mEdge
            // 并且这两个属性都是Drawable类型
            Class cc = (Class) Class.forName(egtObject.getClass()
                    .getName());
            Field mGlow = cc.getDeclaredField("mGlow");
            mGlow.setAccessible(true);
            mGlow.set(egtObject, new ColorDrawable(Color.TRANSPARENT));
            mGlow.set(egbObject, new ColorDrawable(Color.TRANSPARENT));

            Field mEdge = cc.getDeclaredField("mEdge");
            mEdge.setAccessible(true);
            mEdge.set(egtObject, new ColorDrawable(Color.TRANSPARENT));
            mEdge.set(egbObject, new ColorDrawable(Color.TRANSPARENT));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @SuppressLint("NewApi")
    @Override
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX,
                                   int scrollY, int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
        // This is where the magic happens, we have replaced the incoming
        // maxOverScrollY with our own custom variable mMaxYOverscrollDistance;
        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY,
                scrollRangeX, scrollRangeY, maxOverScrollX,
                mMaxYOverscrollDistance, isTouchEvent);
    }
}```


>方案四: 再来一种实现方式, 这种方式能下拉出来, 但是回弹的效果没有, 体验不好

/**

  • 这种方式能下拉出来, 但是回弹的效果没有, 体验不好
    */
    public class BounceListView extends ListView implements Runnable {

    // 手指点位置的Y坐标
    private float mLastDownY = 0f;
    // 移动距离
    private int mDistance = 0;
    private int mStep = 0;
    // 是否移动过
    private boolean mPositive = false;

    /**

    • 构造器
      */
      public BounceListView(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      }

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

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

    /**

    • TouchEvent事件
      */
      @Override
      public boolean onTouchEvent(MotionEvent event) {
      switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
      // 系列事件,手指第一次按下时触发
      if (mLastDownY == 0f && mDistance == 0) {
      mLastDownY = event.getY();
      return true;
      }
      break;

       case MotionEvent.ACTION_CANCEL:
           break;
      
       case MotionEvent.ACTION_UP:
           // 手指离开之后触发
           if (mDistance != 0) {
               mStep = 1;
               mPositive = (mDistance >= 0);
               // 即可把你的Runnable对象增加到UI线程中运行。
               this.post(this);
               return true;
           }
           // 重新赋值
           mLastDownY = 0f;
           mDistance = 0;
           break;
      
       case MotionEvent.ACTION_MOVE: // 手指按下之后滑动触发
           if (mLastDownY != 0f) {
               mDistance = (int) (mLastDownY - event.getY());
               if ((mDistance < 0 && getFirstVisiblePosition() == 0 && getChildAt(
                       0).getTop() == 0)
                       || (mDistance > 0 && getLastVisiblePosition() == getCount() - 1)) {
                   // 第一个位置并且是想下拉,就滑动或者最后一个位置向上拉
                   // 这个判断的作用是在非顶端的部分不会有此滚动
                   mDistance /= 3; // 这里是为了减少滚动的距离
                   scrollTo(0, mDistance); // 滚动
                   return true;
               }
           }
           // 置为0,有自动滑动的效果
           mDistance = 0;
           break;
      

      }
      return super.onTouchEvent(event);
      }

    public void run() {
    mDistance += mDistance > 0 ? -mStep : mStep;
    scrollTo(0, mDistance);
    // 下拉mPositive是false,上拉是true
    if ((mPositive && mDistance <= 0) || (!mPositive && mDistance >= 0)) {
    scrollTo(0, 0);
    mDistance = 0;
    mLastDownY = 0f;
    return;
    }
    mStep += 1;
    this.post(this);
    }
    }

你可能感兴趣的:(ListView 回弹/阻尼效果)