1. 程式人生 > >Android 自定義ScrollView 支援慣性滑動,慣性回彈效果。支援上拉載入更多

Android 自定義ScrollView 支援慣性滑動,慣性回彈效果。支援上拉載入更多

先講下原理:

ScrollView的子View 主要分為3部分:head頭部,滾動內容,fooder底部

我們實現慣性滑動,以及回彈,都是靠超過head或者fooder 就重新滾動到  ,內容的頂部或者底部。

之前看了Pulltorefresh 他是通過不斷改變 head或者 fooder的 pading 值來實現 上拉或者 下拉的效果。感覺有點不流暢,而且層次巢狀得比較多。當然他的好處是擴充套件性好。

因工作需求,需要層次巢狀少,對效能要求非常高。因此重新自定義了ViewGroup實現。

直接上程式碼:

package com.example.administrator.customscrollview;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.OverScroller;

/**
 * 自定義 pulltorefresh Layout
 * TODO: ferris 2015年9月11日 18:52:40
 */
public class PullTorefreshScrollView extends ViewGroup {

    private FoodeLayout fooder_layout;// top and buttom
    private View top_layout;

    private int desireWidth, desireHeight;
    private VelocityTracker velocityTracker;
    private int mPointerId;
    private float x, y;
    private OverScroller mScroller;
    private int maxFlingVelocity, minFlingVelocity;
    private int mTouchSlop;
    protected Boolean isMove = false;
    protected float downX = 0, downY = 0;
    private int top_hight = 0;
    private int scrollYButtom = 0;
    private int nScrollYButtom = 0;

    private int pullDownMin = 0;
    private Boolean isEnablePullDown = true;

    private Boolean isFirst=true;

    public void setEnablePullDown(Boolean isEnablePullDown) {
        this.isEnablePullDown = isEnablePullDown;
    }

    public PullTorefreshScrollView(Context context) {
        super(context);
        init(null, 0);
    }

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

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

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
//        final TypedArray a = getContext().obtainStyledAttributes(
//                attrs, R.styleable.PullTorefreshScrollView, defStyle, 0);
//
//
//        a.recycle();
        mScroller = new OverScroller(getContext());
        maxFlingVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity();
        minFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        fooder_layout = (FoodeLayout) findViewById(R.id.fooder_layout);
        top_layout = findViewById(R.id.top_layout);


        if (isEnablePullDown) {
            fooder_layout.showFooderPull();
        } else {
            fooder_layout.hideFooder();
        }
    }


    public int getScrollYTop() {
        return top_hight;
    }

    public int getScrollYButtom() {
        return scrollYButtom;
    }

    public int getNScrollYTop() {
        return 0;
    }

    public int getNScrollYButtom() {
        return nScrollYButtom;
    }

    public int measureWidth(int widthMeasureSpec) {
        int result = 0;
        int measureMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        switch (measureMode) {
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = width;
                break;
            default:
                break;
        }
        return result;
    }

    public int measureHeight(int heightMeasureSpec) {
        int result = 0;
        int measureMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        switch (measureMode) {
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                result = height;
                break;
            default:
                break;
        }
        return result;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 計算所有child view 要佔用的空間
        int width = measureWidth(widthMeasureSpec);
        int height = measureHeight(heightMeasureSpec);

        desireWidth = 0;
        desireHeight = 0;
        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View v = getChildAt(i);

            if (v.getVisibility() != View.GONE) {

                LayoutParams lp = (LayoutParams) v.getLayoutParams();
                measureChildWithMargins(v, widthMeasureSpec, 0,
                        heightMeasureSpec, 0);

                //只是在這裡增加了垂直或者水平方向的判斷
                if (v.getId() == R.id.top_layout) {
                    top_hight = v.getMeasuredHeight();
                }
                desireWidth = Math.max(desireWidth, v.getMeasuredWidth()
                        + lp.leftMargin + lp.rightMargin);
                desireHeight += v.getMeasuredHeight() + lp.topMargin
                        + lp.bottomMargin;

            }
        }

        // count with padding
        desireWidth += getPaddingLeft() + getPaddingRight();
        desireHeight += getPaddingTop() + getPaddingBottom();

        // see if the size is big enough
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());


        //處理內容比較少的時候,就新增一定的高度
        int scrollHight = height + top_hight * 2;
        if (scrollHight > desireWidth) {
            int offset = scrollHight - desireHeight;
            View view = new View(getContext());
            view.setBackgroundResource(R.color.top_layout_color);
            LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, offset);
            addView(view, getChildCount() - 1, lp);
            desireWidth = scrollHight;
        }


        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
                resolveSize(desireHeight, heightMeasureSpec));

        scrollYButtom = desireHeight - getMeasuredHeight() - top_hight;
        nScrollYButtom = desireHeight - getMeasuredHeight();
        //如果上啦拖出一半的高度,就代表將要執行上啦
        pullDownMin = nScrollYButtom - top_hight / 2;
        if(isFirst){
            scrollTo(0, top_hight);
            isFirst=false;
        }

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int parentLeft = getPaddingLeft();
        final int parentRight = r - l - getPaddingRight();
        final int parentTop = getPaddingTop();
        final int parentBottom = b - t - getPaddingBottom();

        if (BuildConfig.DEBUG)
            Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
                    + parentTop + "   parentright: " + parentRight
                    + "   parentbottom: " + parentBottom);

        int left = parentLeft;
        int top = parentTop;

        int count = getChildCount();
        for (int i = 0; i < count; ++i) {
            View v = getChildAt(i);
            if (v.getVisibility() != View.GONE) {
                LayoutParams lp = (LayoutParams) v.getLayoutParams();
                final int childWidth = v.getMeasuredWidth();
                final int childHeight = v.getMeasuredHeight();
                final int gravity = lp.gravity;
                final int horizontalGravity = gravity
                        & Gravity.HORIZONTAL_GRAVITY_MASK;
                final int verticalGravity = gravity
                        & Gravity.VERTICAL_GRAVITY_MASK;


                // layout vertical, and only consider horizontal gravity

                left = parentLeft;
                top += lp.topMargin;
                switch (horizontalGravity) {
                    case Gravity.LEFT:
                        break;
                    case Gravity.CENTER_HORIZONTAL:
                        left = parentLeft
                                + (parentRight - parentLeft - childWidth) / 2
                                + lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        left = parentRight - childWidth - lp.rightMargin;
                        break;
                }
                v.layout(left, top, left + childWidth, top + childHeight);
                top += childHeight + lp.bottomMargin;
            }

        }


    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
    }

    @Override
    public android.view.ViewGroup.LayoutParams generateLayoutParams(
            AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateLayoutParams(
            android.view.ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    public static class LayoutParams extends MarginLayoutParams {
        public int gravity = -1;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray ta = c.obtainStyledAttributes(attrs,
                    R.styleable.SlideGroup);

            gravity = ta.getInt(R.styleable.SlideGroup_layout_gravity, -1);

            ta.recycle();
        }

        public LayoutParams(int width, int height) {
            this(width, height, -1);
        }

        public LayoutParams(int width, int height, int gravity) {
            super(width, height);
            this.gravity = gravity;
        }

        public LayoutParams(android.view.ViewGroup.LayoutParams source) {
            super(source);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }
    }


    /**
     * onInterceptTouchEvent()用來詢問是否要攔截處理。 onTouchEvent()是用來進行處理。
     * <p/>
     * 例如:parentLayout----childLayout----childView 事件的分發流程:
     * parentLayout::onInterceptTouchEvent()---false?--->
     * childLayout::onInterceptTouchEvent()---false?--->
     * childView::onTouchEvent()---false?--->
     * childLayout::onTouchEvent()---false?---> parentLayout::onTouchEvent()
     * <p/>
     * <p/>
     * <p/>
     * 如果onInterceptTouchEvent()返回false,且分發的子View的onTouchEvent()中返回true,
     * 那麼onInterceptTouchEvent()將收到所有的後續事件。
     * <p/>
     * 如果onInterceptTouchEvent()返回true,原本的target將收到ACTION_CANCEL,該事件
     * 將會發送給我們自己的onTouchEvent()。
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        if (BuildConfig.DEBUG)
            Log.d("onInterceptTouchEvent", "action: " + action);

        if (action == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
            // 該事件可能不是我們的
            return false;
        }

        boolean isIntercept = false;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 如果動畫還未結束,則將此事件交給onTouchEvet()處理,
                // 否則,先分發給子View
                isIntercept = !mScroller.isFinished();
                // 如果此時不攔截ACTION_DOWN時間,應該記錄下觸控地址及手指id,當我們決定攔截ACTION_MOVE的event時,
                // 將會需要這些初始資訊(因為我們的onTouchEvent將可能接收不到ACTION_DOWN事件)
                mPointerId = ev.getPointerId(0);
//          if (!isIntercept) {
                downX = x = ev.getX();
                downY = y = ev.getY();
//          }
                break;
            case MotionEvent.ACTION_MOVE:
                int pointerIndex = ev.findPointerIndex(mPointerId);
                if (BuildConfig.DEBUG)
                    Log.d("onInterceptTouchEvent", "pointerIndex: " + pointerIndex
                            + ", pointerId: " + mPointerId);
                float mx = ev.getX(pointerIndex);
                float my = ev.getY(pointerIndex);

                if (BuildConfig.DEBUG)
                    Log.d("onInterceptTouchEvent", "action_move [touchSlop: "
                            + mTouchSlop + ", deltaX: " + (x - mx) + ", deltaY: "
                            + (y - my) + "]");

                // 根據方向進行攔截,(其實這樣,如果我們的方向是水平的,裡面有一個ScrollView,那麼我們是支援巢狀的)

                if (Math.abs(y - my) >= mTouchSlop) {
                    isIntercept = true;
                }


                //如果不攔截的話,我們不會更新位置,這樣可以通過累積小的移動距離來判斷是否達到可以認為是Move的閾值。
                //這裡當產生攔截的話,會更新位置(這樣相當於損失了mTouchSlop的移動距離,如果不更新,可能會有一點點跳的感覺)
                if (isIntercept) {
                    x = mx;
                    y = my;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // 這是觸控的最後一個事件,無論如何都不會攔截
                if (velocityTracker != null) {
                    velocityTracker.recycle();
                    velocityTracker = null;
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                solvePointerUp(ev);

                break;
        }
        return isIntercept;
    }

    private void solvePointerUp(MotionEvent event) {
        // 獲取離開螢幕的手指的索引
        int pointerIndexLeave = event.getActionIndex();
        int pointerIdLeave = event.getPointerId(pointerIndexLeave);
        if (mPointerId == pointerIdLeave) {
            // 離開螢幕的正是目前的有效手指,此處需要重新調整,並且需要重置VelocityTracker
            int reIndex = pointerIndexLeave == 0 ? 1 : 0;
            mPointerId = event.getPointerId(reIndex);
            // 調整觸控位置,防止出現跳動
            x = event.getX(reIndex);
            y = event.getY(reIndex);
            if (velocityTracker != null)
                velocityTracker.clear();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        final int action = event.getActionMasked();

        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(event);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 獲取索引為0的手指id


                isMove = false;
                mPointerId = event.getPointerId(0);
                x = event.getX();
                y = event.getY();
                if (!mScroller.isFinished())
                    mScroller.abortAnimation();
                break;
            case MotionEvent.ACTION_MOVE:
                isMove = true;
                // 獲取當前手指id所對應的索引,雖然在ACTION_DOWN的時候,我們預設選取索引為0
                // 的手指,但當有第二個手指觸控,並且先前有效的手指up之後,我們會調整有效手指

                // 螢幕上可能有多個手指,我們需要保證使用的是同一個手指的移動軌跡,
                // 因此此處不能使用event.getActionIndex()來獲得索引
                final int pointerIndex = event.findPointerIndex(mPointerId);
                float mx = event.getX(pointerIndex);
                float my = event.getY(pointerIndex);

                moveBy((int) (x - mx), (int) (y - my));

                x = mx;
                y = my;
                break;
            case MotionEvent.ACTION_UP:
                isMove = false;
                velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
                float velocityX = velocityTracker.getXVelocity(mPointerId);
                float velocityY = velocityTracker.getYVelocity(mPointerId);

                completeMove(-velocityX, -velocityY);
                if (velocityTracker != null) {
                    velocityTracker.recycle();
                    velocityTracker = null;
                }
                break;

            case MotionEvent.ACTION_POINTER_UP:
                // 獲取離開螢幕的手指的索引
                isMove = false;
                int pointerIndexLeave = event.getActionIndex();
                int pointerIdLeave = event.getPointerId(pointerIndexLeave);
                if (mPointerId == pointerIdLeave) {
                    // 離開螢幕的正是目前的有效手指,此處需要重新調整,並且需要重置VelocityTracker
                    int reIndex = pointerIndexLeave == 0 ? 1 : 0;
                    mPointerId = event.getPointerId(reIndex);
                    // 調整觸控位置,防止出現跳動
                    x = event.getX(reIndex);
                    y = event.getY(reIndex);
                    if (velocityTracker != null)
                        velocityTracker.clear();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                isMove = false;
                break;
        }
        return true;
    }

    private Boolean isPull = false;

    //此處的moveBy是根據水平或是垂直排放的方向,
//來選擇是水平移動還是垂直移動
    public void moveBy(int deltaX, int deltaY) {
        if (BuildConfig.DEBUG)
            Log.d("moveBy", "deltaX: " + deltaX + "    deltaY: " + deltaY);
        if (Math.abs(deltaY) >= Math.abs(deltaX)) {
            int mScrollY = getScrollY();
            if (mScrollY <= 0) {
                scrollTo(0, 0);
            } else if (mScrollY >= getNScrollYButtom()) {
                scrollTo(0, getNScrollYButtom());


            } else {
                scrollBy(0, deltaY);
            }

            if (isEnablePullDown && deltaY > 0 && mScrollY >= pullDownMin) {
                isPull = true;
                Log.d("onlayout", "isPull: true");
            }
        }


    }

    private void completeMove(float velocityX, float velocityY) {

        int mScrollY = getScrollY();
        int maxY = getScrollYButtom();
        int minY = getScrollYTop();


        if (mScrollY >= maxY) {//如果滾動,超過了 下邊界,就回彈到下邊界

            if (isPull) {//滾動到最底部
                mScroller.startScroll(0, mScrollY, 0, getNScrollYButtom() - mScrollY, 300);
                invalidate();

                //顯示雷達
                fooder_layout.showFooderRadar();
                if (pullDownListem != null) {
                    pullDownListem.onPullDown();
                }
                Log.d("onlayout", "isPull: true 滾動到最底部,顯示出雷達");

            } else {
                mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY);
                invalidate();
                Log.d("onlayout", "isPull: true");
            }

        } else if (mScrollY <= minY) {//如果滾動,超過了上邊界,就回彈到上邊界
            // 超出了上邊界,彈回
            mScroller.startScroll(0, mScrollY, 0, minY - mScrollY);
            invalidate();
        } else if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {//大於1頁的時候
//            mScroller.fling(0, mScrollY, 0, (int) (velocityY * 1.2f), 0, 0, minY, maxY);
            mScroller.fling(0, mScrollY, 0, (int) (velocityY * 2f), 0, 0, getNScrollYTop(), getNScrollYButtom());
            invalidate();
        }


    }


    public void ifNeedScrollBack() {
        int mScrollY = getScrollY();
        int maxY = getScrollYButtom();
        int minY = getScrollYTop();
        if (mScrollY > maxY) {
            // 超出了下邊界,彈回
            mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY);

            invalidate();

        } else if (mScrollY < minY) {
            // 超出了上邊界,彈回
            mScroller.startScroll(0, mScrollY, 0, minY - mScrollY);
            invalidate();
        }
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {

            scrollTo(0, mScroller.getCurrY());

            postInvalidate();

        } else {
            Log.d("onlayout", "computeScroll,isMove:"+isMove+",isPull:"+isPull);
            if (!isMove && !isPull) {
                ifNeedScrollBack();
            }

        }
    }


    public void onPullSuccess() {

        soomToBack();
    }

    public void soomToBack() {
        int mScrollY = getScrollY();
        int maxY = getScrollYButtom();
        Log.d("onlayout", "soomToBack: (maxY - mScrollY)="+(maxY - mScrollY)+",maxY="+maxY+",mScrollY="+mScrollY);
        // 超出了下邊界,彈回
        mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY, 300);
        invalidate();
        postDelayed(new Runnable() {
            @Override
            public void run() {
                fooder_layout.showFooderPull();
                isPull = false;
            }
        }, 310);


    }

    private PullDownListem pullDownListem;

    public void setPullDownListem(PullDownListem pullDownListem) {
        this.pullDownListem = pullDownListem;
    }

    public interface PullDownListem {

        public void onPullDown();

    }
}