android.widget.OverScroller

Here are the examples of the java api android.widget.OverScroller taken from open source projects. By voting up you can indicate which examples are most useful and appropriate.

119 Examples 7

19 Source : OverScrollLayout.java
with Apache License 2.0
from zuoweitan

/**
 * copy from wcy10586/OverscrollLayout and thanks for this
 */
public clreplaced OverScrollLayout extends RelativeLayout {

    private static final String TAG = "OverScrollLayout";

    private ViewConfiguration configuration;

    private View child;

    private float downY;

    private float oldY;

    private int dealtY;

    private Scroller mScroller;

    private float downX;

    private float oldX;

    private int dealtX;

    private boolean isVerticalMove;

    private boolean isHorizontallyMove;

    private boolean isOverScrollTop;

    private boolean isOverScrollBottom;

    private boolean isOverScrollLeft;

    private boolean isOverScrollRight;

    private boolean checkScrollDirectionFinish;

    private boolean canOverScrollHorizontally;

    private boolean canOverScrollVertical;

    private float baseOverScrollLength;

    private boolean topOverScrollEnable = true;

    private boolean bottomOverScrollEnable = true;

    private boolean leftOverScrollEnable = true;

    private boolean rightOverScrollEnable = true;

    private OnOverScrollListener onOverScrollListener;

    private OverScrollCheckListener checkListener;

    public static int SCROLL_VERTICAL = LinearLayout.VERTICAL;

    public static int SCROLL_HORIZONTAL = LinearLayout.HORIZONTAL;

    private float fraction = 0.5f;

    private boolean finishOverScroll;

    private boolean abortScroller;

    private boolean shouldSetScrollerStart;

    private boolean disallowIntercept;

    private GestureDetector detector;

    private FlingRunnable flingRunnable;

    private OverScroller flingScroller;

    private OverScrollRunnable overScrollRunnable;

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

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

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

    @SuppressWarnings("NewApi")
    public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        configuration = ViewConfiguration.get(getContext());
        mScroller = new Scroller(getContext(), new OvershootInterpolator(0.75f));
        flingRunnable = new FlingRunnable();
        overScrollRunnable = new OverScrollRunnable();
        flingScroller = new OverScroller(getContext());
        detector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                if (isOverScrollTop || isOverScrollBottom || isOverScrollLeft || isOverScrollRight) {
                    return false;
                }
                // 
                flingRunnable.start(velocityX, velocityY);
                return false;
            }
        });
    }

    @Override
    protected void onFinishInflate() {
        int childCount = getChildCount();
        if (childCount > 1) {
            throw new IllegalStateException("OverScrollLayout only can host 1 element");
        } else if (childCount == 1) {
            child = getChildAt(0);
            child.setOverScrollMode(OVER_SCROLL_NEVER);
        }
        super.onFinishInflate();
    }

    public void setDisallowInterceptTouchEvent(boolean disallowIntercept) {
        this.disallowIntercept = disallowIntercept;
    }

    public boolean isTopOverScrollEnable() {
        return topOverScrollEnable;
    }

    /**
     * @param topOverScrollEnable true can over scroll top false otherwise
     */
    public void setTopOverScrollEnable(boolean topOverScrollEnable) {
        this.topOverScrollEnable = topOverScrollEnable;
    }

    public boolean isBottomOverScrollEnable() {
        return bottomOverScrollEnable;
    }

    /**
     * @param bottomOverScrollEnable true can over scroll bottom false otherwise
     */
    public void setBottomOverScrollEnable(boolean bottomOverScrollEnable) {
        this.bottomOverScrollEnable = bottomOverScrollEnable;
    }

    public boolean isLeftOverScrollEnable() {
        return leftOverScrollEnable;
    }

    /**
     * @param leftOverScrollEnable true can over scroll left false otherwise
     */
    public void setLeftOverScrollEnable(boolean leftOverScrollEnable) {
        this.leftOverScrollEnable = leftOverScrollEnable;
    }

    public boolean isRightOverScrollEnable() {
        return rightOverScrollEnable;
    }

    /**
     * @param rightOverScrollEnable true can over scroll right false otherwise
     */
    public void setRightOverScrollEnable(boolean rightOverScrollEnable) {
        this.rightOverScrollEnable = rightOverScrollEnable;
    }

    public OnOverScrollListener getOnOverScrollListener() {
        return onOverScrollListener;
    }

    /**
     * @param onOverScrollListener
     */
    public void setOnOverScrollListener(OnOverScrollListener onOverScrollListener) {
        this.onOverScrollListener = onOverScrollListener;
    }

    public OverScrollCheckListener getOverScrollCheckListener() {
        return checkListener;
    }

    /**
     * @param checkListener for custom view check over scroll
     */
    public void setOverScrollCheckListener(OverScrollCheckListener checkListener) {
        this.checkListener = checkListener;
    }

    public float getFraction() {
        return fraction;
    }

    /**
     * @param fraction the fraction for over scroll.it is num[0f,1f],
     */
    public void setFraction(float fraction) {
        if (fraction < 0 || fraction > 1) {
            return;
        }
        this.fraction = fraction;
    }

    private void checkCanOverScrollDirection() {
        if (checkScrollDirectionFinish) {
            return;
        }
        if (checkListener != null) {
            int mOrientation = checkListener.getContentViewScrollDirection();
            canOverScrollHorizontally = RecyclerView.HORIZONTAL == mOrientation;
            canOverScrollVertical = RecyclerView.VERTICAL == mOrientation;
        } else if (child instanceof AbsListView || child instanceof ScrollView || child instanceof WebView) {
            canOverScrollHorizontally = false;
            canOverScrollVertical = true;
        } else if (child instanceof RecyclerView) {
            RecyclerView recyclerView = (RecyclerView) child;
            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
            int mOrientation = -1;
            if (layoutManager instanceof StaggeredGridLayoutManager) {
                mOrientation = ((StaggeredGridLayoutManager) layoutManager).getOrientation();
            } else if (layoutManager instanceof LinearLayoutManager) {
                LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
                mOrientation = manager.getOrientation();
            }
            canOverScrollHorizontally = RecyclerView.HORIZONTAL == mOrientation;
            canOverScrollVertical = RecyclerView.VERTICAL == mOrientation;
        } else if (child instanceof HorizontalScrollView) {
            canOverScrollHorizontally = true;
            canOverScrollVertical = false;
        } else if (child instanceof ViewPager) {
            // forbid ViewPager  over scroll
            canOverScrollHorizontally = false;
            canOverScrollVertical = false;
        } else {
            canOverScrollHorizontally = false;
            canOverScrollVertical = true;
        }
        checkScrollDirectionFinish = true;
        if (canOverScrollVertical) {
            baseOverScrollLength = getHeight();
        } else {
            baseOverScrollLength = getWidth();
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int scrollerY = mScroller.getCurrY();
            scrollTo(mScroller.getCurrX(), scrollerY);
            postInvalidate();
        } else {
            if (abortScroller) {
                abortScroller = false;
                return;
            }
            if (finishOverScroll) {
                isOverScrollTop = false;
                isOverScrollBottom = false;
                isOverScrollLeft = false;
                isOverScrollRight = false;
                finishOverScroll = false;
            }
        }
    }

    protected void mSmoothScrollTo(int fx, int fy) {
        int dx = fx - mScroller.getFinalX();
        int dy = fy - mScroller.getFinalY();
        mSmoothScrollBy(dx, dy);
    }

    protected void mSmoothScrollBy(int dx, int dy) {
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
        invalidate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (disallowIntercept) {
            return super.dispatchTouchEvent(ev);
        }
        detector.onTouchEvent(ev);
        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch(action) {
            case MotionEvent.ACTION_POINTER_DOWN:
                oldY = 0;
                oldX = 0;
                break;
            case MotionEvent.ACTION_DOWN:
                flingRunnable.abort();
                downY = ev.getY();
                oldY = 0;
                dealtY = mScroller.getCurrY();
                if (dealtY == 0) {
                    isVerticalMove = false;
                } else {
                    shouldSetScrollerStart = true;
                    abortScroller = true;
                    mScroller.abortAnimation();
                }
                downX = ev.getX();
                oldX = 0;
                dealtX = mScroller.getCurrX();
                if (dealtX == 0) {
                    isHorizontallyMove = false;
                } else {
                    shouldSetScrollerStart = true;
                    abortScroller = true;
                    mScroller.abortAnimation();
                }
                if (isOverScrollTop || isOverScrollBottom || isOverScrollLeft || isOverScrollRight) {
                    return true;
                }
                checkCanOverScrollDirection();
                break;
            case MotionEvent.ACTION_MOVE:
                if (!canOverScroll()) {
                    return super.dispatchTouchEvent(ev);
                }
                if (canOverScrollVertical) {
                    if (isOverScrollTop || isOverScrollBottom) {
                        if (onOverScrollListener != null) {
                            if (isOverScrollTop) {
                                onOverScrollListener.onTopOverScroll();
                            }
                            if (isOverScrollBottom) {
                                onOverScrollListener.onBottomOverScroll();
                            }
                        }
                        if (shouldSetScrollerStart) {
                            shouldSetScrollerStart = false;
                            mScroller.startScroll(dealtX, dealtY, 0, 0);
                        }
                        if (oldY == 0) {
                            oldY = ev.getY();
                            return true;
                        }
                        dealtY += getDealt(oldY - ev.getY(), dealtY);
                        oldY = ev.getY();
                        if (isOverScrollTop && dealtY > 0) {
                            dealtY = 0;
                        }
                        if (isOverScrollBottom && dealtY < 0) {
                            dealtY = 0;
                        }
                        overScroll(dealtX, dealtY);
                        if ((isOverScrollTop && dealtY == 0 && !isOverScrollBottom) || (isOverScrollBottom && dealtY == 0 && !isOverScrollTop)) {
                            oldY = 0;
                            isOverScrollTop = false;
                            isOverScrollBottom = false;
                            if (!isChildCanScrollVertical()) {
                                return true;
                            }
                            return super.dispatchTouchEvent(resetVertical(ev));
                        }
                        return true;
                    } else {
                        checkMoveDirection(ev.getX(), ev.getY());
                        if (oldY == 0) {
                            oldY = ev.getY();
                            return true;
                        }
                        boolean tempOverScrollTop = isTopOverScroll(ev.getY());
                        if (!isOverScrollTop && tempOverScrollTop) {
                            oldY = ev.getY();
                            isOverScrollTop = tempOverScrollTop;
                            ev.setAction(MotionEvent.ACTION_CANCEL);
                            super.dispatchTouchEvent(ev);
                            return true;
                        }
                        isOverScrollTop = tempOverScrollTop;
                        boolean tempOverScrollBottom = isBottomOverScroll(ev.getY());
                        if (!isOverScrollBottom && tempOverScrollBottom) {
                            oldY = ev.getY();
                            isOverScrollBottom = tempOverScrollBottom;
                            ev.setAction(MotionEvent.ACTION_CANCEL);
                            super.dispatchTouchEvent(ev);
                            return true;
                        }
                        isOverScrollBottom = tempOverScrollBottom;
                        oldY = ev.getY();
                    }
                } else if (canOverScrollHorizontally) {
                    if (isOverScrollLeft || isOverScrollRight) {
                        if (onOverScrollListener != null) {
                            if (isOverScrollLeft) {
                                onOverScrollListener.onLeftOverScroll();
                            }
                            if (isOverScrollRight) {
                                onOverScrollListener.onRightOverScroll();
                            }
                        }
                        if (shouldSetScrollerStart) {
                            shouldSetScrollerStart = false;
                            mScroller.startScroll(dealtX, dealtY, 0, 0);
                        }
                        if (oldX == 0) {
                            oldX = ev.getX();
                            return true;
                        }
                        dealtX += getDealt(oldX - ev.getX(), dealtX);
                        oldX = ev.getX();
                        if (isOverScrollLeft && dealtX > 0) {
                            dealtX = 0;
                        }
                        if (isOverScrollRight && dealtX < 0) {
                            dealtX = 0;
                        }
                        overScroll(dealtX, dealtY);
                        if ((isOverScrollLeft && dealtX == 0 && !isOverScrollRight) || (isOverScrollRight && dealtX == 0 && !isOverScrollLeft)) {
                            oldX = 0;
                            isOverScrollRight = false;
                            isOverScrollLeft = false;
                            if (!isChildCanScrollHorizontally()) {
                                return true;
                            }
                            return super.dispatchTouchEvent(resetHorizontally(ev));
                        }
                        return true;
                    } else {
                        checkMoveDirection(ev.getX(), ev.getY());
                        if (oldX == 0) {
                            oldX = ev.getX();
                            return true;
                        }
                        boolean tempOverScrollLeft = isLeftOverScroll(ev.getX());
                        if (!isOverScrollLeft && tempOverScrollLeft) {
                            oldX = ev.getX();
                            isOverScrollLeft = tempOverScrollLeft;
                            ev.setAction(MotionEvent.ACTION_CANCEL);
                            super.dispatchTouchEvent(ev);
                            return true;
                        }
                        isOverScrollLeft = tempOverScrollLeft;
                        boolean tempOverScrollRight = isRightOverScroll(ev.getX());
                        if (!isOverScrollRight && tempOverScrollRight) {
                            oldX = ev.getX();
                            isOverScrollRight = tempOverScrollRight;
                            ev.setAction(MotionEvent.ACTION_CANCEL);
                            super.dispatchTouchEvent(ev);
                            return true;
                        }
                        isOverScrollRight = tempOverScrollRight;
                        oldX = ev.getX();
                    }
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                oldY = 0;
                oldX = 0;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                finishOverScroll = true;
                mSmoothScrollTo(0, 0);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    private float getDealt(float dealt, float distance) {
        if (dealt * distance < 0)
            return dealt;
        float x = (float) Math.min(Math.max(Math.abs(distance), 0.1) / Math.abs(baseOverScrollLength), 1);
        float y = Math.min(new AccelerateInterpolator(0.15f).getInterpolation(x), 1);
        return dealt * (1 - y);
    }

    private MotionEvent resetVertical(MotionEvent event) {
        oldY = 0;
        dealtY = 0;
        event.setAction(MotionEvent.ACTION_DOWN);
        super.dispatchTouchEvent(event);
        event.setAction(MotionEvent.ACTION_MOVE);
        return event;
    }

    private MotionEvent resetHorizontally(MotionEvent event) {
        oldX = 0;
        dealtX = 0;
        event.setAction(MotionEvent.ACTION_DOWN);
        super.dispatchTouchEvent(event);
        event.setAction(MotionEvent.ACTION_MOVE);
        return event;
    }

    private boolean canOverScroll() {
        return child != null;
    }

    private void overScroll(int dealtX, int dealtY) {
        mSmoothScrollTo(dealtX, dealtY);
    }

    private boolean isTopOverScroll(float currentY) {
        if (isOverScrollTop) {
            return true;
        }
        if (!topOverScrollEnable || !isVerticalMove) {
            return false;
        }
        float dealtY = oldY - currentY;
        return dealtY < 0 && !canChildScrollUp();
    }

    private boolean isBottomOverScroll(float currentY) {
        if (isOverScrollBottom) {
            return true;
        }
        if (!bottomOverScrollEnable || !isVerticalMove) {
            return false;
        }
        float dealtY = oldY - currentY;
        return dealtY > 0 && !canChildScrollDown();
    }

    private boolean isLeftOverScroll(float currentX) {
        if (isOverScrollLeft) {
            return true;
        }
        if (!leftOverScrollEnable || !isHorizontallyMove) {
            return false;
        }
        float dealtX = oldX - currentX;
        return dealtX < 0 && !canChildScrollLeft();
    }

    private boolean isRightOverScroll(float currentX) {
        if (!rightOverScrollEnable || !isHorizontallyMove) {
            return false;
        }
        float dealtX = oldX - currentX;
        return dealtX > 0 && !canChildScrollRight();
    }

    private boolean isChildCanScrollVertical() {
        return canChildScrollDown() || canChildScrollUp();
    }

    private boolean isChildCanScrollHorizontally() {
        return canChildScrollLeft() || canChildScrollRight();
    }

    private void checkMoveDirection(float currentX, float currentY) {
        if (isVerticalMove || isHorizontallyMove) {
            return;
        }
        if (canOverScrollVertical) {
            float dealtY = currentY - downY;
            isVerticalMove = Math.abs(dealtY) >= configuration.getScaledTouchSlop();
        } else if (canOverScrollHorizontally) {
            float dealtX = currentX - downX;
            isHorizontallyMove = Math.abs(dealtX) >= configuration.getScaledTouchSlop();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }

    /**
     * 是否能下拉
     *
     * @return
     */
    private boolean canChildScrollUp() {
        if (checkListener != null) {
            return checkListener.canScrollUp();
        }
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (child instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) child;
                return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
            }
        }
        return ViewCompat.canScrollVertically(child, -1);
    }

    /**
     * 是否能上拉
     *
     * @return
     */
    private boolean canChildScrollDown() {
        if (checkListener != null) {
            return checkListener.canScrollDown();
        }
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (child instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) child;
                return absListView.getChildCount() > 0 && (absListView.getLastVisiblePosition() < absListView.getChildCount() - 1 || absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getHeight() - absListView.getPaddingBottom());
            }
        }
        return ViewCompat.canScrollVertically(child, 1);
    }

    /**
     * 是否能左拉
     *
     * @return
     */
    private boolean canChildScrollLeft() {
        if (checkListener != null) {
            return checkListener.canScrollLeft();
        }
        return ViewCompat.canScrollHorizontally(child, -1);
    }

    /**
     * 是否能右拉
     *
     * @return
     */
    private boolean canChildScrollRight() {
        if (checkListener != null) {
            return checkListener.canScrollRight();
        }
        return ViewCompat.canScrollHorizontally(child, 1);
    }

    private void startOverScrollAim(float currVelocity) {
        float speed = currVelocity / configuration.getScaledMaximumFlingVelocity();
        if (canOverScrollVertical) {
            if (!canChildScrollUp()) {
                overScrollRunnable.start(0, -speed);
            } else {
                overScrollRunnable.start(0, speed);
            }
        } else {
            if (canChildScrollRight()) {
                overScrollRunnable.start(-speed, 0);
            } else {
                overScrollRunnable.start(speed, 0);
            }
        }
    }

    private clreplaced OverScrollRunnable implements Runnable {

        private static final long DELAY_TIME = 20;

        private long duration = 160;

        private float speedX, speedY;

        private long timePreplaced;

        private long startTime;

        private int distanceX, distanceY;

        public void start(float speedX, float speedY) {
            this.speedX = speedX;
            this.speedY = speedY;
            startTime = System.currentTimeMillis();
            run();
        }

        @Override
        public void run() {
            timePreplaced = System.currentTimeMillis() - startTime;
            if (timePreplaced < duration) {
                distanceY = (int) (DELAY_TIME * speedY);
                distanceX = (int) (DELAY_TIME * speedX);
                mSmoothScrollBy(distanceX, distanceY);
                postDelayed(this, DELAY_TIME);
            } else if (timePreplaced > duration) {
                mSmoothScrollTo(0, 0);
            }
        }
    }

    private clreplaced FlingRunnable implements Runnable {

        private static final long DELAY_TIME = 40;

        private boolean abort;

        private int mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();

        public void start(float velocityX, float velocityY) {
            abort = false;
            float velocity = canOverScrollVertical ? velocityY : velocityX;
            flingScroller.fling(0, 0, 0, (int) velocity, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
            postDelayed(this, 40);
        }

        @Override
        public void run() {
            if (!abort && flingScroller.computeScrollOffset()) {
                boolean scrollEnd = false;
                if (canOverScrollVertical) {
                    scrollEnd = !canChildScrollDown() || !canChildScrollUp();
                } else {
                    scrollEnd = !canChildScrollLeft() || !canChildScrollRight();
                }
                float currVelocity = flingScroller.getCurrVelocity();
                if (scrollEnd) {
                    if (currVelocity > mMinimumFlingVelocity) {
                        startOverScrollAim(currVelocity);
                    }
                } else {
                    if (currVelocity > mMinimumFlingVelocity) {
                        postDelayed(this, DELAY_TIME);
                    }
                }
            }
        }

        public void abort() {
            abort = true;
        }
    }
}

19 Source : SpringView.java
with Apache License 2.0
from zuoweitan

/**
 * Created by liaoinstan on 2016/3/11.
 */
public clreplaced SpringView extends ViewGroup {

    private Context context;

    private LayoutInflater inflater;

    private OverScroller mScroller;

    // 监听回调
    private OnFreshListener listener;

    // 用于判断是否在下拉时到达临界点
    private boolean isCallDown = false;

    // 用于判断是否在上拉时到达临界点
    private boolean isCallUp = false;

    // 用于判断是否是拖动动作的第一次move
    private boolean isFirst = true;

    // 是否需要改变样式
    private boolean needChange = false;

    // 是否需要弹回的动画
    private boolean needResetAnim = false;

    // 是否超过一屏时才允许上拉,为false则不满一屏也可以上拉,注意样式为isOverlap时,无论如何也不允许在不满一屏时上拉
    private boolean isFullEnable = false;

    // 当前是否正在拖动
    private boolean isMoveNow = false;

    private long lastMoveTime;

    // 是否禁用(默认可用)
    private boolean enable = true;

    private int MOVE_TIME = 400;

    private int MOVE_TIME_OVER = 200;

    // 是否需要回调接口:TOP 只回调刷新、BOTTOM 只回调加载更多、BOTH 都需要、NONE 都不
    public enum Give {

        BOTH, TOP, BOTTOM, NONE
    }

    public enum Type {

        OVERLAP, FOLLOW
    }

    private Give give = Give.BOTH;

    private Type type = Type.OVERLAP;

    private Type _type;

    // private boolean i1sOverlap = true;       //默认是重叠的样式
    // private boolean _i1sOverlap;             //保存用户动态设置样式时传入的参数
    // 移动参数:计算手指移动量的时候会用到这个值,值越大,移动量越小,若值为1则手指移动多少就滑动多少px
    private final double MOVE_PARA = 2;

    // 最大拉动距离,拉动距离越靠近这个值拉动就越缓慢
    private int MAX_HEADER_PULL_HEIGHT = 600;

    private int MAX_FOOTER_PULL_HEIGHT = 600;

    // 拉动多少距离被认定为刷新(加载)动作
    private int HEADER_LIMIT_HEIGHT;

    private int FOOTER_LIMIT_HEIGHT;

    private int HEADER_SPRING_HEIGHT;

    private int FOOTER_SPRING_HEIGHT;

    // 储存上次的Y坐标
    private float mLastY;

    private float mLastX;

    // 储存第一次的Y坐标
    private float mfirstY;

    // 储存手指拉动的总距离
    private float dsY;

    // 滑动事件目前是否在本控件的控制中
    private boolean isInControl = false;

    // 存储拉动前的位置
    private Rect mRect = new Rect();

    // 头尾内容布局
    private View header;

    private View footer;

    private View contentView;

    public SpringView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        inflater = LayoutInflater.from(context);
        mScroller = new OverScroller(context);
        // 获取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SpringView);
        if (ta.hasValue(R.styleable.SpringView_type)) {
            int type_int = ta.getInt(R.styleable.SpringView_type, 0);
            type = Type.values()[type_int];
        }
        if (ta.hasValue(R.styleable.SpringView_give)) {
            int give_int = ta.getInt(R.styleable.SpringView_give, 0);
            give = Give.values()[give_int];
        }
        if (ta.hasValue(R.styleable.SpringView_header)) {
            headerResoureId = ta.getResourceId(R.styleable.SpringView_header, 0);
        }
        if (ta.hasValue(R.styleable.SpringView_footer)) {
            footerResoureId = ta.getResourceId(R.styleable.SpringView_footer, 0);
        }
        ta.recycle();
    }

    private int headerResoureId;

    private int footerResoureId;

    @Override
    protected void onFinishInflate() {
        contentView = getChildAt(0);
        if (contentView == null) {
            return;
        }
        setPadding(0, 0, 0, 0);
        contentView.setPadding(0, 0, 0, 0);
        if (headerResoureId != 0) {
            inflater.inflate(headerResoureId, this, true);
            header = getChildAt(getChildCount() - 1);
        }
        if (footerResoureId != 0) {
            inflater.inflate(footerResoureId, this, true);
            footer = getChildAt(getChildCount() - 1);
            footer.setVisibility(INVISIBLE);
        }
        // 把内容放在最前端
        contentView.bringToFront();
        super.onFinishInflate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
        // 如果是动态设置的头部,则使用动态设置的参数
        if (headerHander != null) {
            // 设置下拉最大高度,只有在>0时才生效,否则使用默认值
            int xh = headerHander.getDragMaxHeight(header);
            if (xh > 0)
                MAX_HEADER_PULL_HEIGHT = xh;
            // 设置下拉临界高度,只有在>0时才生效,否则默认为header的高度
            int h = headerHander.getDragLimitHeight(header);
            HEADER_LIMIT_HEIGHT = h > 0 ? h : header.getMeasuredHeight();
            // 设置下拉弹动高度,只有在>0时才生效,否则默认和临界高度一致
            int sh = headerHander.getDragSpringHeight(header);
            HEADER_SPRING_HEIGHT = sh > 0 ? sh : HEADER_LIMIT_HEIGHT;
        } else {
            // 不是动态设置的头部,设置默认值
            if (header != null)
                HEADER_LIMIT_HEIGHT = header.getMeasuredHeight();
            HEADER_SPRING_HEIGHT = HEADER_LIMIT_HEIGHT;
        }
        // 设置尾部参数,和上面一样
        if (footerHander != null) {
            int xh = footerHander.getDragMaxHeight(footer);
            if (xh > 0)
                MAX_FOOTER_PULL_HEIGHT = xh;
            int h = footerHander.getDragLimitHeight(footer);
            FOOTER_LIMIT_HEIGHT = h > 0 ? h : footer.getMeasuredHeight();
            int sh = footerHander.getDragSpringHeight(footer);
            FOOTER_SPRING_HEIGHT = sh > 0 ? sh : FOOTER_LIMIT_HEIGHT;
        } else {
            if (footer != null)
                FOOTER_LIMIT_HEIGHT = footer.getMeasuredHeight();
            FOOTER_SPRING_HEIGHT = FOOTER_LIMIT_HEIGHT;
        }
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (contentView != null) {
            if (type == Type.OVERLAP) {
                if (header != null) {
                    header.layout(0, 0, getWidth(), header.getMeasuredHeight());
                }
                if (footer != null) {
                    footer.layout(0, getHeight() - footer.getMeasuredHeight(), getWidth(), getHeight());
                }
            } else if (type == Type.FOLLOW) {
                if (header != null) {
                    header.layout(0, -header.getMeasuredHeight(), getWidth(), 0);
                }
                if (footer != null) {
                    footer.layout(0, getHeight(), getWidth(), getHeight() + footer.getMeasuredHeight());
                }
            }
            contentView.layout(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
        }
    }

    private float dy;

    private float dx;

    private boolean isNeedMyMove;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        dealMulTouchEvent(event);
        int action = event.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                hasCallFull = false;
                hasCallRefresh = false;
                mfirstY = event.getY();
                boolean isTop = isChildScrollToTop();
                boolean isBottom = isChildScrollToBottomFull(isFullEnable);
                if (isTop || isBottom)
                    isNeedMyMove = false;
                break;
            case MotionEvent.ACTION_MOVE:
                dsY += dy;
                isMoveNow = true;
                isNeedMyMove = isNeedMyMove();
                if (isNeedMyMove && !isInControl) {
                    // 把内部控件的事件转发给本控件处理
                    isInControl = true;
                    event.setAction(MotionEvent.ACTION_CANCEL);
                    MotionEvent ev2 = MotionEvent.obtain(event);
                    dispatchTouchEvent(event);
                    ev2.setAction(MotionEvent.ACTION_DOWN);
                    return dispatchTouchEvent(ev2);
                }
                break;
            case MotionEvent.ACTION_UP:
                isMoveNow = false;
                lastMoveTime = System.currentTimeMillis();
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return isNeedMyMove && enable;
    // int action = event.getAction();
    // switch (action){
    // case MotionEvent.ACTION_MOVE:
    // return isNeedMyMove;
    // }
    // return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (contentView == null) {
            return false;
        }
        int action = event.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                isFirst = true;
                // if (!mScroller.isFinished()) mScroller.abortAnimation();//不需要处理
                break;
            case MotionEvent.ACTION_MOVE:
                if (isNeedMyMove) {
                    // 按下的时候关闭回弹
                    needResetAnim = false;
                    // 执行位移操作
                    doMove();
                    // 下拉的时候显示header并隐藏footer,上拉的时候相反
                    if (isTop()) {
                        if (header != null && header.getVisibility() != View.VISIBLE)
                            header.setVisibility(View.VISIBLE);
                        if (footer != null && footer.getVisibility() != View.INVISIBLE)
                            footer.setVisibility(View.INVISIBLE);
                    } else if (isBottom()) {
                        if (header != null && header.getVisibility() != View.INVISIBLE)
                            header.setVisibility(View.INVISIBLE);
                        if (footer != null && footer.getVisibility() != View.VISIBLE)
                            footer.setVisibility(View.VISIBLE);
                    }
                    // 回调onDropAnim接口
                    callOnDropAnim();
                    // 回调callOnPreDrag接口
                    callOnPreDrag();
                    // 回调onLimitDes接口
                    callOnLimitDes();
                    isFirst = false;
                } else {
                    // 手指在产生移动的时候(dy!=0)才重置位置
                    if (dy != 0 && isFlow()) {
                        resetPosition();
                        // 把滚动事件交给内部控件处理
                        event.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(event);
                        isInControl = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                last_top = 0;
                // 松开的时候打开回弹
                needResetAnim = true;
                isFirst = true;
                _firstDrag = true;
                restSmartPosition();
                dsY = 0;
                dy = 0;
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

    /**
     * 处理多点触控的情况,准确地计算Y坐标和移动距离dy
     * 同时兼容单点触控的情况
     */
    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;

    public void dealMulTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    mLastX = x;
                    mLastY = y;
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    dx = x - mLastX;
                    dy = y - mLastY;
                    mLastY = y;
                    mLastX = x;
                    break;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                {
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    if (pointerId != mActivePointerId) {
                        mLastX = MotionEventCompat.getX(ev, pointerIndex);
                        mLastY = MotionEventCompat.getY(ev, pointerIndex);
                        mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    }
                    break;
                }
            case MotionEvent.ACTION_POINTER_UP:
                {
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    if (pointerId == mActivePointerId) {
                        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                        mLastX = MotionEventCompat.getX(ev, newPointerIndex);
                        mLastY = MotionEventCompat.getY(ev, newPointerIndex);
                        mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
                    }
                    break;
                }
        }
    }

    private int last_top;

    private void doMove() {
        if (type == Type.OVERLAP) {
            // 记录移动前的位置
            if (mRect.isEmpty()) {
                mRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
            }
            // 根据下拉高度计算位移距离,(越拉越慢)
            int movedy;
            if (dy > 0) {
                movedy = (int) ((MAX_HEADER_PULL_HEIGHT - contentView.getTop()) / (float) MAX_HEADER_PULL_HEIGHT * dy / MOVE_PARA);
            } else {
                movedy = (int) ((MAX_FOOTER_PULL_HEIGHT - (getHeight() - contentView.getBottom())) / (float) MAX_FOOTER_PULL_HEIGHT * dy / MOVE_PARA);
            }
            int top = contentView.getTop() + movedy;
            contentView.layout(contentView.getLeft(), top, contentView.getRight(), top + contentView.getMeasuredHeight());
        } else if (type == Type.FOLLOW) {
            // 根据下拉高度计算位移距离,(越拉越慢)
            int movedx;
            if (dy > 0) {
                movedx = (int) ((MAX_HEADER_PULL_HEIGHT + getScrollY()) / (float) MAX_HEADER_PULL_HEIGHT * dy / MOVE_PARA);
            } else {
                movedx = (int) ((MAX_FOOTER_PULL_HEIGHT - getScrollY()) / (float) MAX_FOOTER_PULL_HEIGHT * dy / MOVE_PARA);
            }
            scrollBy(0, -movedx);
        }
    }

    private void callOnDropAnim() {
        if (type == Type.OVERLAP) {
            if (contentView.getTop() > 0)
                if (headerHander != null)
                    headerHander.onDropAnim(header, contentView.getTop());
            if (contentView.getTop() < 0)
                if (footerHander != null)
                    footerHander.onDropAnim(footer, contentView.getTop());
        } else if (type == Type.FOLLOW) {
            if (getScrollY() < 0)
                if (headerHander != null)
                    headerHander.onDropAnim(header, -getScrollY());
            if (getScrollY() > 0)
                if (footerHander != null)
                    footerHander.onDropAnim(footer, -getScrollY());
        }
    }

    private boolean _firstDrag = true;

    private void callOnPreDrag() {
        if (_firstDrag) {
            if (isTop()) {
                if (headerHander != null)
                    headerHander.onPreDrag(header);
                _firstDrag = false;
            } else if (isBottom()) {
                if (footerHander != null)
                    footerHander.onPreDrag(footer);
                _firstDrag = false;
            }
        }
    }

    private void callOnLimitDes() {
        boolean topORbottom = false;
        if (type == Type.OVERLAP) {
            topORbottom = contentView.getTop() >= 0 && isChildScrollToTop();
        } else if (type == Type.FOLLOW) {
            topORbottom = getScrollY() <= 0 && isChildScrollToTop();
        }
        if (isFirst) {
            if (topORbottom) {
                isCallUp = true;
                isCallDown = false;
            } else {
                isCallUp = false;
                isCallDown = true;
            }
        }
        if (dy == 0)
            return;
        boolean upORdown = dy < 0;
        if (topORbottom) {
            if (!upORdown) {
                if ((isTopOverFarm()) && !isCallDown) {
                    isCallDown = true;
                    if (headerHander != null)
                        headerHander.onLimitDes(header, upORdown);
                    isCallUp = false;
                }
            } else {
                if (!isTopOverFarm() && !isCallUp) {
                    isCallUp = true;
                    if (headerHander != null)
                        headerHander.onLimitDes(header, upORdown);
                    isCallDown = false;
                }
            }
        } else {
            if (upORdown) {
                if (isBottomOverFarm() && !isCallUp) {
                    isCallUp = true;
                    if (footerHander != null)
                        footerHander.onLimitDes(footer, upORdown);
                    isCallDown = false;
                }
            } else {
                if (!isBottomOverFarm() && !isCallDown) {
                    isCallDown = true;
                    if (footerHander != null)
                        footerHander.onLimitDes(footer, upORdown);
                    isCallUp = false;
                }
            }
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            invalidate();
        }
        // 在滚动动画完全结束后回调接口
        // 滚动回调过程中mScroller.isFinished会多次返回true,导致判断条件被多次进入,设置标志位保证只调用一次
        if (!isMoveNow && type == Type.FOLLOW && mScroller.isFinished()) {
            if (isFullAnim) {
                if (!hasCallFull) {
                    hasCallFull = true;
                    callOnAfterFullAnim();
                }
            } else {
                if (!hasCallRefresh) {
                    hasCallRefresh = true;
                    callOnAfterRefreshAnim();
                }
            }
        }
        if (mScroller.isFinished() && mScroller.getCurrY() == 0 && type == Type.FOLLOW) {
            callOnPositionReset();
        }
    }

    private void callOnPositionReset() {
        if ((type == Type.FOLLOW && getScrollY() == 0) || (type == Type.OVERLAP && contentView.getTop() == 0)) {
            if (headerHander != null) {
                headerHander.onPositionReset();
            }
        }
    }

    private int callFreshORload = 0;

    private boolean isFullAnim;

    private boolean hasCallFull = false;

    private boolean hasCallRefresh = false;

    /**
     * 判断是否需要由该控件来控制滑动事件
     */
    private boolean isNeedMyMove() {
        if (contentView == null) {
            return false;
        }
        if (Math.abs(dy) < Math.abs(dx)) {
            return false;
        }
        boolean isTop = isChildScrollToTop();
        // false不满一屏也算在底部,true不满一屏不算在底部
        boolean isBottom = isChildScrollToBottomFull(isFullEnable);
        if (type == Type.OVERLAP) {
            if (header != null) {
                if (isTop && dy > 0 || contentView.getTop() > 0 + 20) {
                    return true;
                }
            }
            if (footer != null) {
                if (isBottom && dy < 0 || contentView.getBottom() < mRect.bottom - 20) {
                    // if (isFullScrean()&&!isFullEnable)
                    // return true;
                    // else
                    // return false;
                    return true;
                }
            }
        } else if (type == Type.FOLLOW) {
            if (header != null) {
                // 其中的20是一个防止触摸误差的偏移量
                if (isTop && dy > 0 || getScrollY() < 0 - 20) {
                    return true;
                }
            }
            if (footer != null) {
                if (isBottom && dy < 0 || getScrollY() > 0 + 20) {
                    return true;
                }
            }
        }
        return false;
    }

    private void callOnAfterFullAnim() {
        if (callFreshORload != 0) {
            callOnFinishAnim();
        }
        if (needChangeHeader) {
            needChangeHeader = false;
            setHeaderIn(_headerHander);
        }
        if (needChangeFooter) {
            needChangeFooter = false;
            setFooterIn(_footerHander);
        }
        // 动画完成后检查是否需要切换type,是则切换
        if (needChange) {
            changeType(_type);
        }
    }

    private void callOnAfterRefreshAnim() {
        if (type == Type.FOLLOW) {
            if (isTop()) {
                listener.onRefresh();
            } else if (isBottom()) {
                listener.onLoadmore();
            }
        } else if (type == Type.OVERLAP) {
            if (!isMoveNow) {
                long nowtime = System.currentTimeMillis();
                if (nowtime - lastMoveTime >= MOVE_TIME_OVER) {
                    if (callFreshORload == 1)
                        listener.onRefresh();
                    if (callFreshORload == 2)
                        listener.onLoadmore();
                } else {
                    onFinishFreshAndLoad();
                }
            }
        }
    }

    /**
     * 重置控件位置到初始状态
     */
    private void resetPosition() {
        isFullAnim = true;
        // 重置位置的时候,滑动事件已经不在控件的控制中了
        isInControl = false;
        if (type == Type.OVERLAP) {
            if (mRect.bottom == 0 || mRect.right == 0)
                return;
            // 根据下拉高度计算弹回时间,时间最小100,最大400
            int time = 0;
            if (contentView.getHeight() > 0) {
                time = Math.abs(400 * contentView.getTop() / contentView.getHeight());
            }
            if (time < 100)
                time = 100;
            TranslateAnimation animation = new HTranslateAnimation(0, 0, contentView.getTop(), mRect.top);
            animation.setDuration(time);
            animation.setFillAfter(true);
            animation.setAnimationListener(new HTranslateAnimation.OnTranslateListener() {

                @Override
                public void onTranslateChanged(float dx, float dy) {
                    contentView.layout(mRect.left, (int) dy, mRect.right, mRect.bottom);
                    callOnDropAnim();
                }

                @Override
                public void onAnimationStart(Animation animation) {
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    contentView.clearAnimation();
                    callOnAfterFullAnim();
                    callOnPositionReset();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                }
            });
            contentView.startAnimation(animation);
        } else if (type == Type.FOLLOW) {
            mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), MOVE_TIME);
            invalidate();
        }
    // mRect.setEmpty();
    }

    private void callOnFinishAnim() {
        if (callFreshORload != 0) {
            if (callFreshORload == 1) {
                if (headerHander != null)
                    headerHander.onFinishAnim();
                if (give == Give.BOTTOM || give == Give.NONE) {
                    listener.onRefresh();
                }
            } else if (callFreshORload == 2) {
                if (footerHander != null)
                    footerHander.onFinishAnim();
                if (give == Give.TOP || give == Give.NONE) {
                    listener.onLoadmore();
                }
            }
            callFreshORload = 0;
        }
    }

    /**
     * 重置控件位置到刷新状态(或加载状态)
     */
    private void resetRefreshPosition() {
        isFullAnim = false;
        // 重置位置的时候,滑动事件已经不在控件的控制中了
        isInControl = false;
        if (type == Type.OVERLAP) {
            if (mRect.bottom == 0 || mRect.right == 0)
                return;
            if (contentView.getTop() > mRect.top) {
                // 下拉
                Animation animation = new TranslateAnimation(0, 0, contentView.getTop() - HEADER_SPRING_HEIGHT, mRect.top);
                animation.setDuration(MOVE_TIME_OVER);
                animation.setFillAfter(true);
                animation.setAnimationListener(new Animation.AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        callOnAfterRefreshAnim();
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }
                });
                contentView.startAnimation(animation);
                contentView.layout(mRect.left, mRect.top + HEADER_SPRING_HEIGHT, mRect.right, mRect.bottom + HEADER_SPRING_HEIGHT);
            } else {
                // 上拉
                Animation animation = new TranslateAnimation(0, 0, contentView.getTop() + FOOTER_SPRING_HEIGHT, mRect.top);
                animation.setDuration(MOVE_TIME_OVER);
                animation.setFillAfter(true);
                animation.setAnimationListener(new Animation.AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        callOnAfterRefreshAnim();
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }
                });
                contentView.startAnimation(animation);
                contentView.layout(mRect.left, mRect.top - FOOTER_SPRING_HEIGHT, mRect.right, mRect.bottom - FOOTER_SPRING_HEIGHT);
            }
        } else if (type == Type.FOLLOW) {
            if (getScrollY() < 0) {
                // 下拉
                mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - HEADER_SPRING_HEIGHT, MOVE_TIME);
                invalidate();
            } else {
                // 上拉
                mScroller.startScroll(0, getScrollY(), 0, -getScrollY() + FOOTER_SPRING_HEIGHT, MOVE_TIME);
                invalidate();
            }
        }
    }

    public void callFresh() {
        header.setVisibility(VISIBLE);
        if (type == Type.OVERLAP) {
            if (mRect.isEmpty()) {
                mRect.set(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
            }
            Animation animation = new TranslateAnimation(0, 0, contentView.getTop() - HEADER_SPRING_HEIGHT, mRect.top);
            animation.setDuration(MOVE_TIME_OVER);
            animation.setFillAfter(true);
            animation.setAnimationListener(new Animation.AnimationListener() {

                @Override
                public void onAnimationStart(Animation animation) {
                    if (headerHander != null)
                        headerHander.onStartAnim();
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    callFreshORload = 1;
                    needResetAnim = true;
                    listener.onRefresh();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                }
            });
            contentView.startAnimation(animation);
            contentView.layout(mRect.left, mRect.top + HEADER_SPRING_HEIGHT, mRect.right, mRect.bottom + HEADER_SPRING_HEIGHT);
        } else if (type == Type.FOLLOW) {
            isFullAnim = false;
            hasCallRefresh = false;
            callFreshORload = 1;
            needResetAnim = true;
            if (headerHander != null)
                headerHander.onStartAnim();
            mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - HEADER_SPRING_HEIGHT, MOVE_TIME);
            invalidate();
        }
    }

    /**
     * 智能判断是重置控件位置到初始状态还是到刷新/加载状态
     */
    private void restSmartPosition() {
        if (listener == null) {
            resetPosition();
        } else {
            if (isTopOverFarm()) {
                callFreshORload();
                if (give == Give.BOTH || give == Give.TOP)
                    resetRefreshPosition();
                else
                    resetPosition();
            } else if (isBottomOverFarm()) {
                callFreshORload();
                if (give == Give.BOTH || give == Give.BOTTOM)
                    resetRefreshPosition();
                else
                    resetPosition();
            } else {
                resetPosition();
            }
        }
    }

    private void callFreshORload() {
        if (isTop()) {
            // 下拉
            callFreshORload = 1;
            if (type == Type.OVERLAP) {
                if (dsY > 200 || HEADER_LIMIT_HEIGHT >= HEADER_SPRING_HEIGHT) {
                    if (headerHander != null)
                        headerHander.onStartAnim();
                }
            } else if (type == Type.FOLLOW) {
                if (headerHander != null)
                    headerHander.onStartAnim();
            }
        } else if (isBottom()) {
            callFreshORload = 2;
            if (type == Type.OVERLAP) {
                if (dsY < -200 || FOOTER_LIMIT_HEIGHT >= FOOTER_SPRING_HEIGHT) {
                    if (footerHander != null)
                        footerHander.onStartAnim();
                }
            } else if (type == Type.FOLLOW) {
                if (footerHander != null)
                    footerHander.onStartAnim();
            }
        }
    }

    /**
     * 判断目标View是否滑动到顶部-还能否继续滑动
     *
     * @return
     */
    private boolean isChildScrollToTop() {
        // if (android.os.Build.VERSION.SDK_INT < 14) {
        // if (contentView instanceof AbsListView) {
        // final AbsListView absListView = (AbsListView) contentView;
        // return !(absListView.getChildCount() > 0 && (absListView
        // .getFirstVisiblePosition() > 0 || absListView
        // .getChildAt(0).getTop() < absListView.getPaddingTop()));
        // } else {
        // return !(contentView.getScrollY() > 0);
        // }
        // } else {
        // return !ViewCompat.canScrollVertically(contentView, -1);
        // }
        return !ViewCompat.canScrollVertically(contentView, -1);
    }

    /**
     * 是否滑动到底部
     * @return
     */
    private boolean isChildScrollToBottomFull(boolean isFull) {
        // if (isFull){
        // if (isChildScrollToTop()) {
        // return false;
        // }
        // }
        // if (contentView instanceof RecyclerView) {
        // RecyclerView recyclerView = (RecyclerView) contentView;
        // RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        // int count = recyclerView.getAdapter().gereplacedemCount();
        // if (layoutManager instanceof LinearLayoutManager && count > 0) {
        // LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
        // if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == count - 1) {
        // return true;
        // }
        // } else if (layoutManager instanceof StaggeredGridLayoutManager) {
        // StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
        // int[] lasreplacedems = new int[2];
        // staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(lasreplacedems);
        // int lasreplacedem = Math.max(lasreplacedems[0], lasreplacedems[1]);
        // if (lasreplacedem == count - 1) {
        // return true;
        // }
        // }
        // return false;
        // } else if (contentView instanceof AbsListView) {
        // final AbsListView absListView = (AbsListView) contentView;
        // final Adapter adapter = absListView.getAdapter();
        // if (null == adapter || adapter.isEmpty()) {
        // return true;
        // }
        // final int lasreplacedemPosition = adapter.getCount() - 1;
        // final int lastVisiblePosition = absListView.getLastVisiblePosition();
        // if (lastVisiblePosition >= lasreplacedemPosition - 1) {
        // final int childIndex = lastVisiblePosition - absListView.getFirstVisiblePosition();
        // final int childCount = absListView.getChildCount();
        // final int index = Math.max(childIndex, childCount - 1);
        // final View lastVisibleChild = absListView.getChildAt(index);
        // if (lastVisibleChild != null) {
        // return lastVisibleChild.getBottom() <= absListView.getBottom()-absListView.getTop();
        // }
        // }
        // return false;
        // } else if (contentView instanceof ScrollView) {
        // ScrollView scrollView = (ScrollView) contentView;
        // View view = scrollView.getChildAt(scrollView.getChildCount() - 1);
        // if (view != null) {
        // int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()));
        // if (diff == 0) {
        // return true;
        // }
        // if(!isFull) {
        // //如果scrollView中内容不满一屏,也算在底部
        // if (view.getMeasuredHeight() <= scrollView.getMeasuredHeight()) {
        // return true;
        // }
        // }
        // }
        // }
        // return false;
        return !ViewCompat.canScrollVertically(contentView, 1);
    }

    private boolean isChildScrollToBottom() {
        return isChildScrollToBottomFull(true);
    }

    private boolean isFullScrean() {
        boolean isBottom = isChildScrollToBottomFull(false);
        if (isBottom) {
            return isChildScrollToBottomFull(true);
        }
        return true;
    }

    /**
     * 判断顶部拉动是否超过临界值
     */
    private boolean isTopOverFarm() {
        if (type == Type.OVERLAP) {
            return contentView.getTop() > HEADER_LIMIT_HEIGHT;
        } else if (type == Type.FOLLOW) {
            return -getScrollY() > HEADER_LIMIT_HEIGHT;
        } else
            return false;
    }

    /**
     * 判断底部拉动是否超过临界值
     */
    private boolean isBottomOverFarm() {
        if (type == Type.OVERLAP) {
            return getHeight() - contentView.getBottom() > FOOTER_LIMIT_HEIGHT;
        } else if (type == Type.FOLLOW) {
            return getScrollY() > FOOTER_LIMIT_HEIGHT;
        } else
            return false;
    }

    /**
     * 判断当前状态是否拉动顶部
     */
    private boolean isTop() {
        if (type == Type.OVERLAP) {
            return contentView.getTop() > 0;
        } else if (type == Type.FOLLOW) {
            return getScrollY() < 0;
        } else
            return false;
    }

    private boolean isBottom() {
        if (type == Type.OVERLAP) {
            return contentView.getTop() < 0;
        } else if (type == Type.FOLLOW) {
            return getScrollY() > 0;
        } else
            return false;
    }

    private boolean isFlow() {
        if (type == Type.OVERLAP) {
            return contentView.getTop() < 30 && contentView.getTop() > -30;
        } else if (type == Type.FOLLOW) {
            return getScrollY() > -30 && getScrollY() < 30;
        } else
            return false;
    }

    /**
     * 切换Type的方法,之所以不暴露在外部,是防止用户在拖动过程中调用造成布局错乱
     * 所以在外部方法中设置标志,然后在拖动完毕后判断是否需要调用,是则调用
     */
    private void changeType(Type type) {
        this.type = type;
        if (header != null && header.getVisibility() != INVISIBLE)
            header.setVisibility(INVISIBLE);
        if (footer != null && footer.getVisibility() != INVISIBLE)
            footer.setVisibility(INVISIBLE);
        requestLayout();
        needChange = false;
    }

    // #############################################
    // ##            对外暴露的方法               ##
    // #############################################
    /**
     * 重置控件位置,暴露给外部的方法,用于在刷新或者加载完成后调用
     */
    public void onFinishFreshAndLoad() {
        if (!isMoveNow && needResetAnim) {
            boolean needTop = isTop() && (give == Give.TOP || give == Give.BOTH);
            boolean needBottom = isBottom() && (give == Give.BOTTOM || give == Give.BOTH);
            if (needTop || needBottom) {
                if (contentView instanceof ListView) {
                // ((ListView) contentView).smoothScrollByOffset(1);
                // 刷新后调用,才能正确显示刷新的item,如果调用上面的方法,listview会被固定在底部
                // ((ListView) contentView).smoothScrollBy(-1,0);
                }
                resetPosition();
            }
        }
    }

    public void setMoveTime(int time) {
        this.MOVE_TIME = time;
    }

    public void setMoveTimeOver(int time) {
        this.MOVE_TIME_OVER = time;
    }

    /**
     * 是否禁用SpringView
     */
    public void setEnable(boolean enable) {
        this.enable = enable;
    }

    public boolean isEnable() {
        return enable;
    }

    /**
     * 设置监听
     */
    public void setListener(OnFreshListener listener) {
        this.listener = listener;
    }

    /**
     * 动态设置弹性模式
     */
    public void setGive(Give give) {
        this.give = give;
    }

    /**
     * 改变样式的对外接口
     */
    public void setType(Type type) {
        if (isTop() || isBottom()) {
            // 如果当前用户正在拖动,直接调用changeType()会造成布局错乱
            // 设置needChange标志,在执行完拖动后再调用changeType()
            needChange = true;
            // 把参数保持起来
            _type = type;
        } else {
            changeType(type);
        }
    }

    /**
     * 获取当前样式
     */
    public Type getType() {
        return type;
    }

    /**
     * 回调接口
     */
    public interface OnFreshListener {

        /**
         * 下拉刷新,回调接口
         */
        void onRefresh();

        /**
         * 上拉加载,回调接口
         */
        void onLoadmore();
    }

    public View getHeaderView() {
        return header;
    }

    public View getFooterView() {
        return footer;
    }

    private boolean needChangeHeader = false;

    private boolean needChangeFooter = false;

    private DragHander _headerHander;

    private DragHander _footerHander;

    private DragHander headerHander;

    private DragHander footerHander;

    public DragHander getHeader() {
        return headerHander;
    }

    public DragHander getFooter() {
        return footerHander;
    }

    public void setHeader(DragHander headerHander) {
        if (this.headerHander != null && isTop()) {
            needChangeHeader = true;
            _headerHander = headerHander;
            resetPosition();
        } else {
            setHeaderIn(headerHander);
        }
    }

    private void setHeaderIn(DragHander headerHander) {
        this.headerHander = headerHander;
        if (header != null) {
            removeView(this.header);
        }
        headerHander.getView(inflater, this);
        this.header = getChildAt(getChildCount() - 1);
        // 把内容放在最前端
        contentView.bringToFront();
        requestLayout();
    }

    public void setFooter(DragHander footerHander) {
        if (this.footerHander != null && isBottom()) {
            needChangeFooter = true;
            _footerHander = footerHander;
            resetPosition();
        } else {
            setFooterIn(footerHander);
        }
    }

    private void setFooterIn(DragHander footerHander) {
        this.footerHander = footerHander;
        if (footer != null) {
            removeView(footer);
        }
        footerHander.getView(inflater, this);
        this.footer = getChildAt(getChildCount() - 1);
        // 把内容放在最前端
        contentView.bringToFront();
        requestLayout();
    }

    public interface DragHander {

        View getView(LayoutInflater inflater, ViewGroup viewGroup);

        void onPositionReset();

        int getDragLimitHeight(View rootView);

        int getDragMaxHeight(View rootView);

        int getDragSpringHeight(View rootView);

        void onPreDrag(View rootView);

        /**
         * 手指拖动控件过程中的回调,用户可以根据拖动的距离添加拖动过程动画
         * @param dy 拖动距离,下拉为+,上拉为-
         */
        void onDropAnim(View rootView, int dy);

        /**
         * 手指拖动控件过程中每次抵达临界点时的回调,用户可以根据手指方向设置临界动画
         * @param upORdown 是上拉还是下拉
         */
        void onLimitDes(View rootView, boolean upORdown);

        /**
         * 拉动超过临界点后松开时回调
         */
        void onStartAnim();

        /**
         * 头(尾)已经全部弹回时回调
         */
        void onFinishAnim();
    }
}

19 Source : ScrollableImageView.java
with MIT License
from zhangyangjing

/**
 * An extension to the standard Android {@link ImageView}, which makes it
 * respond to Scroll and Fling events. Uses a {@link GestureDetectorCompat} and
 * a {@link OverScroller} to provide scrolling functionality.
 *
 * @author EgorAnd
 */
public clreplaced ScrollableImageView extends ImageView {

    private GestureDetectorCompat gestureDetector;

    private OverScroller overScroller;

    private final int screenW;

    private final int screenH;

    private int positionX = 0;

    private int positionY = 0;

    public ScrollableImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        DisplayMetrics dm = getResources().getDisplayMetrics();
        screenW = dm.widthPixels;
        screenH = dm.heightPixels;
        gestureDetector = new GestureDetectorCompat(context, gestureListener);
        overScroller = new OverScroller(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        gestureDetector.onTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        // computeScrollOffset() returns true only when the scrolling isn't
        // already finished
        if (overScroller.computeScrollOffset()) {
            positionX = overScroller.getCurrX();
            positionY = overScroller.getCurrY();
            scrollTo(positionX, positionY);
        } else {
            // when scrolling is over, we will want to "spring back" if the
            // image is overscrolled
            overScroller.springBack(positionX, positionY, 0, getMaxHorizontal(), 0, getMaxVertical());
        }
    }

    private int getMaxHorizontal() {
        if (null == getDrawable())
            return 0;
        return (Math.abs(getDrawable().getBounds().width() - screenW));
    }

    private int getMaxVertical() {
        if (null == getDrawable())
            return 0;
        return (Math.abs(getDrawable().getBounds().height() - screenH));
    }

    private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {

        @Override
        public boolean onDown(MotionEvent e) {
            overScroller.forceFinished(true);
            ViewCompat.postInvalidateOnAnimation(ScrollableImageView.this);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            overScroller.forceFinished(true);
            overScroller.fling(positionX, positionY, (int) -velocityX, (int) -velocityY, 0, getMaxHorizontal(), 0, getMaxVertical());
            ViewCompat.postInvalidateOnAnimation(ScrollableImageView.this);
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            overScroller.forceFinished(true);
            // normalize scrolling distances to not overscroll the image
            int dx = (int) distanceX;
            int dy = (int) distanceY;
            int newPositionX = positionX + dx;
            int newPositionY = positionY + dy;
            if (newPositionX < 0) {
                dx -= newPositionX;
            } else if (newPositionX > getMaxHorizontal()) {
                dx -= (newPositionX - getMaxHorizontal());
            }
            if (newPositionY < 0) {
                dy -= newPositionY;
            } else if (newPositionY > getMaxVertical()) {
                dy -= (newPositionY - getMaxVertical());
            }
            overScroller.startScroll(positionX, positionY, dx, dy, 0);
            ViewCompat.postInvalidateOnAnimation(ScrollableImageView.this);
            return true;
        }
    };
}

19 Source : ScrollLayout.java
with Apache License 2.0
from Z-bm

/**
 * Created by zbm阿铭 on 2018/2/10.
 */
public clreplaced ScrollLayout extends RelativeLayout {

    private View mTop;

    private int mTopViewHeight;

    private OverScroller mScroller;

    public ScrollLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new OverScroller(context);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 这个id必须能找到
        mTop = findViewById(R.id.toolbar);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mTopViewHeight = mTop.getMeasuredHeight();
    }

    // 这里留出状态栏的高度
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > mTopViewHeight) {
            y = mTopViewHeight;
        }
        if (y != getScrollY()) {
            super.scrollTo(x, y);
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            invalidate();
        }
    }
}

19 Source : GalleryThumbnailView.java
with Apache License 2.0
from yuchuangu85

public clreplaced GalleryThumbnailView extends ViewGroup {

    public interface GalleryThumbnailAdapter extends ListAdapter {

        /**
         * @param position Position to get the intrinsic aspect ratio for
         * @return width / height
         */
        float getIntrinsicAspectRatio(int position);
    }

    private static final String TAG = "GalleryThumbnailView";

    private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f);

    private static final int LAND_UNITS = 2;

    private static final int PORT_UNITS = 3;

    private GalleryThumbnailAdapter mAdapter;

    private final RecycleBin mRecycler = new RecycleBin();

    private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();

    private boolean mDataChanged;

    private int mOldItemCount;

    private int mItemCount;

    private boolean mHreplacedtableIds;

    private int mFirstPosition;

    private boolean mPopulating;

    private boolean mInLayout;

    private int mTouchSlop;

    private int mMaximumVelocity;

    private int mFlingVelocity;

    private float mLastTouchX;

    private float mTouchRemainderX;

    private int mActivePointerId;

    private static final int TOUCH_MODE_IDLE = 0;

    private static final int TOUCH_MODE_DRAGGING = 1;

    private static final int TOUCH_MODE_FLINGING = 2;

    private int mTouchMode;

    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();

    private final OverScroller mScroller;

    private final EdgeEffectCompat mLeftEdge;

    private final EdgeEffectCompat mRightEdge;

    private int mLargeColumnWidth;

    private int mSmallColumnWidth;

    private int mLargeColumnUnitCount = 8;

    private int mSmallColumnUnitCount = 10;

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

    public GalleryThumbnailView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        final ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
        mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
        mFlingVelocity = vc.getScaledMinimumFlingVelocity();
        mScroller = new OverScroller(context);
        mLeftEdge = new EdgeEffectCompat(context);
        mRightEdge = new EdgeEffectCompat(context);
        setWillNotDraw(false);
        setClipToPadding(false);
    }

    @Override
    public void requestLayout() {
        if (!mPopulating) {
            super.requestLayout();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY) {
            Log.e(TAG, "onMeasure: must have an exact width or match_parent! " + "Using fallback spec of EXACTLY " + widthSize);
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + "Using fallback spec of EXACTLY " + heightSize);
        }
        setMeasuredDimension(widthSize, heightSize);
        float portSpaces = mLargeColumnUnitCount / PORT_UNITS;
        float height = getMeasuredHeight() / portSpaces;
        mLargeColumnWidth = (int) (height / ASPECT_RATIO);
        portSpaces++;
        height = getMeasuredHeight() / portSpaces;
        mSmallColumnWidth = (int) (height / ASPECT_RATIO);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mInLayout = true;
        populate();
        mInLayout = false;
        final int width = r - l;
        final int height = b - t;
        mLeftEdge.setSize(width, height);
        mRightEdge.setSize(width, height);
    }

    private void populate() {
        if (getWidth() == 0 || getHeight() == 0) {
            return;
        }
        // TODO: Handle size changing
        // final int colCount = mColCount;
        // if (mItemTops == null || mItemTops.length != colCount) {
        // mItemTops = new int[colCount];
        // mItemBottoms = new int[colCount];
        // final int top = getPaddingTop();
        // final int offset = top + Math.min(mRestoreOffset, 0);
        // Arrays.fill(mItemTops, offset);
        // Arrays.fill(mItemBottoms, offset);
        // mLayoutRecords.clear();
        // if (mInLayout) {
        // removeAllViewsInLayout();
        // } else {
        // removeAllViews();
        // }
        // mRestoreOffset = 0;
        // }
        mPopulating = true;
        layoutChildren(mDataChanged);
        fillRight(mFirstPosition + getChildCount(), 0);
        fillLeft(mFirstPosition - 1, 0);
        mPopulating = false;
        mDataChanged = false;
    }

    final void layoutChildren(boolean queryAdapter) {
    // TODO
    // final int childCount = getChildCount();
    // for (int i = 0; i < childCount; i++) {
    // View child = getChildAt(i);
    // 
    // if (child.isLayoutRequested()) {
    // final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY);
    // final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY);
    // child.measure(widthSpec, heightSpec);
    // child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
    // }
    // 
    // int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
    // mItemBottoms[col] + mItemMargin : child.getTop();
    // if (span > 1) {
    // int lowest = childTop;
    // for (int j = col + 1; j < col + span; j++) {
    // final int bottom = mItemBottoms[j] + mItemMargin;
    // if (bottom > lowest) {
    // lowest = bottom;
    // }
    // }
    // childTop = lowest;
    // }
    // final int childHeight = child.getMeasuredHeight();
    // final int childBottom = childTop + childHeight;
    // final int childLeft = paddingLeft + col * (colWidth + itemMargin);
    // final int childRight = childLeft + child.getMeasuredWidth();
    // child.layout(childLeft, childTop, childRight, childBottom);
    // }
    }

    /**
     * Obtain the view and add it to our list of children. The view can be made
     * fresh, converted from an unused view, or used as is if it was in the
     * recycle bin.
     *
     * @param startPosition Logical position in the list to start from
     * @param x Left or right edge of the view to add
     * @param forward If true, align left edge to x and increase position.
     *                If false, align right edge to x and decrease position.
     * @return Number of views added
     */
    private int makeAndAddColumn(int startPosition, int x, boolean forward) {
        int columnWidth = mLargeColumnWidth;
        int addViews = 0;
        for (int remaining = mLargeColumnUnitCount, i = 0; remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount; i += forward ? 1 : -1, addViews++) {
            if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) {
                // landscape
                remaining -= LAND_UNITS;
            } else {
                // portrait
                remaining -= PORT_UNITS;
                if (remaining < 0) {
                    remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount);
                    columnWidth = mSmallColumnWidth;
                }
            }
        }
        int nextTop = 0;
        for (int i = 0; i < addViews; i++) {
            int position = startPosition + (forward ? i : -i);
            View child = obtainView(position, null);
            if (child.getParent() != this) {
                if (mInLayout) {
                    addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams());
                } else {
                    addView(child, forward ? -1 : 0);
                }
            }
            int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f ? columnWidth / ASPECT_RATIO : columnWidth * ASPECT_RATIO));
            int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
            int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
            child.measure(widthSpec, heightSpec);
            int childLeft = forward ? x : x - columnWidth;
            child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize);
            nextTop += heightSize;
        }
        return addViews;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        mVelocityTracker.addMovement(ev);
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                mVelocityTracker.clear();
                mScroller.abortAnimation();
                mLastTouchX = ev.getX();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mTouchRemainderX = 0;
                if (mTouchMode == TOUCH_MODE_FLINGING) {
                    // Catch!
                    mTouchMode = TOUCH_MODE_DRAGGING;
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                {
                    final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    if (index < 0) {
                        Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + mActivePointerId + " - did StaggeredGridView receive an inconsistent " + "event stream?");
                        return false;
                    }
                    final float x = MotionEventCompat.getX(ev, index);
                    final float dx = x - mLastTouchX + mTouchRemainderX;
                    final int deltaY = (int) dx;
                    mTouchRemainderX = dx - deltaY;
                    if (Math.abs(dx) > mTouchSlop) {
                        mTouchMode = TOUCH_MODE_DRAGGING;
                        return true;
                    }
                }
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mVelocityTracker.addMovement(ev);
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                mVelocityTracker.clear();
                mScroller.abortAnimation();
                mLastTouchX = ev.getX();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mTouchRemainderX = 0;
                break;
            case MotionEvent.ACTION_MOVE:
                {
                    final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    if (index < 0) {
                        Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + mActivePointerId + " - did StaggeredGridView receive an inconsistent " + "event stream?");
                        return false;
                    }
                    final float x = MotionEventCompat.getX(ev, index);
                    final float dx = x - mLastTouchX + mTouchRemainderX;
                    final int deltaX = (int) dx;
                    mTouchRemainderX = dx - deltaX;
                    if (Math.abs(dx) > mTouchSlop) {
                        mTouchMode = TOUCH_MODE_DRAGGING;
                    }
                    if (mTouchMode == TOUCH_MODE_DRAGGING) {
                        mLastTouchX = x;
                        if (!trackMotionScroll(deltaX, true)) {
                            // Break fling velocity if we impacted an edge.
                            mVelocityTracker.clear();
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                mTouchMode = TOUCH_MODE_IDLE;
                break;
            case MotionEvent.ACTION_UP:
                {
                    mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId);
                    if (Math.abs(velocity) > mFlingVelocity) {
                        // TODO
                        mTouchMode = TOUCH_MODE_FLINGING;
                        mScroller.fling(0, 0, (int) velocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
                        mLastTouchX = 0;
                        ViewCompat.postInvalidateOnAnimation(this);
                    } else {
                        mTouchMode = TOUCH_MODE_IDLE;
                    }
                }
                break;
        }
        return true;
    }

    /**
     * @param deltaX Pixels that content should move by
     * @return true if the movement completed, false if it was stopped prematurely.
     */
    private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) {
        final boolean contentFits = contentFits();
        final int allowOverhang = Math.abs(deltaX);
        final int overScrolledBy;
        final int movedBy;
        if (!contentFits) {
            final int overhang;
            final boolean up;
            mPopulating = true;
            if (deltaX > 0) {
                overhang = fillLeft(mFirstPosition - 1, allowOverhang);
                up = true;
            } else {
                overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang);
                up = false;
            }
            movedBy = Math.min(overhang, allowOverhang);
            offsetChildren(up ? movedBy : -movedBy);
            recycleOffscreenViews();
            mPopulating = false;
            overScrolledBy = allowOverhang - overhang;
        } else {
            overScrolledBy = allowOverhang;
            movedBy = 0;
        }
        if (allowOverScroll) {
            final int overScrollMode = ViewCompat.getOverScrollMode(this);
            if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
                if (overScrolledBy > 0) {
                    EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge;
                    edge.onPull((float) Math.abs(deltaX) / getWidth());
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
        }
        return deltaX == 0 || movedBy != 0;
    }

    /**
     * Important: this method will leave offscreen views attached if they
     * are required to maintain the invariant that child view with index i
     * is always the view corresponding to position mFirstPosition + i.
     */
    private void recycleOffscreenViews() {
        final int height = getHeight();
        final int clearAbove = 0;
        final int clearBelow = height;
        for (int i = getChildCount() - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= clearBelow) {
                // There may be other offscreen views, but we need to maintain
                // the invariant doreplacedented above.
                break;
            }
            if (mInLayout) {
                removeViewsInLayout(i, 1);
            } else {
                removeViewAt(i);
            }
            mRecycler.addScrap(child);
        }
        while (getChildCount() > 0) {
            final View child = getChildAt(0);
            if (child.getBottom() >= clearAbove) {
                // There may be other offscreen views, but we need to maintain
                // the invariant doreplacedented above.
                break;
            }
            if (mInLayout) {
                removeViewsInLayout(0, 1);
            } else {
                removeViewAt(0);
            }
            mRecycler.addScrap(child);
            mFirstPosition++;
        }
    }

    final void offsetChildren(int offset) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            child.layout(child.getLeft() + offset, child.getTop(), child.getRight() + offset, child.getBottom());
        }
    }

    private boolean contentFits() {
        final int childCount = getChildCount();
        if (childCount == 0)
            return true;
        if (childCount != mItemCount)
            return false;
        return getChildAt(0).getLeft() >= getPaddingLeft() && getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight();
    }

    private void recycleAllViews() {
        for (int i = 0; i < getChildCount(); i++) {
            mRecycler.addScrap(getChildAt(i));
        }
        if (mInLayout) {
            removeAllViewsInLayout();
        } else {
            removeAllViews();
        }
    }

    private int fillRight(int pos, int overhang) {
        int end = (getRight() - getLeft()) + overhang;
        int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight();
        while (nextLeft < end && pos < mItemCount) {
            pos += makeAndAddColumn(pos, nextLeft, true);
            nextLeft = getChildAt(getChildCount() - 1).getRight();
        }
        final int gridRight = getWidth() - getPaddingRight();
        return getChildAt(getChildCount() - 1).getRight() - gridRight;
    }

    private int fillLeft(int pos, int overhang) {
        int end = getPaddingLeft() - overhang;
        int nextRight = getChildAt(0).getLeft();
        while (nextRight > end && pos >= 0) {
            pos -= makeAndAddColumn(pos, nextRight, false);
            nextRight = getChildAt(0).getLeft();
        }
        mFirstPosition = pos + 1;
        return getPaddingLeft() - getChildAt(0).getLeft();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            final int x = mScroller.getCurrX();
            final int dx = (int) (x - mLastTouchX);
            mLastTouchX = x;
            final boolean stopped = !trackMotionScroll(dx, false);
            if (!stopped && !mScroller.isFinished()) {
                ViewCompat.postInvalidateOnAnimation(this);
            } else {
                if (stopped) {
                    final int overScrollMode = ViewCompat.getOverScrollMode(this);
                    if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
                        final EdgeEffectCompat edge;
                        if (dx > 0) {
                            edge = mLeftEdge;
                        } else {
                            edge = mRightEdge;
                        }
                        edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                    mScroller.abortAnimation();
                }
                mTouchMode = TOUCH_MODE_IDLE;
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (!mLeftEdge.isFinished()) {
            final int restoreCount = canvas.save();
            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
            canvas.rotate(270);
            canvas.translate(-height + getPaddingTop(), 0);
            mLeftEdge.setSize(height, getWidth());
            if (mLeftEdge.draw(canvas)) {
                postInvalidateOnAnimation();
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mRightEdge.isFinished()) {
            final int restoreCount = canvas.save();
            final int width = getWidth();
            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
            canvas.rotate(90);
            canvas.translate(-getPaddingTop(), width);
            mRightEdge.setSize(height, width);
            if (mRightEdge.draw(canvas)) {
                postInvalidateOnAnimation();
            }
            canvas.restoreToCount(restoreCount);
        }
    }

    /**
     * Obtain a populated view from the adapter. If optScrap is non-null and is not
     * reused it will be placed in the recycle bin.
     *
     * @param position position to get view for
     * @param optScrap Optional scrap view; will be reused if possible
     * @return A new view, a recycled view from mRecycler, or optScrap
     */
    private final View obtainView(int position, View optScrap) {
        View view = mRecycler.getTransientStateView(position);
        if (view != null) {
            return view;
        }
        // Reuse optScrap if it's of the right type (and not null)
        final int optType = optScrap != null ? ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
        final int positionViewType = mAdapter.gereplacedemViewType(position);
        final View scrap = optType == positionViewType ? optScrap : mRecycler.getScrapView(positionViewType);
        view = mAdapter.getView(position, scrap, this);
        if (view != scrap && scrap != null) {
            // The adapter didn't use it; put it back.
            mRecycler.addScrap(scrap);
        }
        ViewGroup.LayoutParams lp = view.getLayoutParams();
        if (view.getParent() != this) {
            if (lp == null) {
                lp = generateDefaultLayoutParams();
            } else if (!checkLayoutParams(lp)) {
                lp = generateLayoutParams(lp);
            }
            view.setLayoutParams(lp);
        }
        final LayoutParams sglp = (LayoutParams) lp;
        sglp.position = position;
        sglp.viewType = positionViewType;
        return view;
    }

    public GalleryThumbnailAdapter getAdapter() {
        return mAdapter;
    }

    public void setAdapter(GalleryThumbnailAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mObserver);
        }
        // TODO: If the new adapter says that there are stable IDs, remove certain layout records
        // and onscreen views if they have changed instead of removing all of the state here.
        clearAllState();
        mAdapter = adapter;
        mDataChanged = true;
        mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
        if (adapter != null) {
            adapter.registerDataSetObserver(mObserver);
            mRecycler.setViewTypeCount(adapter.getViewTypeCount());
            mHreplacedtableIds = adapter.hreplacedtableIds();
        } else {
            mHreplacedtableIds = false;
        }
        populate();
    }

    /**
     * Clear all state because the grid will be used for a completely different set of data.
     */
    private void clearAllState() {
        // Clear all layout records and views
        removeAllViews();
        // Reset to the top of the grid
        mFirstPosition = 0;
        // Clear recycler because there could be different view types now
        mRecycler.clear();
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new LayoutParams(lp);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

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

    public static clreplaced LayoutParams extends ViewGroup.LayoutParams {

        private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_span };

        private static final int SPAN_INDEX = 0;

        /**
         * The number of columns this item should span
         */
        public int span = 1;

        /**
         * Item position this view represents
         */
        int position;

        /**
         * Type of this view as reported by the adapter
         */
        int viewType;

        /**
         * The column this view is occupying
         */
        int column;

        /**
         * The stable ID of the item this view displays
         */
        long id = -1;

        public LayoutParams(int height) {
            super(MATCH_PARENT, height);
            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            if (this.width != MATCH_PARENT) {
                Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + " - must be MATCH_PARENT");
                this.width = MATCH_PARENT;
            }
            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
            TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
            span = a.getInteger(SPAN_INDEX, 1);
            a.recycle();
        }

        public LayoutParams(ViewGroup.LayoutParams other) {
            super(other);
            if (this.width != MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with width " + this.width + " - must be MATCH_PARENT");
                this.width = MATCH_PARENT;
            }
            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }
    }

    private clreplaced RecycleBin {

        private ArrayList<View>[] mScrapViews;

        private int mViewTypeCount;

        private int mMaxScrap;

        private SparseArray<View> mTransientStateViews;

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Must have at least one view type (" + viewTypeCount + " types reported)");
            }
            if (viewTypeCount == mViewTypeCount) {
                return;
            }
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
            mScrapViews = scrapViews;
        }

        public void clear() {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                mScrapViews[i].clear();
            }
            if (mTransientStateViews != null) {
                mTransientStateViews.clear();
            }
        }

        public void clearTransientViews() {
            if (mTransientStateViews != null) {
                mTransientStateViews.clear();
            }
        }

        public void addScrap(View v) {
            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
            if (ViewCompat.hasTransientState(v)) {
                if (mTransientStateViews == null) {
                    mTransientStateViews = new SparseArray<View>();
                }
                mTransientStateViews.put(lp.position, v);
                return;
            }
            final int childCount = getChildCount();
            if (childCount > mMaxScrap) {
                mMaxScrap = childCount;
            }
            ArrayList<View> scrap = mScrapViews[lp.viewType];
            if (scrap.size() < mMaxScrap) {
                scrap.add(v);
            }
        }

        public View getTransientStateView(int position) {
            if (mTransientStateViews == null) {
                return null;
            }
            final View result = mTransientStateViews.get(position);
            if (result != null) {
                mTransientStateViews.remove(position);
            }
            return result;
        }

        public View getScrapView(int type) {
            ArrayList<View> scrap = mScrapViews[type];
            if (scrap.isEmpty()) {
                return null;
            }
            final int index = scrap.size() - 1;
            final View result = scrap.get(index);
            scrap.remove(index);
            return result;
        }
    }

    private clreplaced AdapterDataSetObserver extends DataSetObserver {

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
            // TODO: Consider matching these back up if we have stable IDs.
            mRecycler.clearTransientViews();
            if (!mHreplacedtableIds) {
                recycleAllViews();
            }
            // TODO: consider repopulating in a deferred runnable instead
            // (so that successive changes may still be batched)
            requestLayout();
        }

        @Override
        public void onInvalidated() {
        }
    }
}

19 Source : SwipeMenuLayout.java
with Apache License 2.0
from yanzhenjie

/**
 * Created by Yan Zhenjie on 2016/7/27.
 */
public clreplaced SwipeMenuLayout extends FrameLayout implements Controller {

    public static final int DEFAULT_SCROLLER_DURATION = 200;

    private int mLeftViewId = 0;

    private int mContentViewId = 0;

    private int mRightViewId = 0;

    private float mOpenPercent = 0.5f;

    private int mScrollerDuration = DEFAULT_SCROLLER_DURATION;

    private int mScaledTouchSlop;

    private int mLastX;

    private int mLastY;

    private int mDownX;

    private int mDownY;

    private View mContentView;

    private LeftHorizontal mSwipeLeftHorizontal;

    private RightHorizontal mSwipeRightHorizontal;

    private Horizontal mSwipeCurrentHorizontal;

    private boolean shouldResetSwipe;

    private boolean mDragging;

    private boolean swipeEnable = true;

    private OverScroller mScroller;

    private VelocityTracker mVelocityTracker;

    private int mScaledMinimumFlingVelocity;

    private int mScaledMaximumFlingVelocity;

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

    public SwipeMenuLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setClickable(true);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout);
        mLeftViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_leftViewId, mLeftViewId);
        mContentViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_contentViewId, mContentViewId);
        mRightViewId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_rightViewId, mRightViewId);
        typedArray.recycle();
        ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mScaledTouchSlop = configuration.getScaledTouchSlop();
        mScaledMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mScaledMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        mScroller = new OverScroller(getContext());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (mLeftViewId != 0 && mSwipeLeftHorizontal == null) {
            View view = findViewById(mLeftViewId);
            mSwipeLeftHorizontal = new LeftHorizontal(view);
        }
        if (mRightViewId != 0 && mSwipeRightHorizontal == null) {
            View view = findViewById(mRightViewId);
            mSwipeRightHorizontal = new RightHorizontal(view);
        }
        if (mContentViewId != 0 && mContentView == null) {
            mContentView = findViewById(mContentViewId);
        } else {
            TextView errorView = new TextView(getContext());
            errorView.setClickable(true);
            errorView.setGravity(Gravity.CENTER);
            errorView.setTextSize(16);
            errorView.setText("You may not have set the ContentView.");
            mContentView = errorView;
            addView(mContentView);
        }
    }

    /**
     * Set whether open swipe. Default is true.
     *
     * @param swipeEnable true open, otherwise false.
     */
    public void setSwipeEnable(boolean swipeEnable) {
        this.swipeEnable = swipeEnable;
    }

    /**
     * Open the swipe function of the Item?
     *
     * @return open is true, otherwise is false.
     */
    public boolean isSwipeEnable() {
        return swipeEnable;
    }

    /**
     * Set open percentage.
     *
     * @param openPercent such as 0.5F.
     */
    public void setOpenPercent(float openPercent) {
        this.mOpenPercent = openPercent;
    }

    /**
     * Get open percentage.
     *
     * @return such as 0.5F.
     */
    public float getOpenPercent() {
        return mOpenPercent;
    }

    /**
     * The duration of the set.
     *
     * @param scrollerDuration such as 500.
     */
    public void setScrollerDuration(int scrollerDuration) {
        this.mScrollerDuration = scrollerDuration;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercepted = super.onInterceptTouchEvent(ev);
        if (!isSwipeEnable()) {
            return isIntercepted;
        }
        int action = ev.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    mDownX = mLastX = (int) ev.getX();
                    mDownY = (int) ev.getY();
                    return false;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    int disX = (int) (ev.getX() - mDownX);
                    int disY = (int) (ev.getY() - mDownY);
                    return Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY);
                }
            case MotionEvent.ACTION_UP:
                {
                    boolean isClick = mSwipeCurrentHorizontal != null && mSwipeCurrentHorizontal.isClickOnContentView(getWidth(), ev.getX());
                    if (isMenuOpen() && isClick) {
                        smoothCloseMenu();
                        return true;
                    }
                    return false;
                }
            case MotionEvent.ACTION_CANCEL:
                {
                    if (!mScroller.isFinished())
                        mScroller.abortAnimation();
                    return false;
                }
        }
        return isIntercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!isSwipeEnable()) {
            return super.onTouchEvent(ev);
        }
        if (mVelocityTracker == null)
            mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(ev);
        int dx;
        int dy;
        int action = ev.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    mLastX = (int) ev.getX();
                    mLastY = (int) ev.getY();
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    int disX = (int) (mLastX - ev.getX());
                    int disY = (int) (mLastY - ev.getY());
                    if (!mDragging && Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY)) {
                        mDragging = true;
                    }
                    if (mDragging) {
                        if (mSwipeCurrentHorizontal == null || shouldResetSwipe) {
                            if (disX < 0) {
                                if (mSwipeLeftHorizontal != null) {
                                    mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
                                } else {
                                    mSwipeCurrentHorizontal = mSwipeRightHorizontal;
                                }
                            } else {
                                if (mSwipeRightHorizontal != null) {
                                    mSwipeCurrentHorizontal = mSwipeRightHorizontal;
                                } else {
                                    mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
                                }
                            }
                        }
                        scrollBy(disX, 0);
                        mLastX = (int) ev.getX();
                        mLastY = (int) ev.getY();
                        shouldResetSwipe = false;
                    }
                    break;
                }
            case MotionEvent.ACTION_UP:
                {
                    dx = (int) (mDownX - ev.getX());
                    dy = (int) (mDownY - ev.getY());
                    mDragging = false;
                    mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
                    int velocityX = (int) mVelocityTracker.getXVelocity();
                    int velocity = Math.abs(velocityX);
                    if (velocity > mScaledMinimumFlingVelocity) {
                        if (mSwipeCurrentHorizontal != null) {
                            int duration = getSwipeDuration(ev, velocity);
                            if (mSwipeCurrentHorizontal instanceof RightHorizontal) {
                                if (velocityX < 0) {
                                    smoothOpenMenu(duration);
                                } else {
                                    smoothCloseMenu(duration);
                                }
                            } else {
                                if (velocityX > 0) {
                                    smoothOpenMenu(duration);
                                } else {
                                    smoothCloseMenu(duration);
                                }
                            }
                            ViewCompat.postInvalidateOnAnimation(this);
                        }
                    } else {
                        judgeOpenClose(dx, dy);
                    }
                    mVelocityTracker.clear();
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                    if (Math.abs(mDownX - ev.getX()) > mScaledTouchSlop || Math.abs(mDownY - ev.getY()) > mScaledTouchSlop || isLeftMenuOpen() || isRightMenuOpen()) {
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        super.onTouchEvent(ev);
                        return true;
                    }
                    break;
                }
            case MotionEvent.ACTION_CANCEL:
                {
                    mDragging = false;
                    if (!mScroller.isFinished()) {
                        mScroller.abortAnimation();
                    } else {
                        dx = (int) (mDownX - ev.getX());
                        dy = (int) (mDownY - ev.getY());
                        judgeOpenClose(dx, dy);
                    }
                    break;
                }
        }
        return super.onTouchEvent(ev);
    }

    /**
     * compute finish duration.
     *
     * @param ev up event.
     * @param velocity velocity x.
     *
     * @return finish duration.
     */
    private int getSwipeDuration(MotionEvent ev, int velocity) {
        int sx = getScrollX();
        int dx = (int) (ev.getX() - sx);
        final int width = mSwipeCurrentHorizontal.getMenuWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
        int duration;
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageDelta = (float) Math.abs(dx) / width;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, mScrollerDuration);
        return duration;
    }

    float distanceInfluenceForSnapDuration(float f) {
        // center the values about 0.
        f -= 0.5f;
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    private void judgeOpenClose(int dx, int dy) {
        if (mSwipeCurrentHorizontal != null) {
            if (Math.abs(getScrollX()) >= (mSwipeCurrentHorizontal.getMenuView().getWidth() * mOpenPercent)) {
                // auto open
                if (Math.abs(dx) > mScaledTouchSlop || Math.abs(dy) > mScaledTouchSlop) {
                    // swipe up
                    if (isMenuOpenNotEqual()) {
                        smoothCloseMenu();
                    } else {
                        smoothOpenMenu();
                    }
                } else {
                    // normal up
                    if (isMenuOpen()) {
                        smoothCloseMenu();
                    } else {
                        smoothOpenMenu();
                    }
                }
            } else {
                // auto closeMenu
                smoothCloseMenu();
            }
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        if (mSwipeCurrentHorizontal == null) {
            super.scrollTo(x, y);
        } else {
            Horizontal.Checker checker = mSwipeCurrentHorizontal.checkXY(x, y);
            shouldResetSwipe = checker.shouldResetSwipe;
            if (checker.x != getScrollX()) {
                super.scrollTo(checker.x, checker.y);
            }
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset() && mSwipeCurrentHorizontal != null) {
            if (mSwipeCurrentHorizontal instanceof RightHorizontal) {
                scrollTo(Math.abs(mScroller.getCurrX()), 0);
                invalidate();
            } else {
                scrollTo(-Math.abs(mScroller.getCurrX()), 0);
                invalidate();
            }
        }
    }

    public boolean hasLeftMenu() {
        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.canSwipe();
    }

    public boolean hasRightMenu() {
        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.canSwipe();
    }

    @Override
    public boolean isMenuOpen() {
        return isLeftMenuOpen() || isRightMenuOpen();
    }

    @Override
    public boolean isLeftMenuOpen() {
        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpen(getScrollX());
    }

    @Override
    public boolean isRightMenuOpen() {
        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpen(getScrollX());
    }

    @Override
    public boolean isCompleteOpen() {
        return isLeftCompleteOpen() || isRightMenuOpen();
    }

    @Override
    public boolean isLeftCompleteOpen() {
        return mSwipeLeftHorizontal != null && !mSwipeLeftHorizontal.isCompleteClose(getScrollX());
    }

    @Override
    public boolean isRightCompleteOpen() {
        return mSwipeRightHorizontal != null && !mSwipeRightHorizontal.isCompleteClose(getScrollX());
    }

    @Override
    public boolean isMenuOpenNotEqual() {
        return isLeftMenuOpenNotEqual() || isRightMenuOpenNotEqual();
    }

    @Override
    public boolean isLeftMenuOpenNotEqual() {
        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpenNotEqual(getScrollX());
    }

    @Override
    public boolean isRightMenuOpenNotEqual() {
        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpenNotEqual(getScrollX());
    }

    @Override
    public void smoothOpenMenu() {
        smoothOpenMenu(mScrollerDuration);
    }

    @Override
    public void smoothOpenLeftMenu() {
        smoothOpenLeftMenu(mScrollerDuration);
    }

    @Override
    public void smoothOpenRightMenu() {
        smoothOpenRightMenu(mScrollerDuration);
    }

    @Override
    public void smoothOpenLeftMenu(int duration) {
        if (mSwipeLeftHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
            smoothOpenMenu(duration);
        }
    }

    @Override
    public void smoothOpenRightMenu(int duration) {
        if (mSwipeRightHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeRightHorizontal;
            smoothOpenMenu(duration);
        }
    }

    private void smoothOpenMenu(int duration) {
        if (mSwipeCurrentHorizontal != null) {
            mSwipeCurrentHorizontal.autoOpenMenu(mScroller, getScrollX(), duration);
            invalidate();
        }
    }

    @Override
    public void smoothCloseMenu() {
        smoothCloseMenu(mScrollerDuration);
    }

    @Override
    public void smoothCloseLeftMenu() {
        if (mSwipeLeftHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
            smoothCloseMenu();
        }
    }

    @Override
    public void smoothCloseRightMenu() {
        if (mSwipeRightHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeRightHorizontal;
            smoothCloseMenu();
        }
    }

    @Override
    public void smoothCloseMenu(int duration) {
        if (mSwipeCurrentHorizontal != null) {
            mSwipeCurrentHorizontal.autoCloseMenu(mScroller, getScrollX(), duration);
            invalidate();
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int contentViewHeight;
        if (mContentView != null) {
            int contentViewWidth = mContentView.getMeasuredWidthAndState();
            contentViewHeight = mContentView.getMeasuredHeightAndState();
            LayoutParams lp = (LayoutParams) mContentView.getLayoutParams();
            int start = getPaddingLeft();
            int top = getPaddingTop() + lp.topMargin;
            mContentView.layout(start, top, start + contentViewWidth, top + contentViewHeight);
        }
        if (mSwipeLeftHorizontal != null) {
            View leftMenu = mSwipeLeftHorizontal.getMenuView();
            int menuViewWidth = leftMenu.getMeasuredWidthAndState();
            int menuViewHeight = leftMenu.getMeasuredHeightAndState();
            LayoutParams lp = (LayoutParams) leftMenu.getLayoutParams();
            int top = getPaddingTop() + lp.topMargin;
            leftMenu.layout(-menuViewWidth, top, 0, top + menuViewHeight);
        }
        if (mSwipeRightHorizontal != null) {
            View rightMenu = mSwipeRightHorizontal.getMenuView();
            int menuViewWidth = rightMenu.getMeasuredWidthAndState();
            int menuViewHeight = rightMenu.getMeasuredHeightAndState();
            LayoutParams lp = (LayoutParams) rightMenu.getLayoutParams();
            int top = getPaddingTop() + lp.topMargin;
            int parentViewWidth = getMeasuredWidthAndState();
            rightMenu.layout(parentViewWidth, top, parentViewWidth + menuViewWidth, top + menuViewHeight);
        }
    }
}

19 Source : RightHorizontal.java
with Apache License 2.0
from yanzhenjie

@Override
public void autoOpenMenu(OverScroller scroller, int scrollX, int duration) {
    scroller.startScroll(Math.abs(scrollX), 0, getMenuView().getWidth() - Math.abs(scrollX), 0, duration);
}

19 Source : RightHorizontal.java
with Apache License 2.0
from yanzhenjie

@Override
public void autoCloseMenu(OverScroller scroller, int scrollX, int duration) {
    scroller.startScroll(-Math.abs(scrollX), 0, Math.abs(scrollX), 0, duration);
}

19 Source : GingerScroller.java
with Apache License 2.0
from yanzhenjie

@TargetApi(9)
public clreplaced GingerScroller extends ScrollerProxy {

    protected final OverScroller mScroller;

    public GingerScroller(Context context) {
        mScroller = new OverScroller(context);
    }

    @Override
    public boolean computeScrollOffset() {
        return mScroller.computeScrollOffset();
    }

    @Override
    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
    }

    @Override
    public void forceFinished(boolean finished) {
        mScroller.forceFinished(finished);
    }

    @Override
    public boolean isFinished() {
        return mScroller.isFinished();
    }

    @Override
    public int getCurrX() {
        return mScroller.getCurrX();
    }

    @Override
    public int getCurrY() {
        return mScroller.getCurrY();
    }
}

19 Source : AppBarLayoutBehavior.java
with Apache License 2.0
from yangchong211

/**
 * 停止appbarLayout的fling事件
 * @param appBarLayout
 */
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
    // 通过反射拿到HeaderBehavior中的flingRunnable变量
    try {
        Field flingRunnableField = getFlingRunnableField();
        Field scrollerField = getScrollerField();
        if (flingRunnableField != null) {
            flingRunnableField.setAccessible(true);
        }
        if (scrollerField != null) {
            scrollerField.setAccessible(true);
        }
        Runnable flingRunnable = null;
        if (flingRunnableField != null) {
            flingRunnable = (Runnable) flingRunnableField.get(this);
        }
        OverScroller overScroller = null;
        if (scrollerField != null) {
            overScroller = (OverScroller) scrollerField.get(this);
        }
        // 下面是关键点
        if (flingRunnable != null) {
            LogUtil.d(TAG, "存在flingRunnable");
            appBarLayout.removeCallbacks(flingRunnable);
            flingRunnableField.set(this, null);
        }
        if (overScroller != null && !overScroller.isFinished()) {
            overScroller.abortAnimation();
        }
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

19 Source : SmartDragLayout.java
with Apache License 2.0
from y1xian

/**
 * Description: 智能的拖拽布局,优先滚动整体,整体滚到头,则滚动内部能滚动的View
 */
public clreplaced SmartDragLayout extends FrameLayout implements NestedScrollingParent {

    private static final String TAG = "SmartDragLayout";

    private View child;

    OverScroller scroller;

    VelocityTracker tracker;

    ShadowBgAnimator bgAnimator = new ShadowBgAnimator();

    // 是否启用手势拖拽
    boolean enableDrag = true;

    boolean dismissOnTouchOutside = true;

    boolean hreplacedhadowBg = true;

    boolean isUserClose = false;

    // 是否开启三段拖拽
    boolean isThreeDrag = false;

    LayoutStatus status = LayoutStatus.Close;

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

    public SmartDragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SmartDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (enableDrag) {
            scroller = new OverScroller(context);
        }
    }

    int maxY;

    int minY;

    @Override
    public void onViewAdded(View c) {
        super.onViewAdded(c);
        child = c;
    }

    int lastHeight;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        maxY = child.getMeasuredHeight();
        minY = 0;
        int l = getMeasuredWidth() / 2 - child.getMeasuredWidth() / 2;
        if (enableDrag) {
            // horizontal center
            child.layout(l, getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight() + maxY);
            if (status == LayoutStatus.Open) {
                if (isThreeDrag) {
                    // 通过scroll上移
                    scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
                } else {
                    // 通过scroll上移
                    scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
                }
            }
        } else {
            // like bottom gravity
            child.layout(l, getMeasuredHeight() - child.getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight());
        }
        lastHeight = maxY;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        isUserClose = true;
        return super.dispatchTouchEvent(ev);
    }

    float touchX, touchY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (scroller.computeScrollOffset()) {
            touchX = 0;
            touchY = 0;
            return false;
        }
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (enableDrag) {
                    tracker = VelocityTracker.obtain();
                }
                touchX = event.getX();
                touchY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (enableDrag) {
                    tracker.addMovement(event);
                    tracker.computeCurrentVelocity(1000);
                    int dy = (int) (event.getY() - touchY);
                    scrollTo(getScrollX(), getScrollY() - dy);
                    touchY = event.getY();
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // click in child rect
                Rect rect = new Rect();
                child.getGlobalVisibleRect(rect);
                if (!PopupUtils.isInRect(event.getRawX(), event.getRawY(), rect) && dismissOnTouchOutside) {
                    float distance = (float) Math.sqrt(Math.pow(event.getX() - touchX, 2) + Math.pow(event.getY() - touchY, 2));
                    if (distance < ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
                        performClick();
                    }
                } else {
                }
                if (enableDrag) {
                    float yVelocity = tracker.getYVelocity();
                    if (yVelocity > 1500 && !isThreeDrag) {
                        close();
                    } else {
                        finishScroll();
                    }
                    tracker.clear();
                    tracker.recycle();
                }
                break;
            default:
                break;
        }
        return true;
    }

    private void finishScroll() {
        if (enableDrag) {
            int threshold = isScrollUp ? (maxY - minY) / 3 : (maxY - minY) * 2 / 3;
            int dy = (getScrollY() > threshold ? maxY : minY) - getScrollY();
            if (isThreeDrag) {
                int per = maxY / 3;
                if (getScrollY() > per * 2.5f) {
                    dy = maxY - getScrollY();
                } else if (getScrollY() <= per * 2.5f && getScrollY() > per * 1.5f) {
                    dy = per * 2 - getScrollY();
                } else if (getScrollY() > per) {
                    dy = per - getScrollY();
                } else {
                    dy = minY - getScrollY();
                }
            }
            scroller.startScroll(getScrollX(), getScrollY(), 0, dy, PopupManager.getAnimationDuration());
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    boolean isScrollUp;

    @Override
    public void scrollTo(int x, int y) {
        if (y > maxY) {
            y = maxY;
        }
        if (y < minY) {
            y = minY;
        }
        float fraction = (y - minY) * 1f / (maxY - minY);
        isScrollUp = y > getScrollY();
        if (hreplacedhadowBg) {
            setBackgroundColor(bgAnimator.calculateBgColor(fraction));
        }
        if (listener != null) {
            if (isUserClose && fraction == 0f && status != LayoutStatus.Close) {
                status = LayoutStatus.Close;
                listener.onClose();
            } else if (fraction == 1f && status != LayoutStatus.Open) {
                status = LayoutStatus.Open;
                listener.onOpen();
            }
        }
        super.scrollTo(x, y);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isScrollUp = false;
        isUserClose = false;
        setTranslationY(0);
    }

    public void open() {
        status = LayoutStatus.Opening;
        post(new Runnable() {

            @Override
            public void run() {
                int dy = maxY - getScrollY();
                smoothScroll(enableDrag && isThreeDrag ? dy / 3 : dy, true);
            }
        });
    }

    public void close() {
        isUserClose = true;
        status = LayoutStatus.Closing;
        post(new Runnable() {

            @Override
            public void run() {
                smoothScroll(minY - getScrollY(), false);
            }
        });
    }

    public void smoothScroll(final int dy, final boolean isOpen) {
        post(new Runnable() {

            @Override
            public void run() {
                scroller.startScroll(getScrollX(), getScrollY(), 0, dy, (int) (isOpen ? PopupManager.getAnimationDuration() : PopupManager.getAnimationDuration() * 0.8f));
                ViewCompat.postInvalidateOnAnimation(SmartDragLayout.this);
            }
        });
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && enableDrag;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        // 必须要取消,否则会导致滑动初次延迟
        scroller.abortAnimation();
    }

    @Override
    public void onStopNestedScroll(View target) {
        finishScroll();
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        scrollTo(getScrollX(), getScrollY() + dyUnconsumed);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0) {
            // scroll up
            int newY = getScrollY() + dy;
            if (newY < maxY) {
                // dy不一定能消费完
                consumed[1] = dy;
            }
            scrollTo(getScrollX(), newY);
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        boolean isDragging = getScrollY() > minY && getScrollY() < maxY;
        if (isDragging && velocityY < -1500 && !isThreeDrag) {
            close();
        }
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    public void isThreeDrag(boolean isThreeDrag) {
        this.isThreeDrag = isThreeDrag;
    }

    public void enableDrag(boolean enableDrag) {
        this.enableDrag = enableDrag;
    }

    public void dismissOnTouchOutside(boolean dismissOnTouchOutside) {
        this.dismissOnTouchOutside = dismissOnTouchOutside;
    }

    public void hreplacedhadowBg(boolean hreplacedhadowBg) {
        this.hreplacedhadowBg = hreplacedhadowBg;
    }

    private OnCloseListener listener;

    public void setOnCloseListener(OnCloseListener listener) {
        this.listener = listener;
    }

    public interface OnCloseListener {

        void onClose();

        void onOpen();
    }
}

19 Source : FixAppBarLayoutBehavior.java
with Apache License 2.0
from y1xian

/**
 * 停止appbarLayout的fling事件
 * @param appBarLayout
 */
private void stopAppbarLayoutFling(AppBarLayout appBarLayout) {
    // 通过反射拿到HeaderBehavior中的flingRunnable变量
    try {
        Field flingRunnableField = getFlingRunnableField();
        Runnable flingRunnable;
        if (flingRunnableField != null) {
            flingRunnableField.setAccessible(true);
            flingRunnable = (Runnable) flingRunnableField.get(this);
            if (flingRunnable != null) {
                Log.d(TAG, "存在flingRunnable");
                appBarLayout.removeCallbacks(flingRunnable);
                flingRunnableField.set(this, null);
            }
        }
        Field scrollerField = getScrollerField();
        if (scrollerField != null) {
            scrollerField.setAccessible(true);
            OverScroller overScroller = (OverScroller) scrollerField.get(this);
            if (overScroller != null && !overScroller.isFinished()) {
                overScroller.abortAnimation();
            }
        }
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

19 Source : GingerScroller.java
with Apache License 2.0
from xuexiangjys

@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public clreplaced GingerScroller extends ScrollerProxy {

    protected final OverScroller mScroller;

    public GingerScroller(Context context) {
        mScroller = new OverScroller(context);
    }

    @Override
    public boolean computeScrollOffset() {
        return mScroller.computeScrollOffset();
    }

    @Override
    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
    }

    @Override
    public void forceFinished(boolean finished) {
        mScroller.forceFinished(finished);
    }

    @Override
    public boolean isFinished() {
        return mScroller.isFinished();
    }

    @Override
    public int getCurrX() {
        return mScroller.getCurrX();
    }

    @Override
    public int getCurrY() {
        return mScroller.getCurrY();
    }
}

19 Source : AlipayScrollView.java
with Apache License 2.0
from xmuSistone

/**
 * Created by xmuSistone on 2018/12/25.
 */
public clreplaced AlipayScrollView extends ScrollView {

    private AlipayContainerLayout parentView;

    private int[] SLOW_DOWN_STEP = new int[7];

    private float lastProcessY;

    private int downTouchOffset = 0;

    private Spring marginSpring, scrollSpring;

    private View topLayout;

    private int progressColor;

    private ProgressImageView progressImageView;

    private int progressHeight, progressCenterOffset;

    private View firstChildView;

    private int firstViewPosition = 0;

    private boolean refreshing = false;

    private OnRefreshListener onRefreshListener;

    private ScrollChangeListener scrollChangeListener;

    private boolean flinging = false;

    private OverScroller overScroller;

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

    public AlipayScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AlipayScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 1. xml配置信息获取
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.alipay);
        int defaultProgressHeight = dp2px(55);
        this.progressHeight = typedArray.getDimensionPixelSize(R.styleable.alipay_progressHeight, defaultProgressHeight);
        this.progressCenterOffset = typedArray.getDimensionPixelSize(R.styleable.alipay_progressCenterOffset, 0);
        this.progressColor = typedArray.getColor(R.styleable.alipay_progressColor, Color.BLACK);
        typedArray.recycle();
        // 2. 初始化越界拖拽阻力参数
        int step = dp2px(20);
        int initSlowDownThreshold = progressHeight;
        int index = 0;
        for (int i = SLOW_DOWN_STEP.length - 1; i >= 0; i--) {
            SLOW_DOWN_STEP[i] = initSlowDownThreshold + step * index;
            index++;
        }
        // 3. 松手时的动画,用margin来做
        SpringConfig springConfig = SpringConfig.fromOrigamiTensionAndFriction(3, 2);
        SpringSystem mSpringSystem = SpringSystem.create();
        marginSpring = mSpringSystem.createSpring().setSpringConfig(springConfig);
        marginSpring.setOvershootClampingEnabled(true);
        marginSpring.addListener(new SimpleSpringListener() {

            @Override
            public void onSpringUpdate(Spring spring) {
                int yPos = (int) spring.getCurrentValue();
                setFirstViewPosition(yPos);
                onPositionChanged();
            }

            @Override
            public void onSpringAtRest(Spring spring) {
                super.onSpringAtRest(spring);
                if (firstViewPosition < progressHeight / 2) {
                    refreshing = false;
                    progressImageView.stopProgress();
                }
            }
        });
        // 4. snap停靠,属性动画用腻了,还是用spring吧
        scrollSpring = mSpringSystem.createSpring().setSpringConfig(springConfig);
        scrollSpring.setOvershootClampingEnabled(true);
        scrollSpring.addListener(new SimpleSpringListener() {

            @Override
            public void onSpringUpdate(Spring spring) {
                int scrollY = (int) spring.getCurrentValue();
                setScrollY(scrollY);
            }
        });
        // 5. 反射获取scroller,这个是用来在computeScroll中判定ScrollView是否fling停止了,格外注意proguard不能混淆ScrollView
        try {
            Field scrollerField = ScrollView.clreplaced.getDeclaredField("mScroller");
            scrollerField.setAccessible(true);
            this.overScroller = (OverScroller) scrollerField.get(this);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        // 判定是否fling停止,用来判定是否需要snap的逻辑入口
        if (flinging && overScroller.isFinished()) {
            flinging = false;
            if (null != scrollChangeListener) {
                scrollChangeListener.onFlingStop();
            }
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        parentView = (AlipayContainerLayout) getParent();
        topLayout = parentView.getTopLayout();
        progressImageView = parentView.getProgressImageView();
        progressImageView.setProgressColor(progressColor);
        firstChildView = ((ViewGroup) getChildAt(0)).getChildAt(0);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        adjustTopLayoutPos();
        if (null != scrollChangeListener) {
            scrollChangeListener.onScrollChange(t);
        }
    }

    /**
     * ScrollView上下滑动时,topLayout需要跟随滑动
     */
    private void adjustTopLayoutPos() {
        int topLayoutTop = -getScrollY();
        if (topLayoutTop < -topLayout.getHeight()) {
            topLayoutTop = -topLayout.getHeight();
        } else if (topLayoutTop > 0) {
            topLayoutTop = 0;
        }
        final int destLayoutTop = topLayoutTop;
        topLayout.offsetTopAndBottom(destLayoutTop - topLayout.getTop());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            flinging = false;
            lastProcessY = ev.getRawY();
            downTouchOffset = refreshing ? firstViewPosition - progressHeight : firstViewPosition;
            // 手指按下,动画结束
            scrollSpring.setAtRest();
        } else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            if (parentView.getTouchingView() != this) {
                ev.offsetLocation(0, downTouchOffset);
            }
            onDragRelease();
            super.onInterceptTouchEvent(ev);
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            if (parentView.getTouchingView() != this) {
                ev.offsetLocation(0, downTouchOffset);
            }
            super.onTouchEvent(ev);
        } else if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            flinging = true;
            if (parentView.getTouchingView() != this) {
                ev.offsetLocation(0, downTouchOffset);
            }
            onDragRelease();
            super.onTouchEvent(ev);
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            onTouchMove(ev);
        }
        return true;
    }

    /**
     * 专门抽出一个函数用来处理touch拖动
     */
    private void onTouchMove(MotionEvent ev) {
        int distance = (int) (ev.getRawY() - lastProcessY);
        if (getScrollY() == 0) {
            if (firstViewPosition == 0) {
                if (distance < 0) {
                    if (parentView.getTouchingView() != this) {
                        ev.offsetLocation(0, downTouchOffset);
                    }
                    super.onTouchEvent(ev);
                } else {
                    this.setFirstViewPosition(firstViewPosition + distance);
                    onPositionChanged();
                }
            } else {
                // 1. scroll在最顶部,继续向下拉时添加阻力。此段代码就是让移动的distance缩小,越偏离顶部,阻力越大
                int originDistance = distance;
                distance = shrinkDragDistance(distance);
                int newPosition = firstViewPosition + distance;
                if (progressImageView.isRunning()) {
                    // 正在动画
                    if (firstViewPosition == progressHeight) {
                        if (originDistance > 0) {
                            // 向下拉,直接改变firstView的位置
                            setFirstViewPosition(newPosition);
                        } else {
                            if (parentView.getTouchingView() != this) {
                                ev.offsetLocation(0, downTouchOffset);
                            }
                            super.onTouchEvent(ev);
                        }
                    } else {
                        if (newPosition < progressHeight) {
                            newPosition = progressHeight;
                        }
                        setFirstViewPosition(newPosition);
                    }
                } else {
                    // 动画停止
                    if (newPosition < 0) {
                        newPosition = 0;
                    }
                    setFirstViewPosition(newPosition);
                    onPositionChanged();
                }
            }
            lastProcessY = ev.getRawY();
        } else {
            lastProcessY = ev.getRawY();
            if (parentView.getTouchingView() != this) {
                ev.offsetLocation(0, downTouchOffset);
            }
            super.onTouchEvent(ev);
        }
    }

    /**
     * 拖动越界时,让距离缩水,以增加阻力;越界越多,阻力越大
     */
    private int shrinkDragDistance(int distance) {
        if (distance > 0) {
            int tempPosition = firstViewPosition + distance;
            if (tempPosition > SLOW_DOWN_STEP[0]) {
                distance = distance / 128;
            } else if (tempPosition > SLOW_DOWN_STEP[1]) {
                distance = distance / 64;
            } else if (tempPosition > SLOW_DOWN_STEP[2]) {
                distance = distance / 32;
            } else if (tempPosition > SLOW_DOWN_STEP[3]) {
                distance = distance / 16;
            } else if (tempPosition > SLOW_DOWN_STEP[4]) {
                distance = distance / 8;
            } else if (tempPosition > SLOW_DOWN_STEP[5]) {
                distance = distance / 4;
            } else if (tempPosition > SLOW_DOWN_STEP[6]) {
                distance = distance / 2;
            }
        }
        return distance;
    }

    private void onPositionChanged() {
        // 1. 进度
        float progress = (float) firstViewPosition / progressHeight;
        if (progress < 0) {
            progress = 0;
        } else if (progress > 1) {
            progress = 1;
        }
        if (!progressImageView.isRunning()) {
            progressImageView.setStartEndTrim(progress * progress, progress * progress * 2);
        }
        // 2. 缩放
        progressImageView.setPivotY(progressImageView.getHeight());
        progressImageView.setScaleX(progress);
        progressImageView.setScaleY(progress);
        // 3. 位移
        int originProgressPos = (progressHeight - progressImageView.getHeight()) / 2 + progressCenterOffset;
        int progressDestPosition = (int) (originProgressPos + (1 - progress) * progressImageView.getHeight() / 5);
        progressImageView.offsetTopAndBottom(progressDestPosition - progressImageView.getTop());
        // 4. 透明度
        float alpha = progress * 2;
        if (alpha > 1) {
            alpha = 1.0f;
        }
        progressImageView.setAlpha(alpha);
    }

    private void onDragRelease() {
        int top = firstViewPosition;
        if (top >= progressHeight) {
            progressImageView.startProgress();
            marginSpring.setAtRest();
            marginSpring.setCurrentValue(top);
            marginSpring.setEndValue(progressHeight);
            if (!refreshing) {
                refreshing = true;
                if (null != onRefreshListener) {
                    onRefreshListener.onRefresh();
                }
            }
        } else {
            refreshing = false;
            marginSpring.setAtRest();
            marginSpring.setCurrentValue(top);
            marginSpring.setEndValue(0);
        }
    }

    /**
     * 用margin的形式完成下拉位移
     */
    public void setFirstViewPosition(int firstViewPosition) {
        this.firstViewPosition = firstViewPosition;
        ViewGroup.LayoutParams lp = firstChildView.getLayoutParams();
        if (lp instanceof LinearLayout.LayoutParams) {
            LinearLayout.LayoutParams castLp = (LinearLayout.LayoutParams) lp;
            castLp.topMargin = firstViewPosition;
            firstChildView.setLayoutParams(castLp);
        } else if (lp instanceof RelativeLayout.LayoutParams) {
            RelativeLayout.LayoutParams castLp = (RelativeLayout.LayoutParams) lp;
            if (castLp.topMargin != firstViewPosition) {
                castLp.topMargin = firstViewPosition;
                firstChildView.setLayoutParams(castLp);
            }
        } else if (lp instanceof LayoutParams) {
            LayoutParams castLp = (LayoutParams) lp;
            if (castLp.topMargin != firstViewPosition) {
                castLp.topMargin = firstViewPosition;
                firstChildView.setLayoutParams(castLp);
            }
        }
    }

    public boolean isRefreshing() {
        return refreshing;
    }

    /**
     * 手动更新刷新状态
     */
    public void setRefreshing(boolean refreshing) {
        this.refreshing = refreshing;
        this.marginSpring.setCurrentValue(firstViewPosition);
        if (refreshing) {
            progressImageView.startProgress();
            marginSpring.setEndValue(progressHeight);
        } else {
            marginSpring.setEndValue(0);
        }
    }

    /**
     * dp和像素转换
     */
    public int dp2px(float dipValue) {
        float density = getContext().getResources().getDisplayMetrics().density;
        return (int) (dipValue * density + 0.5f);
    }

    public int getProgressHeight() {
        return progressHeight;
    }

    public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
        this.onRefreshListener = onRefreshListener;
    }

    public void setScrollChangeListener(ScrollChangeListener scrollChangeListener) {
        this.scrollChangeListener = scrollChangeListener;
    }

    public void updateProcessY(float rawY) {
        this.lastProcessY = rawY;
    }

    public void snapTo(int scrollY) {
        if (scrollY != getScrollY()) {
            scrollSpring.setCurrentValue(getScrollY());
            scrollSpring.setEndValue(scrollY);
        }
    }

    public interface OnRefreshListener {

        void onRefresh();
    }

    public interface ScrollChangeListener {

        /**
         * 滑动
         */
        void onScrollChange(int scrollY);

        /**
         * 滑动结束监听
         */
        void onFlingStop();
    }
}

19 Source : StickyNavLayout.java
with Apache License 2.0
from xiaohaibin

/**
 * 顾修忠[email protected]/[email protected]
 * Created by guxiuzhong on 2015/12/29.
 * 上滑悬停控件,底部内容区域支持 ScrollView ,ListView,RecyclerView,GridViewWithHeaderAndFooter
 */
public clreplaced StickyNavLayout extends LinearLayout {

    private static final String TAG = "StickyNavLayout";

    private View mTop;

    private View mNav;

    private ViewPager mViewPager;

    private int mTopViewHeight;

    private ViewGroup mInnerScrollView;

    private boolean isTopHidden = false;

    private OverScroller mScroller;

    private VelocityTracker mVelocityTracker;

    private int mTouchSlop;

    private int mMaximumVelocity, mMinimumVelocity;

    private float mLastY;

    private boolean mDragging;

    private boolean isStickNav;

    private boolean isInControl = false;

    private int stickOffset;

    private int mViewPagerMaxHeight;

    private int mTopViewMaxHeight;

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

    public StickyNavLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StickyNavLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setOrientation(LinearLayout.VERTICAL);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyNavLayout);
        isStickNav = a.getBoolean(R.styleable.StickyNavLayout_isStickNav, false);
        stickOffset = a.getDimensionPixelSize(R.styleable.StickyNavLayout_stickOffset, 0);
        a.recycle();
        mScroller = new OverScroller(context);
        mVelocityTracker = VelocityTracker.obtain();
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
    }

    public void setIsStickNav(boolean isStickNav) {
        this.isStickNav = isStickNav;
    }

    /**
     * 设置悬浮,并自动滚动到悬浮位置(即把top区域滚动上去)
     */
    public void setStickNavAndScrollToNav() {
        this.isStickNav = true;
        scrollTo(0, mTopViewHeight);
    }

    /**
     * *
     *  设置顶部区域的高度
     *
     *  @param height height
     */
    public void setTopViewHeight(int height) {
        mTopViewHeight = height;
        mTopViewHeight -= stickOffset;
        if (isStickNav) {
            scrollTo(0, stickOffset);
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mTop = findViewById(R.id.id_stickynavlayout_topview);
        mNav = findViewById(R.id.id_stickynavlayout_indicator);
        View view = findViewById(R.id.id_stickynavlayout_viewpager);
        if (!(view instanceof ViewPager)) {
            throw new RuntimeException("id_stickynavlayout_viewpager show used by ViewPager !");
        } else if (mTop instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) mTop;
            if (viewGroup.getChildCount() >= 2) {
                throw new RuntimeException("if the TopView(android:id=\"R.id.id_stickynavlayout_topview\") is a ViewGroup(ScrollView,LinearLayout,FrameLayout, ....) ,the children count should be one  !");
            }
        }
        mViewPager = (ViewPager) view;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "onMeasure---->>>>>>>>");
        ViewGroup.LayoutParams params = mViewPager.getLayoutParams();
        // 修复键盘弹出后键盘关闭布局高度不对问题
        int height = getMeasuredHeight() - mNav.getMeasuredHeight();
        mViewPagerMaxHeight = (height >= mViewPagerMaxHeight ? height : mViewPagerMaxHeight);
        params.height = /*mViewPagerMaxHeight - stickOffset*/
        height - stickOffset;
        mViewPager.setLayoutParams(params);
        // 修复键盘弹出后Top高度不对问题
        int topHeight = mTop instanceof ViewGroup ? ((ViewGroup) mTop).getChildAt(0).getMeasuredHeight() : mTop.getMeasuredHeight();
        ViewGroup.LayoutParams topParams = mTop.getLayoutParams();
        Log.d(TAG, "topHeight---->>>>>>>>" + topHeight);
        mTopViewMaxHeight = (topHeight >= mTopViewMaxHeight ? topHeight : mTopViewMaxHeight);
        topParams.height = /*mTopViewMaxHeight*/
        topHeight;
        mTop.setLayoutParams(topParams);
        // 设置mTopViewHeight
        mTopViewHeight = topParams.height - stickOffset;
        Log.d(TAG, "onMeasure--mTopViewHeight:" + mTopViewHeight);
        isTopHidden = getScrollY() == mTopViewHeight;
    }

    /**
     * 更新top区域的视图,如果是处于悬浮状态,隐藏top区域的控件是不起作用的!!
     */
    public void updateTopViews() {
        if (isTopHidden) {
            return;
        }
        final ViewGroup.LayoutParams params = mTop.getLayoutParams();
        mTop.post(new Runnable() {

            @Override
            public void run() {
                if (mTop instanceof ViewGroup) {
                    ViewGroup viewGroup = (ViewGroup) mTop;
                    int height = viewGroup.getChildAt(0).getHeight();
                    mTopViewHeight = height - stickOffset;
                    params.height = height;
                    mTop.setLayoutParams(params);
                    params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
                } else {
                    mTopViewHeight = mTop.getMeasuredHeight() - stickOffset;
                }
            }
        });
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        final ViewGroup.LayoutParams params = mTop.getLayoutParams();
        Log.d(TAG, "onSizeChanged-mTopViewHeight:" + mTopViewHeight);
        mTop.post(new Runnable() {

            @Override
            public void run() {
                if (mTop instanceof ViewGroup) {
                    ViewGroup viewGroup = (ViewGroup) mTop;
                    int height = viewGroup.getChildAt(0).getHeight();
                    mTopViewHeight = height - stickOffset;
                    params.height = height;
                    mTop.setLayoutParams(params);
                    mTop.requestLayout();
                } else {
                    mTopViewHeight = mTop.getMeasuredHeight() - stickOffset;
                }
                Log.d(TAG, "mTopViewHeight:" + mTopViewHeight);
                if (null != mInnerScrollView) {
                    Log.d(TAG, "mInnerScrollViewHeight:" + mInnerScrollView.getMeasuredHeight());
                }
                if (isStickNav) {
                    scrollTo(0, mTopViewHeight);
                }
            }
        });
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        float y = ev.getY();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = y - mLastY;
                getCurrentScrollView();
                if (mInnerScrollView instanceof ScrollView) {
                    if (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0 && !isInControl) {
                        isInControl = true;
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        dispatchTouchEvent(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        isSticky = true;
                        return dispatchTouchEvent(ev2);
                    }
                } else if (mInnerScrollView instanceof ListView) {
                    ListView lv = (ListView) mInnerScrollView;
                    View c = lv.getChildAt(lv.getFirstVisiblePosition());
                    if (!isInControl && c != null && c.getTop() == 0 && isTopHidden && dy > 0) {
                        isInControl = true;
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        dispatchTouchEvent(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        isSticky = true;
                        return dispatchTouchEvent(ev2);
                    }
                } else if (mInnerScrollView instanceof RecyclerView) {
                    RecyclerView rv = (RecyclerView) mInnerScrollView;
                    if (rv.getLayoutManager() == null) {
                        throw new IllegalStateException("RecyclerView does not have LayoutManager instance.");
                    }
                    View c = rv.getChildAt(0);
                    if (!isInControl && c != null && c.getTop() == 0 && /*android.support.v4.view.ViewCompat.canScrollVertically(rv, -1)*/
                    isTopHidden && dy > 0) {
                        isInControl = true;
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        MotionEvent ev2 = MotionEvent.obtain(ev);
                        dispatchTouchEvent(ev);
                        ev2.setAction(MotionEvent.ACTION_DOWN);
                        isSticky = true;
                        return dispatchTouchEvent(ev2);
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case // 处理悬停后立刻抬起的处理
            MotionEvent.ACTION_UP:
                float distance = y - mLastY;
                if (isSticky && /*distance==0.0f*/
                Math.abs(distance) <= mTouchSlop) {
                    isSticky = false;
                    return true;
                } else {
                    isSticky = false;
                    return super.dispatchTouchEvent(ev);
                }
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    // mNav-view 是否悬停的标志
    private boolean isSticky;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        float y = ev.getY();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = y - mLastY;
                getCurrentScrollView();
                if (Math.abs(dy) > mTouchSlop) {
                    mDragging = true;
                    if (mInnerScrollView instanceof ScrollView) {
                        // 如果topView没有隐藏
                        // 或sc的scrollY = 0 && topView隐藏 && 下拉,则拦截
                        if (!isTopHidden || (mInnerScrollView.getScrollY() == 0 && isTopHidden && dy > 0)) {
                            initVelocityTrackerIfNotExists();
                            mVelocityTracker.addMovement(ev);
                            mLastY = y;
                            return true;
                        }
                    } else if (mInnerScrollView instanceof ListView) {
                        ListView lv = (ListView) mInnerScrollView;
                        View c = lv.getChildAt(lv.getFirstVisiblePosition());
                        if (!isTopHidden || (c != null && c.getTop() == 0 && isTopHidden && dy > 0)) {
                            initVelocityTrackerIfNotExists();
                            mVelocityTracker.addMovement(ev);
                            mLastY = y;
                            return true;
                        } else {
                            if (lv.getAdapter() != null && lv.getAdapter().getCount() == 0) {
                                // 当ListView或ScrollView 没有数据为空时
                                initVelocityTrackerIfNotExists();
                                mVelocityTracker.addMovement(ev);
                                mLastY = y;
                                return true;
                            }
                        }
                    } else if (mInnerScrollView instanceof RecyclerView) {
                        RecyclerView rv = (RecyclerView) mInnerScrollView;
                        if (rv.getLayoutManager() == null) {
                            throw new IllegalStateException("RecyclerView does not have LayoutManager instance.");
                        }
                        View c = rv.getChildAt(0);
                        if (!isTopHidden || (c != null && c.getTop() == 0 && /*!android.support.v4.view.ViewCompat.canScrollVertically(rv, -1)*/
                        isTopHidden && dy > 0)) {
                            initVelocityTrackerIfNotExists();
                            mVelocityTracker.addMovement(ev);
                            mLastY = y;
                            return true;
                        } else {
                            if (rv.getAdapter() != null && rv.getAdapter().gereplacedemCount() == 0) {
                                initVelocityTrackerIfNotExists();
                                mVelocityTracker.addMovement(ev);
                                mLastY = y;
                                return true;
                            }
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mDragging = false;
                recycleVelocityTracker();
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    private void getCurrentScrollView() {
        int currenreplacedem = mViewPager.getCurrenreplacedem();
        PagerAdapter a = mViewPager.getAdapter();
        if (a instanceof FragmentPagerAdapter) {
            FragmentPagerAdapter fadapter = (FragmentPagerAdapter) a;
            Fragment item = fadapter.gereplacedem(currenreplacedem);
            View v = item.getView();
            if (v != null) {
                mInnerScrollView = (ViewGroup) (v.findViewById(R.id.id_stickynavlayout_innerscrollview));
            }
        } else if (a instanceof FragmentStatePagerAdapter) {
            FragmentStatePagerAdapter fsAdapter = (FragmentStatePagerAdapter) a;
            Fragment item = fsAdapter.gereplacedem(currenreplacedem);
            View v = item.getView();
            if (v != null) {
                mInnerScrollView = (ViewGroup) (v.findViewById(R.id.id_stickynavlayout_innerscrollview));
            }
        } else {
            throw new RuntimeException("mViewPager  should be  used  FragmentPagerAdapter or  FragmentStatePagerAdapter  !");
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(event);
        int action = event.getAction();
        float y = event.getY();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                mLastY = y;
                return true;
            case MotionEvent.ACTION_MOVE:
                float dy = y - mLastY;
                if (!mDragging && Math.abs(dy) > mTouchSlop) {
                    mDragging = true;
                }
                if (mDragging) {
                    scrollBy(0, (int) -dy);
                    // 如果topView隐藏,且上滑动时,则改变当前事件为ACTION_DOWN
                    if (getScrollY() == mTopViewHeight && dy < 0) {
                        event.setAction(MotionEvent.ACTION_DOWN);
                        dispatchTouchEvent(event);
                        isInControl = false;
                        isSticky = true;
                    } else {
                        isSticky = false;
                    }
                }
                mLastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                mDragging = false;
                recycleVelocityTracker();
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_UP:
                mDragging = false;
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocityY = (int) mVelocityTracker.getYVelocity();
                if (Math.abs(velocityY) > mMinimumVelocity) {
                    fling(-velocityY);
                }
                recycleVelocityTracker();
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    public void fling(int velocityY) {
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
        invalidate();
    }

    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > mTopViewHeight) {
            y = mTopViewHeight;
        }
        if (y != getScrollY()) {
            super.scrollTo(x, y);
        }
        isTopHidden = getScrollY() == mTopViewHeight;
        // set  listener 设置悬浮监听回调
        if (listener != null) {
            // if(lastIsTopHidden!=isTopHidden){
            // lastIsTopHidden=isTopHidden;
            listener.isStick(isTopHidden);
            // }
            listener.scrollPercent((float) getScrollY() / (float) mTopViewHeight);
        }
    }

    // private  boolean lastIsTopHidden;//记录上次是否悬浮
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            invalidate();
        }
    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    public int getStickOffset() {
        return stickOffset;
    }

    public void setStickOffset(int stickOffset) {
        this.stickOffset = stickOffset;
    }

    private onStickStateChangeListener listener;

    /**
     * 悬浮状态回调
     */
    public interface onStickStateChangeListener {

        /**
         * 是否悬浮的回调
         *
         * @param isStick true 悬浮 ,false 没有悬浮
         */
        void isStick(boolean isStick);

        /**
         * 距离悬浮的距离的百分比
         *
         * @param percent 0~1(向上) or 1~0(向下) 的浮点数
         */
        void scrollPercent(float percent);
    }

    public void setOnStickStateChangeListener(onStickStateChangeListener listener) {
        this.listener = listener;
    }
}

19 Source : PullToRefreshView.java
with Apache License 2.0
from wuchao226

/**
 * Created by Anthony on 2016/7/18.
 * 实现对子view 的上拉和下拉的监听实现,提供下拉和下拉的视图和接口
 */
public clreplaced PullToRefreshView extends ViewGroup {

    private LayoutInflater mInflater;

    private OverScroller mScroller;

    private OnRefreshListener mListener;

    /**
     * 头部View
     */
    private View header;

    private BaseIndicator mHeaderIndicator;

    private String mHeaderIndicatorClreplacedName;

    /**
     * 尾部View
     */
    private View footer;

    private BaseIndicator mFooterIndicator;

    private String mFooterIndicatorClreplacedName;

    /**
     * 内容View
     */
    private View contentView;

    private int mHeaderActionPosition;

    private int mFooterActionPosition;

    private int mHeaderHoldingPosition;

    private int mFooterHoldingPosition;

    private boolean isPullDownEnable = true;

    private boolean isPullUpEnable = true;

    private float mLastX;

    private float mLastY;

    private float deltaX = 0;

    private float deltaY = 0;

    private int IDLE = 0;

    private int PULL_DOWN = 1;

    private int PULL_UP = 2;

    // 自动刷新时的状态
    private int AUTO_SCROLL_PULL_DOWN = 3;

    private int mStatus = IDLE;

    private boolean isLoading = false;

    private long mStartLoadingTime;

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

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

    public PullToRefreshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mInflater = LayoutInflater.from(context);
        mScroller = new OverScroller(context);
        // 获取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
        if (ta.hasValue(R.styleable.PullToRefresh_header_indicator)) {
            mHeaderIndicatorClreplacedName = ta.getString(R.styleable.PullToRefresh_header_indicator);
        }
        if (ta.hasValue(R.styleable.PullToRefresh_footer_indicator)) {
            mFooterIndicatorClreplacedName = ta.getString(R.styleable.PullToRefresh_footer_indicator);
        }
        ta.recycle();
    }

    @Override
    protected void onFinishInflate() {
        if (getChildCount() != 1) {
            throw new RuntimeException("The child of VIPullToRefresh should be only one!!!");
        }
        contentView = getChildAt(0);
        setPadding(0, 0, 0, 0);
        contentView.setPadding(0, 0, 0, 0);
        mHeaderIndicator = getIndicator(mHeaderIndicatorClreplacedName);
        if (mHeaderIndicator == null) {
            mHeaderIndicator = new DefaultHeader();
        }
        header = mHeaderIndicator.createView(mInflater, this);
        mFooterIndicator = getIndicator(mFooterIndicatorClreplacedName);
        if (mFooterIndicator == null) {
            mFooterIndicator = new DefaultFooter();
        }
        footer = mFooterIndicator.createView(mInflater, this);
        contentView.bringToFront();
        super.onFinishInflate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
        mHeaderActionPosition = header.getMeasuredHeight() / 3 + header.getMeasuredHeight();
        mFooterActionPosition = footer.getMeasuredHeight() / 3 + footer.getMeasuredHeight();
        mHeaderHoldingPosition = header.getMeasuredHeight();
        mFooterHoldingPosition = footer.getMeasuredHeight();
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (contentView != null) {
            if (header != null) {
                header.layout(0, -header.getMeasuredHeight(), getWidth(), 0);
            }
            if (footer != null) {
                footer.layout(0, getHeight(), getWidth(), getHeight() + footer.getMeasuredHeight());
            }
            // if (header != null) {
            // header.layout(0, 0, getWidth(), header.getMeasuredHeight());
            // }
            // if (footer != null) {
            // footer.layout(0, getHeight() - footer.getMeasuredHeight(), getWidth(), getHeight());
            // }
            contentView.layout(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
        }
    }

    private boolean isInControl = false;

    private boolean isNeedIntercept;

    private boolean isNeedIntercept() {
        if (deltaY > 0 && isContentScrollToTop() || getScrollY() < 0 - 10) {
            mStatus = PULL_DOWN;
            return true;
        }
        if (deltaY < 0 && isContentScrollToBottom() || getScrollY() > 0 + 10) {
            mStatus = PULL_UP;
            return true;
        }
        return false;
    }

    /**
     * dispatchTouchEvent主要用于记录触摸事件的初始状态
     * 因为这里是所有触摸事件的入口函数所有事件都会经过这里
     * <p>
     * 因此如果你需要观测整个事件序列从开始到最后,无论它是否被本ViewGroup拦截,那么请在这个函数中进行
     * <p>
     * 为什么不在onInterceptTouchEvent中观测??
     * 因为在onInterceptTouchEvent中进行记录可能漏掉一些事件
     * 例如:当布局内部控件在onTouchEvent函数中对DOWN返回true,那么后续的MOVE事件和UP事件就可能不会传入onInterceptTouchEvent函数中
     * 再比如:当本ViewGroup自身对第一个MOVE事件进行拦截即onInterceptTouchEvent返回true时后续的MOVE和UP事件都不会传入onInterceptTouchEvent函数中
     * 而是直接进入onTouchEvent中去
     */
    private VelocityTracker mVelocityTracker;

    private float yVelocity;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        dealMulreplacedouch(ev);
        final int action = MotionEventCompat.getActionMasked(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
                } else {
                    mVelocityTracker.clear();
                }
                mVelocityTracker.addMovement(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(ev);
                mVelocityTracker.computeCurrentVelocity(500);
                yVelocity = mVelocityTracker.getYVelocity();
                isNeedIntercept = isNeedIntercept();
                if (isNeedIntercept && !isInControl) {
                    // 把内部控件的事件转发给本控件处理
                    isInControl = true;
                    ev.setAction(MotionEvent.ACTION_CANCEL);
                    MotionEvent ev2 = MotionEvent.obtain(ev);
                    dispatchTouchEvent(ev);
                    ev2.setAction(MotionEvent.ACTION_DOWN);
                    return dispatchTouchEvent(ev2);
                }
                break;
            case MotionEvent.ACTION_UP:
                /**
                 * 为什么将本ViewGroup自动返回初始位置的触发函数放在dispatchTouchEvent中?
                 * 由于下面onTouchEvent代码中ACTION_MOVE时有一段对本ViewGroup当前事件控制权转移给内部控件的代码
                 * 因此这会使得最后的Up event不会到本ViewGroup中的onTouchEvent中去
                 * 所以只能将autoBackToOriginalPosition()前移到dispatchTouchEvent()中来
                 */
                autoBackToPosition();
                isNeedIntercept = false;
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return isNeedIntercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                return true;
            case MotionEvent.ACTION_MOVE:
                if (!isPullDownEnable && deltaY > 0 && getScrollY() <= 0) {
                    break;
                }
                if (!isPullUpEnable && deltaY < 0 && getScrollY() >= 0) {
                    break;
                }
                if (isNeedIntercept) {
                    if (mStatus == PULL_DOWN && getScrollY() > 0) {
                        break;
                    }
                    if (mStatus == PULL_UP && getScrollY() < 0) {
                        break;
                    }
                    scrollBy(0, (int) (-getMoveFloat(yVelocity, deltaY)));
                    updateIndicator();
                } else {
                    ev.setAction(MotionEvent.ACTION_DOWN);
                    dispatchTouchEvent(ev);
                    isInControl = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                autoBackToPosition();
                return true;
        }
        return true;
    }

    /**
     * 处理多点触控的情况
     * 记录手指按下和移动时的各种相关数值
     * mActivePointerId为有效手指的ID,后续所有移动数值均来自这个手指
     * 当前active的手指只有一个且为后按下的那个
     */
    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;

    public void dealMulreplacedouch(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    mLastX = x;
                    mLastY = y;
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, pointerIndex);
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    // 下拉时deltaY为正,上拉时deltaY为负
                    deltaX = x - mLastX;
                    deltaY = y - mLastY;
                    mLastY = y;
                    mLastX = x;
                    break;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                {
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    if (pointerId != mActivePointerId) {
                        mLastX = MotionEventCompat.getX(ev, pointerIndex);
                        mLastY = MotionEventCompat.getY(ev, pointerIndex);
                        mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    }
                    break;
                }
            case MotionEvent.ACTION_POINTER_UP:
                {
                    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
                    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    if (pointerId == mActivePointerId) {
                        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                        mLastX = MotionEventCompat.getX(ev, newPointerIndex);
                        mLastY = MotionEventCompat.getY(ev, newPointerIndex);
                        mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
                    }
                    break;
                }
        }
    }

    @Override
    public void computeScroll() {
        // 先判断mScroller滚动是否完成
        if (mScroller.computeScrollOffset()) {
            // 这里调用View的scrollTo()完成实际的滚动
            scrollTo(0, mScroller.getCurrY());
            // 立即重绘View实现滚动效果
            invalidate();
        }
    }

    /**
     * 速度控制函数
     * 当手指移动速度越接近10000px每500毫秒时获得的控件移动距离越小
     * 1.5f为权重,越大速度控制越明显,表现为用户越不容易拉动本控件,即拉动越吃力
     * <p>
     * 若不进行速度控制,将可能导致一系列问题,其中包括
     * 用户下拉一段距离,突然很快加速上拉控件,这时footer将被拖出,但此时内部控件并未到达它的底部
     * 这样显示不符合上拉加载的逻辑
     */
    private float getMoveFloat(float velocity, float org) {
        return ((10000f - Math.abs(velocity)) / 10000f * org) / 1.5f;
    }

    /**
     * 判断该回到初始状态还是Loading状态
     */
    private void autoBackToPosition() {
        if (mStatus == PULL_DOWN && Math.abs(getScrollY()) < mHeaderActionPosition) {
            autoBackToOriginalPosition();
        } else if (mStatus == PULL_DOWN && Math.abs(getScrollY()) > mHeaderActionPosition) {
            autoBackToLoadingPosition();
        } else if (mStatus == PULL_UP && Math.abs(getScrollY()) < mFooterActionPosition) {
            autoBackToOriginalPosition();
        } else if (mStatus == PULL_UP && Math.abs(getScrollY()) > mFooterActionPosition) {
            autoBackToLoadingPosition();
        }
    }

    /**
     * 回到Loading状态
     */
    private void autoBackToLoadingPosition() {
        mStartLoadingTime = System.currentTimeMillis();
        if (mStatus == PULL_DOWN) {
            mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - mHeaderHoldingPosition, 400);
            invalidate();
            if (!isLoading) {
                isLoading = true;
                if (mListener != null)
                    mListener.onRefresh();
            }
        }
        if (mStatus == PULL_UP) {
            mScroller.startScroll(0, getScrollY(), 0, mFooterHoldingPosition - getScrollY(), 400);
            invalidate();
            if (!isLoading) {
                isLoading = true;
                if (mListener != null)
                    mListener.onLoadMore();
            }
        }
        loadingIndicator();
    }

    /**
     * 回到初始状态
     */
    private void autoBackToOriginalPosition() {
        mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 400);
        invalidate();
        this.postDelayed(new Runnable() {

            @Override
            public void run() {
                restoreIndicator();
                mStatus = IDLE;
            }
        }, 500);
    }

    private boolean isContentScrollToTop() {
        return !ViewCompat.canScrollVertically(contentView, -1);
    }

    private boolean isContentScrollToBottom() {
        return !ViewCompat.canScrollVertically(contentView, 1);
    }

    /**
     * 在拖动过程中调用Indicator(Header或Footer)的接口函数完成相应的指示性变化
     * 例如:下拉刷新、放开刷新的变化
     */
    private void updateIndicator() {
        if (mStatus == PULL_DOWN && deltaY > 0) {
            if (Math.abs(getScrollY()) > mHeaderActionPosition) {
                mHeaderIndicator.onAction();
            }
        } else if (mStatus == PULL_DOWN && deltaY < 0) {
            if (Math.abs(getScrollY()) < mHeaderActionPosition) {
                mHeaderIndicator.onUnaction();
            }
        } else if (mStatus == PULL_UP && deltaY < 0) {
            if (Math.abs(getScrollY()) > mFooterActionPosition) {
                mFooterIndicator.onAction();
            }
        } else if (mStatus == PULL_UP && deltaY > 0) {
            if (Math.abs(getScrollY()) < mFooterActionPosition) {
                mFooterIndicator.onUnaction();
            }
        }
    }

    /**
     * 本控件自动返回初始位置后恢复Indicator到初始状态
     */
    private void restoreIndicator() {
        mHeaderIndicator.onRestore();
        mFooterIndicator.onRestore();
    }

    /**
     * 本控件自动返回Loading位置后设置Indicator为Loading状态
     */
    private void loadingIndicator() {
        if (mStatus == PULL_DOWN) {
            mHeaderIndicator.onLoading();
        }
        if (mStatus == PULL_UP) {
            mFooterIndicator.onLoading();
        }
    }

    public void onFinishLoading() {
        long delta = System.currentTimeMillis() - mStartLoadingTime;
        if (delta > 2000) {
            isLoading = false;
            autoBackToOriginalPosition();
        } else {
            new Handler().postDelayed(new Runnable() {

                @Override
                public void run() {
                    isLoading = false;
                    autoBackToOriginalPosition();
                }
            }, 1000);
        }
    }

    public void onAutoRefresh() {
        mStartLoadingTime = System.currentTimeMillis();
        mStatus = PULL_DOWN;
        mScroller.startScroll(0, getScrollY(), 0, -mHeaderHoldingPosition, 400);
        invalidate();
        if (!isLoading) {
            isLoading = true;
            if (mListener != null)
                mListener.onRefresh();
        }
        loadingIndicator();
    }

    /**
     * Interface
     */
    public interface OnRefreshListener {

        void onRefresh();

        void onLoadMore();
    }

    private BaseIndicator getIndicator(String clreplacedName) {
        if (!TextUtils.isEmpty(clreplacedName)) {
            try {
                Clreplaced clazz = Clreplaced.forName(clreplacedName);
                return (BaseIndicator) clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * Getter and Setter
     */
    public void setListener(OnRefreshListener mListener) {
        this.mListener = mListener;
    }

    public boolean isPullDownEnable() {
        return isPullDownEnable;
    }

    public void setPullDownEnable(boolean pullDownEnable) {
        isPullDownEnable = pullDownEnable;
    }

    public boolean isPullUpEnable() {
        return isPullUpEnable;
    }

    public void setPullUpEnable(boolean pullUpEnable) {
        isPullUpEnable = pullUpEnable;
    }
}

19 Source : TvGridLayout.java
with Apache License 2.0
from woshidasusu

/**
 * Created by dasu on 2018/4/23.
 * 微信公众号:dasuAndroidTv
 * blog:https://www.jianshu.com/u/bb52a2918096
 *
 * 网格容器
 */
public clreplaced TvGridLayout extends FrameLayout {

    private static final String TAG = "TvGridLayout";

    // 滑动的时长
    private static final int ANIMATED_SCROLL_GAP = 500;

    private static Interpolator sInterpolator = new AccelerateDecelerateInterpolator();

    private static int[] sTwoInt = new int[2];

    private Context mContext;

    private OverScroller mScroller;

    private long mLastScroll;

    private int mCurPageIndex = 0;

    private int mRightEdge;

    private boolean mIsOnScrolling;

    private Adapter mAdapter;

    private int mWidth;

    private int mHeight;

    private int mItemSpace;

    private boolean mIsConsumeKeyEvent;

    private SparseArray<View> mFirstChildOfPage = new SparseArray<>();

    private SparseIntArray mWidthOfPage = new SparseIntArray();

    private OnBorderListener mBorderListener;

    private OnScrollListener mScrollListener;

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

    private void init(Context context) {
        mContext = context;
        setHorizontalScrollBarEnabled(false);
        mScroller = new OverScroller(context, sInterpolator);
        setClipChildren(false);
        setClipToPadding(false);
    }

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

    public void sereplacedemSpace(int itemSpace) {
        mItemSpace = itemSpace;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int x = mScroller.getCurrX();
            int finalX = mScroller.getFinalX();
            if (oldX != x) {
                scrollTo(x, 0);
            }
            if (x == finalX) {
                if (mIsOnScrolling) {
                    mIsOnScrolling = false;
                    if (mScrollListener != null) {
                        mScrollListener.onScrollEnd();
                    }
                }
            }
        } else {
            if (mIsOnScrolling) {
                mIsOnScrolling = false;
                if (mScrollListener != null) {
                    mScrollListener.onScrollEnd();
                }
            }
        }
    }

    public Adapter getAdapter() {
        return mAdapter;
    }

    public void setAdapter(Adapter adapter) {
        if (mAdapter == adapter) {
            return;
        }
        if (mAdapter != null) {
            mAdapter.onSwitchAdapter(adapter, mAdapter);
        }
        mAdapter = adapter;
        if (mAdapter != null) {
            post(new Runnable() {

                @Override
                public void run() {
                    removeAllViews();
                    layoutChildren();
                }
            });
        }
    }

    private void layoutChildren() {
        mFirstChildOfPage.clear();
        mWidthOfPage.clear();
        mRightEdge = 0;
        mCurPageIndex = 0;
        // layoutChildrenOfPages(0, mAdapter.getGroupCount());
        if (this.getLocalVisibleRect(new Rect())) {
            layoutChildrenOfPages(0, 1);
            post(new Runnable() {

                @Override
                public void run() {
                    layoutChildrenOfPages(1, mAdapter.getPageCount());
                }
            });
        } else {
            post(new Runnable() {

                @Override
                public void run() {
                    layoutChildrenOfPages(0, mAdapter.getPageCount());
                }
            });
        }
    }

    private void layoutChildrenOfPages(int fromPage, int toPage) {
        int contentWidth = mWidth - getPaddingLeft() - getPaddingRight();
        int contentHeight = mHeight - getPaddingTop() - getPaddingBottom();
        for (int j = fromPage; j < toPage; j++) {
            // 列数
            int column = mAdapter.getPageColumn(j);
            // 行数
            int row = mAdapter.getPageRow(j);
            // 每个item宽度
            float itemWidth = (contentWidth) * 1.0f / column;
            // 每个item高度
            float itemHeight = (contentHeight) * 1.0f / row;
            int pageWidth = 0;
            // 遍历每个item
            for (int i = 0; i < mAdapter.getChildCount(j); i++) {
                ItemCoordinate childCoordinate = mAdapter.getChildCoordinate(j, i);
                if (childCoordinate == null) {
                    continue;
                }
                int pointStartX = childCoordinate.start.x;
                int pointStartY = childCoordinate.start.y;
                int pointEndX = childCoordinate.end.x;
                int pointEndY = childCoordinate.end.y;
                // item大小,包括间距
                int width = (int) ((pointEndX - pointStartX) * itemWidth);
                int height = (int) ((pointEndY - pointStartY) * itemHeight);
                // item位置
                int marginLeft = (int) (pointStartX * itemWidth + contentWidth * j);
                int marginTop = (int) (pointStartY * itemHeight);
                if (marginLeft < 0) {
                    marginLeft = 0;
                }
                if (marginTop < 0) {
                    marginTop = 0;
                }
                // 获取item view
                View view = mAdapter.getChildView(j, i, width, height);
                if (view == null) {
                    continue;
                }
                // 开始layout
                // 扣除间距
                LayoutParams params = new LayoutParams(width - mItemSpace * 2, height - mItemSpace * 2);
                params.topMargin = marginTop + mItemSpace;
                params.leftMargin = marginLeft + mItemSpace;
                params.mItemCoordinate = childCoordinate;
                params.pageIndex = j;
                // 记录每一页长度
                int maxWidth = marginLeft + width - contentWidth * j;
                pageWidth = Math.max(pageWidth, maxWidth);
                int maxRight = marginLeft + width;
                mRightEdge = Math.max(mRightEdge, maxRight);
                if (childCoordinate.start.x == 0 && childCoordinate.start.y == 0) {
                    mFirstChildOfPage.put(j, view);
                }
                if (j == 0 && childCoordinate.start.x == 0 && childCoordinate.start.y == 0) {
                    addView(view, 0, params);
                } else {
                    addView(view, params);
                }
            }
            mWidthOfPage.put(j, pageWidth);
        }
    }

    public View getFirstChildOfScreen(int screenIndex) {
        if (mFirstChildOfPage != null) {
            return mFirstChildOfPage.get(screenIndex);
        }
        return null;
    }

    public void setOnBorderListener(OnBorderListener listener) {
        mBorderListener = listener;
    }

    public void smoothScrollTo(int dx) {
        smoothScrollBy(dx - getScrollX());
    }

    public void smoothScrollBy(int dx) {
        if (getChildCount() == 0) {
            // Nothing to do.
            return;
        }
        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
        final int width = getWidth() - getPaddingLeft();
        final int rightEdge = mRightEdge + getPaddingRight();
        final int maxX = Math.max(0, rightEdge - width);
        if (duration > ANIMATED_SCROLL_GAP) {
            if (mScrollListener != null) {
                mScrollListener.onScrollStart();
            }
            mIsOnScrolling = true;
            final int scrollX = getScrollX();
            dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
            mScroller.startScroll(scrollX, 0, dx, 0, ANIMATED_SCROLL_GAP);
            postInvalidateOnAnimation();
        } else {
            int finalX = 0;
            boolean needAdjustScrollX = false;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                finalX = mScroller.getFinalX();
                needAdjustScrollX = true;
            }
            dx = Math.max(0, Math.min(finalX + dx, maxX)) - finalX;
            if (needAdjustScrollX) {
                dx = finalX + dx;
            } else {
                dx = getScrollX() + dx;
            }
            if (mScrollListener != null) {
                mScrollListener.onScrollStart();
            }
            scrollTo(dx, getScrollY());
            if (mScrollListener != null) {
                post(new Runnable() {

                    @Override
                    public void run() {
                        mScrollListener.onScrollEnd();
                    }
                });
            }
        }
        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
    }

    private boolean executeKeyEvent(KeyEvent event) {
        int keyCode = event.getKeyCode();
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            mIsConsumeKeyEvent = false;
            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
                if (checkIfOnBorder(FOCUS_LEFT, sTwoInt)) {
                    mIsConsumeKeyEvent = true;
                    if (mBorderListener != null && mBorderListener.onLeft(sTwoInt[0], sTwoInt[1])) {
                        return true;
                    }
                    scrollToPage(sTwoInt[1]);
                }
            } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
                if (checkIfOnBorder(FOCUS_RIGHT, sTwoInt)) {
                    mIsConsumeKeyEvent = true;
                    if (mBorderListener != null && mBorderListener.onRight(sTwoInt[0], sTwoInt[1])) {
                        return true;
                    }
                    scrollToPage(sTwoInt[1]);
                }
            }
        } else {
            if (mIsConsumeKeyEvent) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        // GridMenuLayout与ViewPager合用时,当GridMenuLayout焦点在边界移动时,会去触发ViewPager的切菜单事件
        // 对于下个菜单的GridMenuLayout,需要默认聚焦到第一个子View,使用系统默认的焦点寻找策略会出问题
        // 所以在这个回调里进行处理
        if (previouslyFocusedRect == null && (direction != FOCUS_UP && direction != FOCUS_DOWN)) {
            final View view = mFirstChildOfPage.get(mCurPageIndex);
            if (view != null) {
                view.post(new Runnable() {

                    @Override
                    public void run() {
                        view.requestFocus();
                    }
                });
            }
        }
        return super.requestFocus(direction, previouslyFocusedRect);
    }

    private boolean checkIfOnBorder(int direction, int[] twoPage) {
        if (direction == FOCUS_LEFT || direction == FOCUS_RIGHT) {
            View view = getFocusedChild();
            View childView = findChildView(view);
            if (childView != null) {
                int curPage = ((LayoutParams) childView.getLayoutParams()).pageIndex;
                twoPage[0] = twoPage[1] = curPage;
                View nextFocusView = view.focusSearch(direction);
                View nextChildView = findChildView(nextFocusView);
                if (nextChildView == null) {
                    return true;
                }
                int nextPage = ((LayoutParams) nextChildView.getLayoutParams()).pageIndex;
                twoPage[1] = nextPage;
                return curPage != nextPage;
            }
        }
        return false;
    }

    public void scrollToPage(int pageIndex) {
        scrollToPage(pageIndex, true);
    }

    private View findChildView(View view) {
        View childView = null;
        if (view != null) {
            if (view.getParent() == this) {
                childView = view;
            } else {
                boolean isChild = false;
                ViewParent parent = view.getParent();
                for (; parent.getParent() instanceof ViewGroup; ) {
                    if (parent.getParent() == this) {
                        isChild = true;
                        break;
                    }
                    parent = parent.getParent();
                }
                if (isChild) {
                    childView = (View) parent;
                }
            }
        }
        return childView;
    }

    public void scrollToPage(int pageIndex, boolean smooth) {
        if (mCurPageIndex != pageIndex) {
            int pageWidth = getWidth() - getPaddingLeft() - getPaddingRight();
            int scrollXDelta = (pageIndex - mCurPageIndex) * (pageWidth);
            // todo 计算有问题
            if (mCurPageIndex == 0 || mCurPageIndex == mAdapter.getPageCount() - 1) {
                int w = mWidthOfPage.get(mCurPageIndex, 0);
                if (pageWidth != w) {
                    int adjustW = pageWidth - w;
                    scrollXDelta += scrollXDelta > 0 ? -adjustW : adjustW;
                }
            }
            if (scrollXDelta != 0) {
                if (smooth) {
                    smoothScrollBy(scrollXDelta);
                } else {
                    final int width = getWidth();
                    final int rightEdge = mRightEdge + getPaddingRight();
                    final int maxX = Math.max(0, rightEdge - width);
                    final int scrollX = getScrollX();
                    int dx = Math.max(0, Math.min(scrollX + scrollXDelta, maxX)) - scrollX;
                    scrollBy(dx, 0);
                }
            }
            mCurPageIndex = pageIndex;
        }
    }

    public void setOnScrollListener(OnScrollListener listener) {
        mScrollListener = listener;
    }

    public int getCurrentPage() {
        return mCurPageIndex;
    }

    public interface OnBorderListener {

        boolean onLeft(int curPageIndex, int nextPageIndex);

        boolean onRight(int curPageIndex, int nextPageIndex);
    }

    public interface OnScrollListener {

        void onScrollStart();

        void onScrollEnd();
    }

    public static abstract clreplaced Adapter {

        public abstract int getPageRow(int pageIndex);

        public abstract int getPageColumn(int pageIndex);

        public abstract ItemCoordinate getChildCoordinate(int pageIndex, int childIndex);

        public abstract View getChildView(int groupPosition, int childPosition, int childW, int childH);

        public abstract int getChildCount(int pageIndex);

        public abstract int getPageCount();

        protected void onSwitchAdapter(Adapter newAdapter, Adapter oldAdapter) {
        }
    }

    /**
     * 用于记录每个小格item项的坐标位置
     */
    public static clreplaced ItemCoordinate {

        // 左上角坐标
        public Point start;

        // 右下角坐标
        public Point end;

        @Override
        public String toString() {
            return "ItemCoordinate{" + "start=" + start + ", end=" + end + '}';
        }
    }

    private static clreplaced LayoutParams extends FrameLayout.LayoutParams {

        ItemCoordinate mItemCoordinate;

        int pageIndex;

        public LayoutParams(int width, int height) {
            super(width, height);
        }
    }
}

19 Source : SwipeMenuLayout.java
with Apache License 2.0
from weileng11

/**
 * Created by Yan Zhenjie on 2016/7/27.
 */
public clreplaced SwipeMenuLayout extends FrameLayout implements SwipeSwitch {

    public static final int DEFAULT_SCROLLER_DURATION = 200;

    private int mLeftViewId = 0;

    private int mContentViewId = 0;

    private int mRightViewId = 0;

    private float mOpenPercent = 0.5f;

    private int mScrollerDuration = DEFAULT_SCROLLER_DURATION;

    private int mScaledTouchSlop;

    private int mLastX;

    private int mLastY;

    private int mDownX;

    private int mDownY;

    private View mContentView;

    private SwipeLeftHorizontal mSwipeLeftHorizontal;

    private SwipeRightHorizontal mSwipeRightHorizontal;

    private SwipeHorizontal mSwipeCurrentHorizontal;

    private boolean shouldResetSwipe;

    private boolean mDragging;

    private boolean swipeEnable = true;

    private OverScroller mScroller;

    private VelocityTracker mVelocityTracker;

    private int mScaledMinimumFlingVelocity;

    private int mScaledMaximumFlingVelocity;

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

    public SwipeMenuLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.recycler_swipe_SwipeMenuLayout);
        mLeftViewId = typedArray.getResourceId(R.styleable.recycler_swipe_SwipeMenuLayout_leftViewId, mLeftViewId);
        mContentViewId = typedArray.getResourceId(R.styleable.recycler_swipe_SwipeMenuLayout_contentViewId, mContentViewId);
        mRightViewId = typedArray.getResourceId(R.styleable.recycler_swipe_SwipeMenuLayout_rightViewId, mRightViewId);
        typedArray.recycle();
        ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mScaledTouchSlop = configuration.getScaledTouchSlop();
        mScaledMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
        mScaledMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
        mScroller = new OverScroller(getContext());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (mLeftViewId != 0 && mSwipeLeftHorizontal == null) {
            View view = findViewById(mLeftViewId);
            mSwipeLeftHorizontal = new SwipeLeftHorizontal(view);
        }
        if (mRightViewId != 0 && mSwipeRightHorizontal == null) {
            View view = findViewById(mRightViewId);
            mSwipeRightHorizontal = new SwipeRightHorizontal(view);
        }
        if (mContentViewId != 0 && mContentView == null) {
            mContentView = findViewById(mContentViewId);
        } else {
            TextView errorView = new TextView(getContext());
            errorView.setClickable(true);
            errorView.setGravity(Gravity.CENTER);
            errorView.setTextSize(16);
            errorView.setText("You may not have set the ContentView.");
            mContentView = errorView;
            addView(mContentView);
        }
    }

    /**
     * Set whether open swipe. Default is true.
     *
     * @param swipeEnable true open, otherwise false.
     */
    public void setSwipeEnable(boolean swipeEnable) {
        this.swipeEnable = swipeEnable;
    }

    /**
     * Open the swipe function of the Item?
     *
     * @return open is true, otherwise is false.
     */
    public boolean isSwipeEnable() {
        return swipeEnable;
    }

    /**
     * Set open percentage.
     *
     * @param openPercent such as 0.5F.
     */
    public void setOpenPercent(float openPercent) {
        this.mOpenPercent = openPercent;
    }

    /**
     * Get open percentage.
     *
     * @return such as 0.5F.
     */
    public float getOpenPercent() {
        return mOpenPercent;
    }

    /**
     * The duration of the set.
     *
     * @param scrollerDuration such as 500.
     */
    public void setScrollerDuration(int scrollerDuration) {
        this.mScrollerDuration = scrollerDuration;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercepted = super.onInterceptTouchEvent(ev);
        int action = ev.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    mDownX = mLastX = (int) ev.getX();
                    mDownY = (int) ev.getY();
                    return false;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    int disX = (int) (ev.getX() - mDownX);
                    int disY = (int) (ev.getY() - mDownY);
                    boolean i = Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY);
                    return i;
                }
            case MotionEvent.ACTION_UP:
                {
                    boolean isClick = mSwipeCurrentHorizontal != null && mSwipeCurrentHorizontal.isClickOnContentView(getWidth(), ev.getX());
                    if (isMenuOpen() && isClick) {
                        smoothCloseMenu();
                        return true;
                    }
                    return false;
                }
            case MotionEvent.ACTION_CANCEL:
                {
                    if (!mScroller.isFinished())
                        mScroller.abortAnimation();
                    return false;
                }
        }
        return isIntercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mVelocityTracker == null)
            mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(ev);
        int dx;
        int dy;
        int action = ev.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    mLastX = (int) ev.getX();
                    mLastY = (int) ev.getY();
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    if (!isSwipeEnable())
                        break;
                    int disX = (int) (mLastX - ev.getX());
                    int disY = (int) (mLastY - ev.getY());
                    if (!mDragging && Math.abs(disX) > mScaledTouchSlop && Math.abs(disX) > Math.abs(disY)) {
                        mDragging = true;
                    }
                    if (mDragging) {
                        if (mSwipeCurrentHorizontal == null || shouldResetSwipe) {
                            if (disX < 0) {
                                if (mSwipeLeftHorizontal != null)
                                    mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
                                else
                                    mSwipeCurrentHorizontal = mSwipeRightHorizontal;
                            } else {
                                if (mSwipeRightHorizontal != null)
                                    mSwipeCurrentHorizontal = mSwipeRightHorizontal;
                                else
                                    mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
                            }
                        }
                        scrollBy(disX, 0);
                        mLastX = (int) ev.getX();
                        mLastY = (int) ev.getY();
                        shouldResetSwipe = false;
                    }
                    break;
                }
            case MotionEvent.ACTION_UP:
                {
                    dx = (int) (mDownX - ev.getX());
                    dy = (int) (mDownY - ev.getY());
                    mDragging = false;
                    mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
                    int velocityX = (int) mVelocityTracker.getXVelocity();
                    int velocity = Math.abs(velocityX);
                    if (velocity > mScaledMinimumFlingVelocity) {
                        if (mSwipeCurrentHorizontal != null) {
                            int duration = getSwipeDuration(ev, velocity);
                            if (mSwipeCurrentHorizontal instanceof SwipeRightHorizontal) {
                                if (velocityX < 0) {
                                    smoothOpenMenu(duration);
                                } else {
                                    smoothCloseMenu(duration);
                                }
                            } else {
                                if (velocityX > 0) {
                                    smoothOpenMenu(duration);
                                } else {
                                    smoothCloseMenu(duration);
                                }
                            }
                            ViewCompat.postInvalidateOnAnimation(this);
                        }
                    } else {
                        judgeOpenClose(dx, dy);
                    }
                    mVelocityTracker.clear();
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                    if (Math.abs(mDownX - ev.getX()) > mScaledTouchSlop || Math.abs(mDownY - ev.getY()) > mScaledTouchSlop || isLeftMenuOpen() || isRightMenuOpen()) {
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        super.onTouchEvent(ev);
                        return true;
                    }
                    break;
                }
            case MotionEvent.ACTION_CANCEL:
                {
                    mDragging = false;
                    if (!mScroller.isFinished()) {
                        mScroller.abortAnimation();
                    } else {
                        dx = (int) (mDownX - ev.getX());
                        dy = (int) (mDownY - ev.getY());
                        judgeOpenClose(dx, dy);
                    }
                    break;
                }
        }
        return super.onTouchEvent(ev);
    }

    /**
     * compute finish duration.
     *
     * @param ev       up event.
     * @param velocity velocity x.
     * @return finish duration.
     */
    private int getSwipeDuration(MotionEvent ev, int velocity) {
        int sx = getScrollX();
        int dx = (int) (ev.getX() - sx);
        final int width = mSwipeCurrentHorizontal.getMenuWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
        int duration;
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageDelta = (float) Math.abs(dx) / width;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, mScrollerDuration);
        return duration;
    }

    float distanceInfluenceForSnapDuration(float f) {
        // center the values about 0.
        f -= 0.5f;
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    private void judgeOpenClose(int dx, int dy) {
        if (mSwipeCurrentHorizontal != null) {
            if (Math.abs(getScrollX()) >= (mSwipeCurrentHorizontal.getMenuView().getWidth() * mOpenPercent)) {
                // auto open
                if (Math.abs(dx) > mScaledTouchSlop || Math.abs(dy) > mScaledTouchSlop) {
                    // swipe up
                    if (isMenuOpenNotEqual())
                        smoothCloseMenu();
                    else
                        smoothOpenMenu();
                } else {
                    // normal up
                    if (isMenuOpen())
                        smoothCloseMenu();
                    else
                        smoothOpenMenu();
                }
            } else {
                // auto closeMenu
                smoothCloseMenu();
            }
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        if (mSwipeCurrentHorizontal == null) {
            super.scrollTo(x, y);
        } else {
            SwipeHorizontal.Checker checker = mSwipeCurrentHorizontal.checkXY(x, y);
            shouldResetSwipe = checker.shouldResetSwipe;
            if (checker.x != getScrollX()) {
                super.scrollTo(checker.x, checker.y);
            }
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset() && mSwipeCurrentHorizontal != null) {
            if (mSwipeCurrentHorizontal instanceof SwipeRightHorizontal) {
                scrollTo(Math.abs(mScroller.getCurrX()), 0);
                invalidate();
            } else {
                scrollTo(-Math.abs(mScroller.getCurrX()), 0);
                invalidate();
            }
        }
    }

    public boolean hasLeftMenu() {
        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.canSwipe();
    }

    public boolean hasRightMenu() {
        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.canSwipe();
    }

    @Override
    public boolean isMenuOpen() {
        return isLeftMenuOpen() || isRightMenuOpen();
    }

    @Override
    public boolean isLeftMenuOpen() {
        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpen(getScrollX());
    }

    @Override
    public boolean isRightMenuOpen() {
        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpen(getScrollX());
    }

    @Override
    public boolean isCompleteOpen() {
        return isLeftCompleteOpen() || isRightMenuOpen();
    }

    @Override
    public boolean isLeftCompleteOpen() {
        return mSwipeLeftHorizontal != null && !mSwipeLeftHorizontal.isCompleteClose(getScrollX());
    }

    @Override
    public boolean isRightCompleteOpen() {
        return mSwipeRightHorizontal != null && !mSwipeRightHorizontal.isCompleteClose(getScrollX());
    }

    @Override
    public boolean isMenuOpenNotEqual() {
        return isLeftMenuOpenNotEqual() || isRightMenuOpenNotEqual();
    }

    @Override
    public boolean isLeftMenuOpenNotEqual() {
        return mSwipeLeftHorizontal != null && mSwipeLeftHorizontal.isMenuOpenNotEqual(getScrollX());
    }

    @Override
    public boolean isRightMenuOpenNotEqual() {
        return mSwipeRightHorizontal != null && mSwipeRightHorizontal.isMenuOpenNotEqual(getScrollX());
    }

    @Override
    public void smoothOpenMenu() {
        smoothOpenMenu(mScrollerDuration);
    }

    @Override
    public void smoothOpenLeftMenu() {
        smoothOpenLeftMenu(mScrollerDuration);
    }

    @Override
    public void smoothOpenRightMenu() {
        smoothOpenRightMenu(mScrollerDuration);
    }

    @Override
    public void smoothOpenLeftMenu(int duration) {
        if (mSwipeLeftHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
            smoothOpenMenu(duration);
        }
    }

    @Override
    public void smoothOpenRightMenu(int duration) {
        if (mSwipeRightHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeRightHorizontal;
            smoothOpenMenu(duration);
        }
    }

    private void smoothOpenMenu(int duration) {
        if (mSwipeCurrentHorizontal != null) {
            mSwipeCurrentHorizontal.autoOpenMenu(mScroller, getScrollX(), duration);
            invalidate();
        }
    }

    @Override
    public void smoothCloseMenu() {
        smoothCloseMenu(mScrollerDuration);
    }

    @Override
    public void smoothCloseLeftMenu() {
        if (mSwipeLeftHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeLeftHorizontal;
            smoothCloseMenu();
        }
    }

    @Override
    public void smoothCloseRightMenu() {
        if (mSwipeRightHorizontal != null) {
            mSwipeCurrentHorizontal = mSwipeRightHorizontal;
            smoothCloseMenu();
        }
    }

    @Override
    public void smoothCloseMenu(int duration) {
        if (mSwipeCurrentHorizontal != null) {
            mSwipeCurrentHorizontal.autoCloseMenu(mScroller, getScrollX(), duration);
            invalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int contentViewHeight = 0;
        if (mContentView != null) {
            measureChildWithMargins(mContentView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            contentViewHeight = mContentView.getMeasuredHeight();
        }
        if (mSwipeLeftHorizontal != null) {
            View leftMenu = mSwipeLeftHorizontal.getMenuView();
            int menuViewHeight = contentViewHeight == 0 ? leftMenu.getMeasuredHeightAndState() : contentViewHeight;
            int menuWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
            int menuHeightSpec = MeasureSpec.makeMeasureSpec(menuViewHeight, MeasureSpec.EXACTLY);
            leftMenu.measure(menuWidthSpec, menuHeightSpec);
        }
        if (mSwipeRightHorizontal != null) {
            View rightMenu = mSwipeRightHorizontal.getMenuView();
            int menuViewHeight = contentViewHeight == 0 ? rightMenu.getMeasuredHeightAndState() : contentViewHeight;
            int menuWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
            int menuHeightSpec = MeasureSpec.makeMeasureSpec(menuViewHeight, MeasureSpec.EXACTLY);
            rightMenu.measure(menuWidthSpec, menuHeightSpec);
        }
        if (contentViewHeight > 0) {
            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), contentViewHeight);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int contentViewHeight;
        if (mContentView != null) {
            int contentViewWidth = mContentView.getMeasuredWidthAndState();
            contentViewHeight = mContentView.getMeasuredHeightAndState();
            LayoutParams lp = (LayoutParams) mContentView.getLayoutParams();
            int start = getPaddingLeft();
            int top = getPaddingTop() + lp.topMargin;
            mContentView.layout(start, top, start + contentViewWidth, top + contentViewHeight);
        }
        if (mSwipeLeftHorizontal != null) {
            View leftMenu = mSwipeLeftHorizontal.getMenuView();
            int menuViewWidth = leftMenu.getMeasuredWidthAndState();
            int menuViewHeight = leftMenu.getMeasuredHeightAndState();
            LayoutParams lp = (LayoutParams) leftMenu.getLayoutParams();
            int top = getPaddingTop() + lp.topMargin;
            leftMenu.layout(-menuViewWidth, top, 0, top + menuViewHeight);
        }
        if (mSwipeRightHorizontal != null) {
            View rightMenu = mSwipeRightHorizontal.getMenuView();
            int menuViewWidth = rightMenu.getMeasuredWidthAndState();
            int menuViewHeight = rightMenu.getMeasuredHeightAndState();
            LayoutParams lp = (LayoutParams) rightMenu.getLayoutParams();
            int top = getPaddingTop() + lp.topMargin;
            int parentViewWidth = getMeasuredWidthAndState();
            rightMenu.layout(parentViewWidth, top, parentViewWidth + menuViewWidth, top + menuViewHeight);
        }
    }
}

19 Source : Viewport.java
with Apache License 2.0
from weexteam

/**
 * This is the default implementation for the viewport.
 * This implementation so for a normal viewport
 * where there is a horizontal x-axis and a
 * vertical y-axis.
 */
public clreplaced Viewport {

    /**
     * this reference value is used to generate the
     * vertical labels. It is used when the y axis bounds
     * is set manual and humanRounding=false. it will be the minValueY value.
     */
    protected double referenceY = Double.NaN;

    /**
     * this reference value is used to generate the
     * horizontal labels. It is used when the x axis bounds
     * is set manual and humanRounding=false. it will be the minValueX value.
     */
    protected double referenceX = Double.NaN;

    /**
     * flag whether the vertical scaling is activated
     */
    protected boolean scalableY;

    /**
     * the reference number to generate the labels
     *
     * @return by default 0, only when manual bounds and no human rounding
     * is active, the min x value is returned
     */
    protected double getReferenceX() {
        // if the bounds is manual then we take the
        // original manual min y value as reference
        if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) {
            if (Double.isNaN(referenceX)) {
                referenceX = getMinX(false);
            }
            return referenceX;
        } else {
            // starting from 0 so that the steps have nice numbers
            return 0;
        }
    }

    /**
     * listener to notify when x bounds changed after
     * scaling or scrolling.
     * This can be used to load more detailed data.
     */
    public interface OnXAxisBoundsChangedListener {

        /**
         * Called after scaling or scrolling with
         * the new bounds
         *
         * @param minX min x value
         * @param maxX max x value
         */
        void onXAxisBoundsChanged(double minX, double maxX, OnXAxisBoundsChangedListener.Reason reason);

        public enum Reason {

            SCROLL, SCALE
        }
    }

    /**
     * listener for the scale gesture
     */
    private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {

        /**
         * called by android
         * @param detector detector
         * @return always true
         */
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // --- horizontal scaling ---
            double viewportWidth = mCurrentViewport.width();
            if (mMaxXAxisSize != 0) {
                if (viewportWidth > mMaxXAxisSize) {
                    viewportWidth = mMaxXAxisSize;
                }
            }
            double center = mCurrentViewport.left + viewportWidth / 2;
            float scaleSpanX;
            if (android.os.Build.VERSION.SDK_INT >= 11 && scalableY) {
                scaleSpanX = detector.getCurrentSpanX() / detector.getPreviousSpanX();
            } else {
                scaleSpanX = detector.getScaleFactor();
            }
            viewportWidth /= scaleSpanX;
            mCurrentViewport.left = center - viewportWidth / 2;
            mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
            // viewportStart must not be < minX
            double minX = getMinX(true);
            if (mCurrentViewport.left < minX) {
                mCurrentViewport.left = minX;
                mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
            }
            // viewportStart + viewportSize must not be > maxX
            double maxX = getMaxX(true);
            if (viewportWidth == 0) {
                mCurrentViewport.right = maxX;
            }
            double overlap = mCurrentViewport.left + viewportWidth - maxX;
            if (overlap > 0) {
                // scroll left
                if (mCurrentViewport.left - overlap > minX) {
                    mCurrentViewport.left -= overlap;
                    mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
                } else {
                    // maximal scale
                    mCurrentViewport.left = minX;
                    mCurrentViewport.right = maxX;
                }
            }
            // --- vertical scaling ---
            if (scalableY && android.os.Build.VERSION.SDK_INT >= 11) {
                double viewportHeight = mCurrentViewport.height() * -1;
                if (mMaxYAxisSize != 0) {
                    if (viewportHeight > mMaxYAxisSize) {
                        viewportHeight = mMaxYAxisSize;
                    }
                }
                center = mCurrentViewport.bottom + viewportHeight / 2;
                viewportHeight /= detector.getCurrentSpanY() / detector.getPreviousSpanY();
                mCurrentViewport.bottom = center - viewportHeight / 2;
                mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
                // ignore bounds when second scale
                // viewportStart must not be < minY
                double minY = getMinY(true);
                if (mCurrentViewport.bottom < minY) {
                    mCurrentViewport.bottom = minY;
                    mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
                }
                // viewportStart + viewportSize must not be > maxY
                double maxY = getMaxY(true);
                if (viewportHeight == 0) {
                    mCurrentViewport.top = maxY;
                }
                overlap = mCurrentViewport.bottom + viewportHeight - maxY;
                if (overlap > 0) {
                    // scroll left
                    if (mCurrentViewport.bottom - overlap > minY) {
                        mCurrentViewport.bottom -= overlap;
                        mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
                    } else {
                        // maximal scale
                        mCurrentViewport.bottom = minY;
                        mCurrentViewport.top = maxY;
                    }
                }
            }
            // adjustSteps viewport, labels, etc.
            mGraphView.onDataChanged(true, false);
            ViewCompat.postInvalidateOnAnimation(mGraphView);
            return true;
        }

        /**
         * called when scaling begins
         *
         * @param detector detector
         * @return true if it is scalable
         */
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            if (mIsScalable) {
                mScalingActive = true;
                return true;
            } else {
                return false;
            }
        }

        /**
         * called when sacling ends
         * This will re-adjustSteps the viewport.
         *
         * @param detector detector
         */
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            mScalingActive = false;
            // notify
            if (mOnXAxisBoundsChangedListener != null) {
                mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCALE);
            }
            ViewCompat.postInvalidateOnAnimation(mGraphView);
        }
    };

    /**
     * simple gesture listener to track scroll events
     */
    private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {

        @Override
        public boolean onDown(MotionEvent e) {
            if (!mIsScrollable || mScalingActive)
                return false;
            // Initiates the decay phase of any active edge effects.
            releaseEdgeEffects();
            // Aborts any active scroll animations and invalidates.
            mScroller.forceFinished(true);
            ViewCompat.postInvalidateOnAnimation(mGraphView);
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (!mIsScrollable || mScalingActive)
                return false;
            // Scrolling uses math based on the viewport (as opposed to math using pixels).
            /**
             * Pixel offset is the offset in screen pixels, while viewport offset is the
             * offset within the current viewport. For additional information on surface sizes
             * and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
             * additional information about the viewport, see the comments for
             * {@link mCurrentViewport}.
             */
            double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth();
            double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight();
            int completeWidth = (int) ((mCompleteRange.width() / mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth());
            int completeHeight = (int) ((mCompleteRange.height() / mCurrentViewport.height()) * (double) mGraphView.getGraphContentHeight());
            int scrolledX = (int) (completeWidth * (mCurrentViewport.left + viewportOffsetX - mCompleteRange.left) / mCompleteRange.width());
            int scrolledY = (int) (completeHeight * (mCurrentViewport.bottom + viewportOffsetY - mCompleteRange.bottom) / mCompleteRange.height() * -1);
            boolean canScrollX = mCurrentViewport.left > mCompleteRange.left || mCurrentViewport.right < mCompleteRange.right;
            boolean canScrollY = mCurrentViewport.bottom > mCompleteRange.bottom || mCurrentViewport.top < mCompleteRange.top;
            // second scale
            double viewportOffsetY2 = 0d;
            canScrollY &= scrollableY;
            if (canScrollX) {
                if (viewportOffsetX < 0) {
                    double tooMuch = mCurrentViewport.left + viewportOffsetX - mCompleteRange.left;
                    if (tooMuch < 0) {
                        viewportOffsetX -= tooMuch;
                    }
                } else {
                    double tooMuch = mCurrentViewport.right + viewportOffsetX - mCompleteRange.right;
                    if (tooMuch > 0) {
                        viewportOffsetX -= tooMuch;
                    }
                }
                mCurrentViewport.left += viewportOffsetX;
                mCurrentViewport.right += viewportOffsetX;
                // notify
                if (mOnXAxisBoundsChangedListener != null) {
                    mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCROLL);
                }
            }
            if (canScrollY) {
                // if we have the second axis we ignore the max/min range
                if (viewportOffsetY < 0) {
                    double tooMuch = mCurrentViewport.bottom + viewportOffsetY - mCompleteRange.bottom;
                    if (tooMuch < 0) {
                        viewportOffsetY -= tooMuch;
                    }
                } else {
                    double tooMuch = mCurrentViewport.top + viewportOffsetY - mCompleteRange.top;
                    if (tooMuch > 0) {
                        viewportOffsetY -= tooMuch;
                    }
                }
                mCurrentViewport.top += viewportOffsetY;
                mCurrentViewport.bottom += viewportOffsetY;
            }
            if (canScrollX && scrolledX < 0) {
                mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth());
            }
            if (canScrollY && scrolledY < 0) {
                mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight());
            }
            if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) {
                mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth()) / (float) mGraphView.getGraphContentWidth());
            }
            if (canScrollY && scrolledY > completeHeight - mGraphView.getGraphContentHeight()) {
                mEdgeEffectTop.onPull((scrolledY - completeHeight + mGraphView.getGraphContentHeight()) / (float) mGraphView.getGraphContentHeight());
            }
            // adjustSteps viewport, labels, etc.
            mGraphView.onDataChanged(true, false);
            ViewCompat.postInvalidateOnAnimation(mGraphView);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // fling((int) -velocityX, (int) -velocityY);
            return true;
        }
    };

    /**
     * the state of the axis bounds
     */
    public enum AxisBoundsStatus {

        /**
         * initial means that the bounds gets
         * auto adjusted if they are not manual.
         * After adjusting the status comes to
         * #AUTO_ADJUSTED.
         */
        INITIAL,
        /**
         * after the bounds got auto-adjusted,
         * this status will set.
         */
        AUTO_ADJUSTED,
        /**
         * means that the bounds are fix (manually) and
         * are not to be auto-adjusted.
         */
        FIX
    }

    /**
     * paint to draw background
     */
    private Paint mPaint;

    /**
     * reference to the graphview
     */
    private final ChartView mGraphView;

    /**
     * this holds the current visible viewport
     * left = minX, right = maxX
     * bottom = minY, top = maxY
     */
    protected RectD mCurrentViewport = new RectD();

    /**
     * maximum allowed viewport size (horizontal)
     * 0 means use the bounds of the actual data that is
     * available
     */
    protected double mMaxXAxisSize = 0;

    /**
     * maximum allowed viewport size (vertical)
     * 0 means use the bounds of the actual data that is
     * available
     */
    protected double mMaxYAxisSize = 0;

    /**
     * this holds the whole range of the data
     * left = minX, right = maxX
     * bottom = minY, top = maxY
     */
    protected RectD mCompleteRange = new RectD();

    /**
     * flag whether scaling is currently active
     */
    protected boolean mScalingActive;

    /**
     * flag whether the viewport is scrollable
     */
    private boolean mIsScrollable;

    /**
     * flag whether the viewport is scalable
     */
    private boolean mIsScalable;

    /**
     * flag whether the viewport is scalable
     * on the Y axis
     */
    private boolean scrollableY;

    /**
     * gesture detector to detect scrolling
     */
    protected GestureDetector mGestureDetector;

    /**
     * detect scaling
     */
    protected ScaleGestureDetector mScaleGestureDetector;

    /**
     * not used - for fling
     */
    protected OverScroller mScroller;

    /**
     * not used
     */
    private EdgeEffectCompat mEdgeEffectTop;

    /**
     * not used
     */
    private EdgeEffectCompat mEdgeEffectBottom;

    /**
     * glow effect when scrolling left
     */
    private EdgeEffectCompat mEdgeEffectLeft;

    /**
     * glow effect when scrolling right
     */
    private EdgeEffectCompat mEdgeEffectRight;

    /**
     * state of the x axis
     */
    protected AxisBoundsStatus mXAxisBoundsStatus;

    /**
     * state of the y axis
     */
    protected AxisBoundsStatus mYAxisBoundsStatus;

    /**
     * flag whether the x axis bounds are manual
     */
    private boolean mXAxisBoundsManual;

    /**
     * flag whether the y axis bounds are manual
     */
    private boolean mYAxisBoundsManual;

    /**
     * background color of the viewport area
     * it is recommended to use a semi-transparent color
     */
    private int mBackgroundColor;

    /**
     * listener to notify when x bounds changed after
     * scaling or scrolling.
     * This can be used to load more detailed data.
     */
    protected OnXAxisBoundsChangedListener mOnXAxisBoundsChangedListener;

    /**
     * optional draw a border between the labels
     * and the viewport
     */
    private boolean mDrawBorder;

    /**
     * color of the border
     *
     * @see #setDrawBorder(boolean)
     */
    private Integer mBorderColor;

    /**
     * custom paint to use for the border
     *
     * @see #setDrawBorder(boolean)
     */
    private Paint mBorderPaint;

    /**
     * creates the viewport
     *
     * @param graphView graphview
     */
    Viewport(ChartView graphView) {
        mScroller = new OverScroller(graphView.getContext());
        mEdgeEffectTop = new EdgeEffectCompat(graphView.getContext());
        mEdgeEffectBottom = new EdgeEffectCompat(graphView.getContext());
        mEdgeEffectLeft = new EdgeEffectCompat(graphView.getContext());
        mEdgeEffectRight = new EdgeEffectCompat(graphView.getContext());
        mGestureDetector = new GestureDetector(graphView.getContext(), mGestureListener);
        mScaleGestureDetector = new ScaleGestureDetector(graphView.getContext(), mScaleGestureListener);
        mGraphView = graphView;
        mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        mBackgroundColor = Color.TRANSPARENT;
        mPaint = new Paint();
    }

    /**
     * will be called on a touch event.
     * needed to use scaling and scrolling
     *
     * @param event
     * @return true if it was consumed
     */
    public boolean onTouchEvent(MotionEvent event) {
        boolean b = mScaleGestureDetector.onTouchEvent(event);
        b |= mGestureDetector.onTouchEvent(event);
        return b;
    }

    /**
     * change the state of the x axis.
     * normally you do not call this method.
     * If you want to set manual axis use
     * {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
     *
     * @param s state
     */
    public void setXAxisBoundsStatus(AxisBoundsStatus s) {
        mXAxisBoundsStatus = s;
    }

    /**
     * change the state of the y axis.
     * normally you do not call this method.
     * If you want to set manual axis use
     * {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
     *
     * @param s state
     */
    public void setYAxisBoundsStatus(AxisBoundsStatus s) {
        mYAxisBoundsStatus = s;
    }

    /**
     * @return whether the viewport is scrollable
     */
    public boolean isScrollable() {
        return mIsScrollable;
    }

    /**
     * @param mIsScrollable whether is viewport is scrollable
     */
    public void setScrollable(boolean mIsScrollable) {
        this.mIsScrollable = mIsScrollable;
    }

    /**
     * @return the x axis state
     */
    public AxisBoundsStatus getXAxisBoundsStatus() {
        return mXAxisBoundsStatus;
    }

    /**
     * @return the y axis state
     */
    public AxisBoundsStatus getYAxisBoundsStatus() {
        return mYAxisBoundsStatus;
    }

    /**
     * caches the complete range (minX, maxX, minY, maxY)
     * by iterating all series and all datapoints and
     * stores it into #mCompleteRange
     * <p>
     * for the x-range it will respect the series on the
     * second scale - not for y-values
     */
    public void calcCompleteRange() {
        List<Series> series = mGraphView.getSeries();
        List<Series> seriesInclusiveSecondScale = new ArrayList<>(mGraphView.getSeries());
        mCompleteRange.set(0d, 0d, 0d, 0d);
        if (!seriesInclusiveSecondScale.isEmpty() && !seriesInclusiveSecondScale.get(0).isEmpty()) {
            double d = seriesInclusiveSecondScale.get(0).getLowestValueX();
            for (Series s : seriesInclusiveSecondScale) {
                if (!s.isEmpty() && d > s.getLowestValueX()) {
                    d = s.getLowestValueX();
                }
            }
            mCompleteRange.left = d;
            d = seriesInclusiveSecondScale.get(0).getHighestValueX();
            for (Series s : seriesInclusiveSecondScale) {
                if (!s.isEmpty() && d < s.getHighestValueX()) {
                    d = s.getHighestValueX();
                }
            }
            mCompleteRange.right = d;
            if (!series.isEmpty() && !series.get(0).isEmpty()) {
                d = series.get(0).getLowestValueY();
                for (Series s : series) {
                    if (!s.isEmpty() && d > s.getLowestValueY()) {
                        d = s.getLowestValueY();
                    }
                }
                mCompleteRange.bottom = d;
                d = series.get(0).getHighestValueY();
                for (Series s : series) {
                    if (!s.isEmpty() && d < s.getHighestValueY()) {
                        d = s.getHighestValueY();
                    }
                }
                mCompleteRange.top = d;
            }
        }
        // calc current viewport bounds
        if (mYAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
            mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        }
        if (mYAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
            mCurrentViewport.top = mCompleteRange.top;
            mCurrentViewport.bottom = mCompleteRange.bottom;
        }
        if (mXAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
            mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        }
        if (mXAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
            mCurrentViewport.left = mCompleteRange.left;
            mCurrentViewport.right = mCompleteRange.right;
        } else if (mXAxisBoundsManual && !mYAxisBoundsManual && mCompleteRange.width() != 0) {
            // getPerformanceList highest/lowest of current viewport
            // lowest
            double d = Double.MAX_VALUE;
            for (Series s : series) {
                Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
                while (values.hasNext()) {
                    double v = values.next().getY();
                    if (d > v) {
                        d = v;
                    }
                }
            }
            if (d != Double.MAX_VALUE) {
                mCurrentViewport.bottom = d;
            }
            // highest
            d = Double.MIN_VALUE;
            for (Series s : series) {
                Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
                while (values.hasNext()) {
                    double v = values.next().getY();
                    if (d < v) {
                        d = v;
                    }
                }
            }
            if (d != Double.MIN_VALUE) {
                mCurrentViewport.top = d;
            }
        }
        // fixes blank screen when range is zero
        if (mCurrentViewport.left == mCurrentViewport.right)
            mCurrentViewport.right++;
        if (mCurrentViewport.top == mCurrentViewport.bottom)
            mCurrentViewport.top++;
    }

    /**
     * @param completeRange if true => minX of the complete range of all series
     *                      if false => minX of the current visible viewport
     * @return the min x value
     */
    public double getMinX(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.left;
        } else {
            return mCurrentViewport.left;
        }
    }

    /**
     * @param completeRange if true => maxX of the complete range of all series
     *                      if false => maxX of the current visible viewport
     * @return the max x value
     */
    public double getMaxX(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.right;
        } else {
            return mCurrentViewport.right;
        }
    }

    /**
     * @param completeRange if true => minY of the complete range of all series
     *                      if false => minY of the current visible viewport
     * @return the min y value
     */
    public double getMinY(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.bottom;
        } else {
            return mCurrentViewport.bottom;
        }
    }

    /**
     * @param completeRange if true => maxY of the complete range of all series
     *                      if false => maxY of the current visible viewport
     * @return the max y value
     */
    public double getMaxY(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.top;
        } else {
            return mCurrentViewport.top;
        }
    }

    /**
     * set the maximal y value for the current viewport.
     * Make sure to set the y bounds to manual via
     * {@link #setYAxisBoundsManual(boolean)}
     *
     * @param y max / highest value
     */
    public void setMaxY(double y) {
        mCurrentViewport.top = y;
    }

    /**
     * set the minimal y value for the current viewport.
     * Make sure to set the y bounds to manual via
     * {@link #setYAxisBoundsManual(boolean)}
     *
     * @param y min / lowest value
     */
    public void setMinY(double y) {
        mCurrentViewport.bottom = y;
    }

    /**
     * set the maximal x value for the current viewport.
     * Make sure to set the x bounds to manual via
     * {@link #setXAxisBoundsManual(boolean)}
     *
     * @param x max / highest value
     */
    public void setMaxX(double x) {
        mCurrentViewport.right = x;
    }

    /**
     * set the minimal x value for the current viewport.
     * Make sure to set the x bounds to manual via
     * {@link #setXAxisBoundsManual(boolean)}
     *
     * @param x min / lowest value
     */
    public void setMinX(double x) {
        mCurrentViewport.left = x;
    }

    /**
     * release the glowing effects
     */
    private void releaseEdgeEffects() {
        mEdgeEffectLeft.onRelease();
        mEdgeEffectRight.onRelease();
        mEdgeEffectTop.onRelease();
        mEdgeEffectBottom.onRelease();
    }

    /**
     * not used currently
     *
     * @param velocityX
     * @param velocityY
     */
    private void fling(int velocityX, int velocityY) {
        velocityY = 0;
        releaseEdgeEffects();
        // Flings use math in pixels (as opposed to math based on the viewport).
        int maxX = (int) ((mCurrentViewport.width() / mCompleteRange.width()) * (float) mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth();
        int maxY = (int) ((mCurrentViewport.height() / mCompleteRange.height()) * (float) mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight();
        int startX = (int) ((mCurrentViewport.left - mCompleteRange.left) / mCompleteRange.width()) * maxX;
        int startY = (int) ((mCurrentViewport.top - mCompleteRange.top) / mCompleteRange.height()) * maxY;
        mScroller.forceFinished(true);
        mScroller.fling(startX, startY, velocityX, velocityY, 0, maxX, 0, maxY, mGraphView.getGraphContentWidth() / 2, mGraphView.getGraphContentHeight() / 2);
        ViewCompat.postInvalidateOnAnimation(mGraphView);
    }

    /**
     * not used currently
     */
    public void computeScroll() {
    }

    /**
     * Draws the overscroll "glow" at the four edges of the chart region, if necessary.
     *
     * @see EdgeEffectCompat
     */
    private void drawEdgeEffectsUnclipped(Canvas canvas) {
        // The methods below rotate and translate the canvas as needed before drawing the glow,
        // since EdgeEffectCompat always draws a top-glow at 0,0.
        boolean needsInvalidate = false;
        if (!mEdgeEffectTop.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop());
            mEdgeEffectTop.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
            if (mEdgeEffectTop.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeEffectBottom.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
            canvas.rotate(180, mGraphView.getGraphContentWidth() / 2, 0);
            mEdgeEffectBottom.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
            if (mEdgeEffectBottom.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeEffectLeft.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
            canvas.rotate(-90, 0, 0);
            mEdgeEffectLeft.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
            if (mEdgeEffectLeft.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeEffectRight.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop());
            canvas.rotate(90, 0, 0);
            mEdgeEffectRight.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
            if (mEdgeEffectRight.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (needsInvalidate) {
            ViewCompat.postInvalidateOnAnimation(mGraphView);
        }
    }

    /**
     * will be first called in order to draw
     * the canvas
     * Used to draw the background
     *
     * @param c canvas.
     */
    public void drawFirst(Canvas c) {
        // draw background
        if (mBackgroundColor != Color.TRANSPARENT) {
            mPaint.setColor(mBackgroundColor);
            c.drawRect(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaint);
        }
        if (mDrawBorder) {
            Paint p;
            if (mBorderPaint != null) {
                p = mBorderPaint;
            } else {
                p = mPaint;
                p.setColor(getBorderColor());
            }
            c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
            c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
        }
    }

    /**
     * draws the glowing edge effect
     *
     * @param c canvas
     */
    public void draw(Canvas c) {
        drawEdgeEffectsUnclipped(c);
    }

    /**
     * @return background of the viewport area
     */
    public int getBackgroundColor() {
        return mBackgroundColor;
    }

    /**
     * @param mBackgroundColor background of the viewport area
     *                         use transparent to have no background
     */
    public void setBackgroundColor(int mBackgroundColor) {
        this.mBackgroundColor = mBackgroundColor;
    }

    /**
     * @return whether the viewport is scalable
     */
    public boolean isScalable() {
        return mIsScalable;
    }

    /**
     * active the scaling/zooming feature
     * notice: sets the x axis bounds to manual
     *
     * @param mIsScalable whether the viewport is scalable
     */
    public void setScalable(boolean mIsScalable) {
        this.mIsScalable = mIsScalable;
        if (mIsScalable) {
            mIsScrollable = true;
            // set viewport to manual
            setXAxisBoundsManual(true);
        }
    }

    /**
     * @return whether the x axis bounds are manual.
     * @see #setMinX(double)
     * @see #setMaxX(double)
     */
    public boolean isXAxisBoundsManual() {
        return mXAxisBoundsManual;
    }

    /**
     * @param mXAxisBoundsManual whether the x axis bounds are manual.
     * @see #setMinX(double)
     * @see #setMaxX(double)
     */
    public void setXAxisBoundsManual(boolean mXAxisBoundsManual) {
        this.mXAxisBoundsManual = mXAxisBoundsManual;
        if (mXAxisBoundsManual) {
            mXAxisBoundsStatus = AxisBoundsStatus.FIX;
        }
    }

    /**
     * @return whether the y axis bound are manual
     */
    public boolean isYAxisBoundsManual() {
        return mYAxisBoundsManual;
    }

    /**
     * @param mYAxisBoundsManual whether the y axis bounds are manual
     * @see #setMaxY(double)
     * @see #setMinY(double)
     */
    public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
        this.mYAxisBoundsManual = mYAxisBoundsManual;
        if (mYAxisBoundsManual) {
            mYAxisBoundsStatus = AxisBoundsStatus.FIX;
        }
    }

    /**
     * forces the viewport to scroll to the end
     * of the range by keeping the current viewport size.
     * <p>
     * Important: Only takes effect if x axis bounds are manual.
     *
     * @see #setXAxisBoundsManual(boolean)
     */
    public void scrollToEnd() {
        if (mXAxisBoundsManual) {
            double size = mCurrentViewport.width();
            mCurrentViewport.right = mCompleteRange.right;
            mCurrentViewport.left = mCompleteRange.right - size;
            mGraphView.onDataChanged(true, false);
        } else {
            Log.w("GraphView", "scrollToEnd works only with manual x axis bounds");
        }
    }

    /**
     * @return the listener when there is one registered.
     */
    public OnXAxisBoundsChangedListener getOnXAxisBoundsChangedListener() {
        return mOnXAxisBoundsChangedListener;
    }

    /**
     * set a listener to notify when x bounds changed after
     * scaling or scrolling.
     * This can be used to load more detailed data.
     *
     * @param l the listener to use
     */
    public void setOnXAxisBoundsChangedListener(OnXAxisBoundsChangedListener l) {
        mOnXAxisBoundsChangedListener = l;
    }

    /**
     * optional draw a border between the labels
     * and the viewport
     *
     * @param drawBorder true to draw the border
     */
    public void setDrawBorder(boolean drawBorder) {
        this.mDrawBorder = drawBorder;
    }

    /**
     * the border color used. will be ignored when
     * a custom paint is set.
     *
     * @return border color. by default the grid color is used
     * @see #setDrawBorder(boolean)
     */
    public int getBorderColor() {
        if (mBorderColor != null) {
            return mBorderColor;
        }
        return mGraphView.getGridLabelRenderer().getGridColor();
    }

    /**
     * the border color used. will be ignored when
     * a custom paint is set.
     *
     * @param borderColor null to reset
     */
    public void setBorderColor(Integer borderColor) {
        this.mBorderColor = borderColor;
    }

    /**
     * custom paint to use for the border. border color
     * will be ignored
     *
     * @param borderPaint
     * @see #setDrawBorder(boolean)
     */
    public void setBorderPaint(Paint borderPaint) {
        this.mBorderPaint = borderPaint;
    }

    /**
     * activate/deactivate the vertical scrolling
     *
     * @param scrollableY true to activate
     */
    public void setScrollableY(boolean scrollableY) {
        this.scrollableY = scrollableY;
    }

    /**
     * the reference number to generate the labels
     *
     * @return by default 0, only when manual bounds and no human rounding
     * is active, the min y value is returned
     */
    protected double getReferenceY() {
        // if the bounds is manual then we take the
        // original manual min y value as reference
        if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRounding()) {
            if (Double.isNaN(referenceY)) {
                referenceY = getMinY(false);
            }
            return referenceY;
        } else {
            // starting from 0 so that the steps have nice numbers
            return 0;
        }
    }

    /**
     * activate or deactivate the vertical zooming/scaling functionallity.
     * This will automatically activate the vertical scrolling and the
     * horizontal scaling/scrolling feature.
     *
     * @param scalableY true to activate
     */
    public void setScalableY(boolean scalableY) {
        if (scalableY) {
            this.scrollableY = true;
            setScalable(true);
            if (android.os.Build.VERSION.SDK_INT < 11) {
                Log.w("GraphView", "Vertical scaling requires minimum Android 3.0 (API Level 11)");
            }
        }
        this.scalableY = scalableY;
    }

    /**
     * maximum allowed viewport size (horizontal)
     * 0 means use the bounds of the actual data that is
     * available
     */
    public double getMaxXAxisSize() {
        return mMaxXAxisSize;
    }

    /**
     * maximum allowed viewport size (vertical)
     * 0 means use the bounds of the actual data that is
     * available
     */
    public double getMaxYAxisSize() {
        return mMaxYAxisSize;
    }

    /**
     * Set the max viewport size (horizontal)
     * This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
     * could force the user to only be able to see 2 hours of data at a time.
     * Default value is 0 (disabled)
     *
     * @param mMaxXAxisViewportSize maximum size of viewport
     */
    public void setMaxXAxisSize(double mMaxXAxisViewportSize) {
        this.mMaxXAxisSize = mMaxXAxisViewportSize;
    }

    /**
     * Set the max viewport size (vertical)
     * This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
     * could force the user to only be able to see 2 hours of data at a time.
     * Default value is 0 (disabled)
     *
     * @param mMaxYAxisViewportSize maximum size of viewport
     */
    public void setMaxYAxisSize(double mMaxYAxisViewportSize) {
        this.mMaxYAxisSize = mMaxYAxisViewportSize;
    }

    public clreplaced RectD {

        public double left;

        public double right;

        public double top;

        public double bottom;

        public double width() {
            return right - left;
        }

        public double height() {
            return bottom - top;
        }

        public void set(double lLeft, double lTop, double lRight, double lBottom) {
            left = lLeft;
            right = lRight;
            top = lTop;
            bottom = lBottom;
        }
    }
}

19 Source : GingerScroller.java
with Apache License 2.0
from Vanish136

public clreplaced GingerScroller extends ScrollerProxy {

    protected final OverScroller mScroller;

    private boolean mFirstScroll = false;

    public GingerScroller(Context context) {
        mScroller = new OverScroller(context);
    }

    @Override
    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
    }

    @Override
    public boolean computeScrollOffset() {
        // Workaround for first scroll returning 0 for the direction of the edge it hits.
        // Simply recompute values.
        if (mFirstScroll) {
            mScroller.computeScrollOffset();
            mFirstScroll = false;
        }
        return mScroller.computeScrollOffset();
    }

    @Override
    public boolean isFinished() {
        return mScroller.isFinished();
    }

    @Override
    public void forceFinished(boolean finished) {
        mScroller.forceFinished(finished);
    }

    @Override
    public int getCurrX() {
        return mScroller.getCurrX();
    }

    @Override
    public int getCurrY() {
        return mScroller.getCurrY();
    }
}

19 Source : CardStackView.java
with Apache License 2.0
from triline3

public clreplaced CardStackView extends ViewGroup implements ScrollDelegate {

    private static final int INVALID_POINTER = -1;

    public static final int INVALID_TYPE = -1;

    public static final int ANIMATION_STATE_START = 0;

    public static final int ANIMATION_STATE_END = 1;

    public static final int ANIMATION_STATE_CANCEL = 2;

    private static final String TAG = "CardStackView";

    public static final int ALL_DOWN = 0;

    public static final int UP_DOWN = 1;

    public static final int UP_DOWN_STACK = 2;

    static final int DEFAULT_SELECT_POSITION = -1;

    private int mTotalLength;

    private int mOverlapGaps;

    private int mOverlapGapsCollapse;

    private int mNumBottomShow;

    private StackAdapter mStackAdapter;

    private final ViewDataObserver mObserver = new ViewDataObserver();

    private int mSelectPosition = DEFAULT_SELECT_POSITION;

    private int mShowHeight;

    private List<ViewHolder> mViewHolders;

    private AnimatorAdapter mAnimatorAdapter;

    private int mDuration;

    private OverScroller mScroller;

    private int mLastMotionY;

    private boolean mIsBeingDragged = false;

    private VelocityTracker mVelocityTracker;

    private int mTouchSlop;

    private int mMinimumVelocity;

    private int mMaximumVelocity;

    private int mActivePointerId = INVALID_POINTER;

    private final int[] mScrollOffset = new int[2];

    private int mNestedYOffset;

    private boolean mScrollEnable = true;

    private ScrollDelegate mScrollDelegate;

    private ItemExpendListener mItemExpendListener;

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

    public CardStackView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CardStackView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, 0);
    }

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

    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CardStackView, defStyleAttr, defStyleRes);
        setOverlapGaps(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGaps, dp2px(20)));
        setOverlapGapsCollapse(array.getDimensionPixelSize(R.styleable.CardStackView_stackOverlapGapsCollapse, dp2px(20)));
        setDuration(array.getInt(R.styleable.CardStackView_stackDuration, AnimatorAdapter.ANIMATION_DURATION));
        setAnimationType(array.getInt(R.styleable.CardStackView_stackAnimationType, UP_DOWN_STACK));
        setNumBottomShow(array.getInt(R.styleable.CardStackView_stackNumBottomShow, 3));
        array.recycle();
        mViewHolders = new ArrayList<>();
        initScroller();
    }

    private void initScroller() {
        mScroller = new OverScroller(getContext());
        setFocusable(true);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    }

    private int dp2px(int value) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (value * scale + 0.5f);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        checkContentHeightByParent();
        measureChild(widthMeasureSpec, heightMeasureSpec);
    }

    private void checkContentHeightByParent() {
        View parentView = (View) getParent();
        mShowHeight = parentView.getMeasuredHeight() - parentView.getPaddingTop() - parentView.getPaddingBottom();
    }

    private void measureChild(int widthMeasureSpec, int heightMeasureSpec) {
        int maxWidth = 0;
        mTotalLength = 0;
        mTotalLength += getPaddingTop() + getPaddingBottom();
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final int totalLength = mTotalLength;
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.mHeaderHeight == -1)
                lp.mHeaderHeight = child.getMeasuredHeight();
            final int childHeight = lp.mHeaderHeight;
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin);
            mTotalLength -= mOverlapGaps * 2;
            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
        }
        mTotalLength += mOverlapGaps * 2;
        int heightSize = mTotalLength;
        heightSize = Math.max(heightSize, mShowHeight);
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0), heightSizeAndState);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChild();
    }

    private void layoutChild() {
        int childTop = getPaddingTop();
        int childLeft = getPaddingLeft();
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            final int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            childTop += lp.topMargin;
            if (i != 0) {
                childTop -= mOverlapGaps * 2;
                child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
            } else {
                child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
            }
            childTop += lp.mHeaderHeight;
        }
    }

    public void updateSelectPosition(final int selectPosition) {
        post(new Runnable() {

            @Override
            public void run() {
                doCardClickAnimation(mViewHolders.get(selectPosition), selectPosition);
            }
        });
    }

    public void clearSelectPosition() {
        updateSelectPosition(mSelectPosition);
    }

    public void clearScrollYAndTranslation() {
        if (mSelectPosition != DEFAULT_SELECT_POSITION) {
            clearSelectPosition();
        }
        if (mScrollDelegate != null)
            mScrollDelegate.setViewScrollY(0);
        requestLayout();
    }

    public void setAdapter(StackAdapter stackAdapter) {
        mStackAdapter = stackAdapter;
        mStackAdapter.registerObserver(mObserver);
        refreshView();
    }

    public void setAnimationType(int type) {
        AnimatorAdapter animatorAdapter;
        switch(type) {
            case ALL_DOWN:
                animatorAdapter = new AllMoveDownAnimatorAdapter(this);
                break;
            case UP_DOWN:
                animatorAdapter = new UpDownAnimatorAdapter(this);
                break;
            default:
                animatorAdapter = new UpDownStackAnimatorAdapter(this);
                break;
        }
        setAnimatorAdapter(animatorAdapter);
    }

    public void setAnimatorAdapter(AnimatorAdapter animatorAdapter) {
        clearScrollYAndTranslation();
        mAnimatorAdapter = animatorAdapter;
        if (mAnimatorAdapter instanceof UpDownStackAnimatorAdapter) {
            mScrollDelegate = new StackScrollDelegateImpl(this);
        } else {
            mScrollDelegate = this;
        }
    }

    private void refreshView() {
        removeAllViews();
        mViewHolders.clear();
        for (int i = 0; i < mStackAdapter.gereplacedemCount(); i++) {
            ViewHolder holder = getViewHolder(i);
            holder.position = i;
            holder.onItemExpand(i == mSelectPosition);
            addView(holder.itemView);
            setClickAnimator(holder, i);
            mStackAdapter.bindViewHolder(holder, i);
        }
        requestLayout();
    }

    ViewHolder getViewHolder(int i) {
        if (i == DEFAULT_SELECT_POSITION)
            return null;
        ViewHolder viewHolder;
        if (mViewHolders.size() <= i || mViewHolders.get(i).mItemViewType != mStackAdapter.gereplacedemViewType(i)) {
            viewHolder = mStackAdapter.createView(this, mStackAdapter.gereplacedemViewType(i));
            mViewHolders.add(viewHolder);
        } else {
            viewHolder = mViewHolders.get(i);
        }
        return viewHolder;
    }

    private void setClickAnimator(final ViewHolder holder, final int position) {
        setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                if (mSelectPosition == DEFAULT_SELECT_POSITION)
                    return;
                performItemClick(mViewHolders.get(mSelectPosition));
            }
        });
        holder.itemView.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                performItemClick(holder);
            }
        });
    }

    public void next() {
        if (mSelectPosition == DEFAULT_SELECT_POSITION || mSelectPosition == mViewHolders.size() - 1)
            return;
        performItemClick(mViewHolders.get(mSelectPosition + 1));
    }

    public void pre() {
        if (mSelectPosition == DEFAULT_SELECT_POSITION || mSelectPosition == 0)
            return;
        performItemClick(mViewHolders.get(mSelectPosition - 1));
    }

    public boolean isExpending() {
        return mSelectPosition != DEFAULT_SELECT_POSITION;
    }

    public void performItemClick(ViewHolder viewHolder) {
        doCardClickAnimation(viewHolder, viewHolder.position);
    }

    private void doCardClickAnimation(final ViewHolder viewHolder, int position) {
        checkContentHeightByParent();
        mAnimatorAdapter.itemClick(viewHolder, position);
    }

    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }
        if (getViewScrollY() == 0 && !canScrollVertically(1)) {
            return false;
        }
        switch(action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE:
                {
                    final int activePointerId = mActivePointerId;
                    if (activePointerId == INVALID_POINTER) {
                        break;
                    }
                    final int pointerIndex = ev.findPointerIndex(activePointerId);
                    if (pointerIndex == -1) {
                        LogUtil.e("Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent");
                        break;
                    }
                    final int y = (int) ev.getY(pointerIndex);
                    final int yDiff = Math.abs(y - mLastMotionY);
                    if (yDiff > mTouchSlop) {
                        mIsBeingDragged = true;
                        mLastMotionY = y;
                        initVelocityTrackerIfNotExists();
                        mVelocityTracker.addMovement(ev);
                        mNestedYOffset = 0;
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                    break;
                }
            case MotionEvent.ACTION_DOWN:
                {
                    final int y = (int) ev.getY();
                    mLastMotionY = y;
                    mActivePointerId = ev.getPointerId(0);
                    initOrResetVelocityTracker();
                    mVelocityTracker.addMovement(ev);
                    mIsBeingDragged = !mScroller.isFinished();
                    break;
                }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                recycleVelocityTracker();
                if (mScroller.springBack(getViewScrollX(), getViewScrollY(), 0, 0, 0, getScrollRange())) {
                    postInvalidate();
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }
        if (!mScrollEnable) {
            mIsBeingDragged = false;
        }
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!mIsBeingDragged) {
            super.onTouchEvent(ev);
        }
        performClick();
        if (!mScrollEnable) {
            return true;
        }
        initVelocityTrackerIfNotExists();
        MotionEvent vtev = MotionEvent.obtain(ev);
        final int actionMasked = ev.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }
        vtev.offsetLocation(0, mNestedYOffset);
        switch(actionMasked) {
            case MotionEvent.ACTION_DOWN:
                {
                    if (getChildCount() == 0) {
                        return false;
                    }
                    if ((mIsBeingDragged = !mScroller.isFinished())) {
                        final ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                    if (!mScroller.isFinished()) {
                        mScroller.abortAnimation();
                    }
                    mLastMotionY = (int) ev.getY();
                    mActivePointerId = ev.getPointerId(0);
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    LogUtil.e("Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }
                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    mLastMotionY = y - mScrollOffset[1];
                    final int range = getScrollRange();
                    if (mScrollDelegate instanceof StackScrollDelegateImpl) {
                        mScrollDelegate.scrollViewTo(0, deltaY + mScrollDelegate.getViewScrollY());
                    } else {
                        if (overScrollBy(0, deltaY, 0, getViewScrollY(), 0, range, 0, 0, true)) {
                            mVelocityTracker.clear();
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                    if (getChildCount() > 0) {
                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                            fling(-initialVelocity);
                        } else {
                            if (mScroller.springBack(getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange())) {
                                postInvalidate();
                            }
                        }
                        mActivePointerId = INVALID_POINTER;
                    }
                }
                endDrag();
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0) {
                    if (mScroller.springBack(getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange())) {
                        postInvalidate();
                    }
                    mActivePointerId = INVALID_POINTER;
                }
                endDrag();
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                {
                    final int index = ev.getActionIndex();
                    mLastMotionY = (int) ev.getY(index);
                    mActivePointerId = ev.getPointerId(index);
                    break;
                }
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionY = (int) ev.getY(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    private int getScrollRange() {
        int scrollRange = 0;
        if (getChildCount() > 0) {
            scrollRange = Math.max(0, mTotalLength - mShowHeight);
        }
        return scrollRange;
    }

    @Override
    public boolean performClick() {
        return super.performClick();
    }

    @Override
    protected int computeVerticalScrollRange() {
        final int count = getChildCount();
        final int contentHeight = mShowHeight;
        if (count == 0) {
            return contentHeight;
        }
        int scrollRange = mTotalLength;
        final int scrollY = mScrollDelegate.getViewScrollY();
        final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
        if (scrollY < 0) {
            scrollRange -= scrollY;
        } else if (scrollY > overscrollBottom) {
            scrollRange += scrollY - overscrollBottom;
        }
        return scrollRange;
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (!mScroller.isFinished()) {
            final int oldX = mScrollDelegate.getViewScrollX();
            final int oldY = mScrollDelegate.getViewScrollY();
            mScrollDelegate.setViewScrollX(scrollX);
            mScrollDelegate.setViewScrollY(scrollY);
            onScrollChanged(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), oldX, oldY);
            if (clampedY) {
                mScroller.springBack(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, 0, 0, getScrollRange());
            }
        } else {
            super.scrollTo(scrollX, scrollY);
        }
    }

    @Override
    protected int computeVerticalScrollOffset() {
        return Math.max(0, super.computeVerticalScrollOffset());
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            mScrollDelegate.scrollViewTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }

    public void fling(int velocityY) {
        if (getChildCount() > 0) {
            int height = mShowHeight;
            int bottom = mTotalLength;
            mScroller.fling(mScrollDelegate.getViewScrollX(), mScrollDelegate.getViewScrollY(), 0, velocityY, 0, 0, 0, Math.max(0, bottom - height), 0, 0);
            postInvalidate();
        }
    }

    @Override
    public void scrollTo(int x, int y) {
        if (getChildCount() > 0) {
            x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), getWidth());
            y = clamp(y, mShowHeight, mTotalLength);
            if (x != mScrollDelegate.getViewScrollX() || y != mScrollDelegate.getViewScrollY()) {
                super.scrollTo(x, y);
            }
        }
    }

    @Override
    public int getViewScrollX() {
        return getScrollX();
    }

    @Override
    public void scrollViewTo(int x, int y) {
        scrollTo(x, y);
    }

    @Override
    public void setViewScrollY(int y) {
        setScrollY(y);
    }

    @Override
    public void setViewScrollX(int x) {
        setScrollX(x);
    }

    @Override
    public int getViewScrollY() {
        return getScrollY();
    }

    private void endDrag() {
        mIsBeingDragged = false;
        recycleVelocityTracker();
    }

    private static int clamp(int n, int my, int child) {
        if (my >= child || n < 0) {
            return 0;
        }
        if ((my + n) > child) {
            return child - my;
        }
        return n;
    }

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

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

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

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    public static clreplaced LayoutParams extends MarginLayoutParams {

        public int mHeaderHeight;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.CardStackView);
            mHeaderHeight = array.getDimensionPixelSize(R.styleable.CardStackView_stackHeaderHeight, -1);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

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

    public static abstract clreplaced Adapter<VH extends ViewHolder> {

        private final AdapterDataObservable mObservable = new AdapterDataObservable();

        VH createView(ViewGroup parent, int viewType) {
            VH holder = onCreateView(parent, viewType);
            holder.mItemViewType = viewType;
            return holder;
        }

        protected abstract VH onCreateView(ViewGroup parent, int viewType);

        public void bindViewHolder(VH holder, int position) {
            onBindViewHolder(holder, position);
        }

        protected abstract void onBindViewHolder(VH holder, int position);

        public abstract int gereplacedemCount();

        public int gereplacedemViewType(int position) {
            return 0;
        }

        public final void notifyDataSetChanged() {
            mObservable.notifyChanged();
        }

        public void registerObserver(AdapterDataObserver observer) {
            mObservable.registerObserver(observer);
        }
    }

    public static abstract clreplaced ViewHolder {

        public View itemView;

        int mItemViewType = INVALID_TYPE;

        int position;

        public ViewHolder(View view) {
            itemView = view;
        }

        public Context getContext() {
            return itemView.getContext();
        }

        public abstract void onItemExpand(boolean b);

        protected void onAnimationStateChange(int state, boolean willBeSelect) {
        }
    }

    public static clreplaced AdapterDataObservable extends Observable<AdapterDataObserver> {

        public boolean hasObservers() {
            return !mObservers.isEmpty();
        }

        public void notifyChanged() {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

    public static abstract clreplaced AdapterDataObserver {

        public void onChanged() {
        }
    }

    private clreplaced ViewDataObserver extends AdapterDataObserver {

        @Override
        public void onChanged() {
            refreshView();
        }
    }

    public int getSelectPosition() {
        return mSelectPosition;
    }

    public void setSelectPosition(int selectPosition) {
        mSelectPosition = selectPosition;
        mItemExpendListener.onItemExpend(mSelectPosition != DEFAULT_SELECT_POSITION);
    }

    public int getOverlapGaps() {
        return mOverlapGaps;
    }

    public void setOverlapGaps(int overlapGaps) {
        mOverlapGaps = overlapGaps;
    }

    public int getOverlapGapsCollapse() {
        return mOverlapGapsCollapse;
    }

    public void setOverlapGapsCollapse(int overlapGapsCollapse) {
        mOverlapGapsCollapse = overlapGapsCollapse;
    }

    public void setScrollEnable(boolean scrollEnable) {
        mScrollEnable = scrollEnable;
    }

    public int getShowHeight() {
        return mShowHeight;
    }

    public int getTotalLength() {
        return mTotalLength;
    }

    public void setDuration(int duration) {
        mDuration = duration;
    }

    public int getDuration() {
        if (mAnimatorAdapter != null)
            return mDuration;
        return 0;
    }

    public void setNumBottomShow(int numBottomShow) {
        mNumBottomShow = numBottomShow;
    }

    public int getNumBottomShow() {
        return mNumBottomShow;
    }

    public ScrollDelegate getScrollDelegate() {
        return mScrollDelegate;
    }

    public ItemExpendListener gereplacedemExpendListener() {
        return mItemExpendListener;
    }

    public void sereplacedemExpendListener(ItemExpendListener itemExpendListener) {
        mItemExpendListener = itemExpendListener;
    }

    public interface ItemExpendListener {

        void onItemExpend(boolean expend);
    }
}

19 Source : InnerRuler.java
with MIT License
from totond

/**
 * 内部尺子抽象类
 */
public abstract clreplaced InnerRuler extends View {

    public static final String TAG = "ruler";

    protected Context mContext;

    protected BooheeRuler mParent;

    // 加入放大倍数来防止精度丢失而导致无限绘制
    protected static final int SCALE_TO_PX_FACTOR = 100;

    // 惯性回滚最小偏移值,小于这个值就应该直接滑动到目的点
    protected static final int MIN_SCROLLER_DP = 1;

    protected float minScrollerPx = MIN_SCROLLER_DP;

    protected Paint mSmallScalePaint, mBigScalePaint, mTextPaint, mOutLinePaint;

    // 当前刻度值
    protected float mCurrentScale = 0;

    // 最大刻度数
    protected int mMaxLength = 0;

    // 长度、最小可滑动值、最大可滑动值
    protected int mLength, mMinPosition = 0, mMaxPosition = 0;

    // 控制滑动
    protected OverScroller mOverScroller;

    // 一格大刻度多少格小刻度
    protected int mCount = 10;

    // 提前刻画量
    protected int mDrawOffset;

    // 速度获取
    protected VelocityTracker mVelocityTracker;

    // 惯性最大最小速度
    protected int mMaximumVelocity, mMinimumVelocity;

    // 回调接口
    protected RulerCallback mRulerCallback;

    // 边界效果
    protected EdgeEffect mStartEdgeEffect, mEndEdgeEffect;

    // 边缘效应长度
    protected int mEdgeLength;

    public InnerRuler(Context context, BooheeRuler booheeRuler) {
        super(context);
        mParent = booheeRuler;
        init(context);
    }

    public void init(Context context) {
        mContext = context;
        mMaxLength = mParent.getMaxScale() - mParent.getMinScale();
        mCurrentScale = mParent.getCurrentScale();
        mCount = mParent.getCount();
        mDrawOffset = mCount * mParent.getInterval() / 2;
        minScrollerPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MIN_SCROLLER_DP, context.getResources().getDisplayMetrics());
        initPaints();
        mOverScroller = new OverScroller(mContext);
        // mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        // 配置速度
        mVelocityTracker = VelocityTracker.obtain();
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        initEdgeEffects();
        // 第一次进入,跳转到设定刻度
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                getViewTreeObserver().removeOnGlobalLayoutListener(this);
                goToScale(mCurrentScale);
            }
        });
        checkAPILevel();
    }

    // 初始化画笔
    private void initPaints() {
        mSmallScalePaint = new Paint();
        mSmallScalePaint.setStrokeWidth(mParent.getSmallScaleWidth());
        mSmallScalePaint.setColor(mParent.getScaleColor());
        mSmallScalePaint.setStrokeCap(Paint.Cap.ROUND);
        mBigScalePaint = new Paint();
        mBigScalePaint.setColor(mParent.getScaleColor());
        mBigScalePaint.setStrokeWidth(mParent.getBigScaleWidth());
        mBigScalePaint.setStrokeCap(Paint.Cap.ROUND);
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(mParent.getTextColor());
        mTextPaint.setTextSize(mParent.getTextSize());
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        // mTextPaint.setStrokeJoin(Paint.Join.ROUND);
        mOutLinePaint = new Paint();
        mOutLinePaint.setStrokeWidth(mParent.getOutLineWidth());
        mOutLinePaint.setAntiAlias(true);
        mOutLinePaint.setColor(mParent.getScaleColor());
    }

    // 初始化边缘效果
    public void initEdgeEffects() {
        if (mParent.canEdgeEffect()) {
            if (mStartEdgeEffect == null || mEndEdgeEffect == null) {
                mStartEdgeEffect = new EdgeEffect(mContext);
                mEndEdgeEffect = new EdgeEffect(mContext);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    mStartEdgeEffect.setColor(mParent.getEdgeColor());
                    mEndEdgeEffect.setColor(mParent.getEdgeColor());
                }
                mEdgeLength = mParent.getCursorHeight() + mParent.getInterval() * mParent.getCount();
            }
        }
    }

    // API小于18则关闭硬件加速,否则setAntiAlias()方法不生效
    private void checkAPILevel() {
        if (Build.VERSION.SDK_INT < 18) {
            setLayerType(LAYER_TYPE_NONE, null);
        }
    }

    @Override
    public void computeScroll() {
        if (mOverScroller.computeScrollOffset()) {
            scrollTo(mOverScroller.getCurrX(), mOverScroller.getCurrY());
            // 这是最后OverScroller的最后一次滑动,如果这次滑动完了mCurrentScale不是整数,则把尺子移动到最近的整数位置
            if (!mOverScroller.computeScrollOffset()) {
                int currentIntScale = Math.round(mCurrentScale);
                if ((Math.abs(mCurrentScale - currentIntScale) > 0.001f)) {
                    // Fling完进行一次检测回滚
                    scrollBackToCurrentScale(currentIntScale);
                }
            }
            postInvalidate();
        }
    }

    protected abstract void scrollBackToCurrentScale();

    protected abstract void scrollBackToCurrentScale(int currentIntScale);

    protected abstract void goToScale(float scale);

    public abstract void refreshSize();

    // 设置尺子当前刻度
    public void setCurrentScale(float currentScale) {
        this.mCurrentScale = currentScale;
        goToScale(mCurrentScale);
    }

    public void setRulerCallback(RulerCallback RulerCallback) {
        this.mRulerCallback = RulerCallback;
    }

    public float getCurrentScale() {
        return mCurrentScale;
    }
}

19 Source : ScrollAndScaleView.java
with Apache License 2.0
from tifezh

/**
 * 可以滑动和放大的view
 * Created by tian on 2016/5/3.
 */
public abstract clreplaced ScrollAndScaleView extends RelativeLayout implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener {

    protected int mScrollX = 0;

    protected GestureDetectorCompat mDetector;

    protected ScaleGestureDetector mScaleDetector;

    protected boolean isLongPress = false;

    private OverScroller mScroller;

    protected boolean touch = false;

    protected float mScaleX = 1;

    protected float mScaleXMax = 2f;

    protected float mScaleXMin = 0.5f;

    private boolean mMultipleTouch = false;

    private boolean mScrollEnable = true;

    private boolean mScaleEnable = true;

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

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

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

    private void init() {
        setWillNotDraw(false);
        mDetector = new GestureDetectorCompat(getContext(), this);
        mScaleDetector = new ScaleGestureDetector(getContext(), this);
        mScroller = new OverScroller(getContext());
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (!isLongPress && !isMultipleTouch()) {
            scrollBy(Math.round(distanceX), 0);
            return true;
        }
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        isLongPress = true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        if (!isTouch() && isScrollEnable()) {
            mScroller.fling(mScrollX, 0, Math.round(velocityX / mScaleX), 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            if (!isTouch()) {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            } else {
                mScroller.forceFinished(true);
            }
        }
    }

    @Override
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX - Math.round(x / mScaleX), 0);
    }

    @Override
    public void scrollTo(int x, int y) {
        if (!isScrollEnable()) {
            mScroller.forceFinished(true);
            return;
        }
        int oldX = mScrollX;
        mScrollX = x;
        if (mScrollX < getMinScrollX()) {
            mScrollX = getMinScrollX();
            onRightSide();
            mScroller.forceFinished(true);
        } else if (mScrollX > getMaxScrollX()) {
            mScrollX = getMaxScrollX();
            onLeftSide();
            mScroller.forceFinished(true);
        }
        onScrollChanged(mScrollX, 0, oldX, 0);
        invalidate();
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (!isScaleEnable()) {
            return false;
        }
        float oldScale = mScaleX;
        mScaleX *= detector.getScaleFactor();
        if (mScaleX < mScaleXMin) {
            mScaleX = mScaleXMin;
        } else if (mScaleX > mScaleXMax) {
            mScaleX = mScaleXMax;
        } else {
            onScaleChanged(mScaleX, oldScale);
        }
        return true;
    }

    protected void onScaleChanged(float scale, float oldScale) {
        invalidate();
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                touch = true;
                break;
            case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() == 1) {
                    // 长按之后移动
                    if (isLongPress) {
                        onLongPress(event);
                    }
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                isLongPress = false;
                touch = false;
                invalidate();
                break;
            case MotionEvent.ACTION_CANCEL:
                isLongPress = false;
                touch = false;
                invalidate();
                break;
        }
        mMultipleTouch = event.getPointerCount() > 1;
        this.mDetector.onTouchEvent(event);
        this.mScaleDetector.onTouchEvent(event);
        return true;
    }

    /**
     * 滑到了最左边
     */
    abstract public void onLeftSide();

    /**
     * 滑到了最右边
     */
    abstract public void onRightSide();

    /**
     * 是否在触摸中
     *
     * @return
     */
    public boolean isTouch() {
        return touch;
    }

    /**
     * 获取位移的最小值
     *
     * @return
     */
    public abstract int getMinScrollX();

    /**
     * 获取位移的最大值
     *
     * @return
     */
    public abstract int getMaxScrollX();

    /**
     * 设置ScrollX
     *
     * @param scrollX
     */
    public void setScrollX(int scrollX) {
        this.mScrollX = scrollX;
        scrollTo(scrollX, 0);
    }

    /**
     * 是否是多指触控
     * @return
     */
    public boolean isMultipleTouch() {
        return mMultipleTouch;
    }

    protected void checkAndFixScrollX() {
        if (mScrollX < getMinScrollX()) {
            mScrollX = getMinScrollX();
            mScroller.forceFinished(true);
        } else if (mScrollX > getMaxScrollX()) {
            mScrollX = getMaxScrollX();
            mScroller.forceFinished(true);
        }
    }

    public float getScaleXMax() {
        return mScaleXMax;
    }

    public float getScaleXMin() {
        return mScaleXMin;
    }

    public boolean isScrollEnable() {
        return mScrollEnable;
    }

    public boolean isScaleEnable() {
        return mScaleEnable;
    }

    /**
     * 设置缩放的最大值
     */
    public void setScaleXMax(float scaleXMax) {
        mScaleXMax = scaleXMax;
    }

    /**
     * 设置缩放的最小值
     */
    public void setScaleXMin(float scaleXMin) {
        mScaleXMin = scaleXMin;
    }

    /**
     * 设置是否可以滑动
     */
    public void setScrollEnable(boolean scrollEnable) {
        mScrollEnable = scrollEnable;
    }

    /**
     * 设置是否可以缩放
     */
    public void setScaleEnable(boolean scaleEnable) {
        mScaleEnable = scaleEnable;
    }

    @Override
    public float getScaleX() {
        return mScaleX;
    }
}

19 Source : RNScrollView.java
with MIT License
from TaumuLu

@TargetApi(11)
public clreplaced RNScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener {

    @Nullable
    private static Field sScrollerField;

    private static boolean sTriedToGetScrollerField = false;

    private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();

    @Nullable
    private final OverScroller mScroller;

    private final VelocityHelper mVelocityHelper = new VelocityHelper();

    @Nullable
    private Rect mClippingRect;

    private boolean mDoneFlinging;

    private boolean mDragging;

    private boolean mFlinging;

    private boolean mRemoveClippedSubviews;

    static private boolean mScrollEnabled = true;

    private boolean mSendMomentumEvents;

    @Nullable
    private FpsListener mFpsListener = null;

    @Nullable
    private String mScrollPerfTag;

    @Nullable
    private Drawable mEndBackground;

    private int mEndFillColor = Color.TRANSPARENT;

    private View mContentView;

    private ReactViewBackgroundManager mReactBackgroundManager;

    public RNScrollView(ReactContext context) {
        this(context, null);
    }

    public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) {
        super(context);
        mFpsListener = fpsListener;
        mReactBackgroundManager = new ReactViewBackgroundManager(this);
        mScroller = getOverScrollerFromParent();
        setOnHierarchyChangeListener(this);
        setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
    }

    @Nullable
    private OverScroller getOverScrollerFromParent() {
        OverScroller scroller;
        if (!sTriedToGetScrollerField) {
            sTriedToGetScrollerField = true;
            try {
                sScrollerField = ScrollView.clreplaced.getDeclaredField("mScroller");
                sScrollerField.setAccessible(true);
            } catch (NoSuchFieldException e) {
                Log.w(ReactConstants.TAG, "Failed to get mScroller field for ScrollView! " + "This app will exhibit the bounce-back scrolling bug :(");
            }
        }
        if (sScrollerField != null) {
            try {
                Object scrollerValue = sScrollerField.get(this);
                if (scrollerValue instanceof OverScroller) {
                    scroller = (OverScroller) scrollerValue;
                } else {
                    Log.w(ReactConstants.TAG, "Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " + "This app will exhibit the bounce-back scrolling bug :(");
                    scroller = null;
                }
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
            }
        } else {
            scroller = null;
        }
        return scroller;
    }

    public void setSendMomentumEvents(boolean sendMomentumEvents) {
        mSendMomentumEvents = sendMomentumEvents;
    }

    public void setScrollPerfTag(@Nullable String scrollPerfTag) {
        mScrollPerfTag = scrollPerfTag;
    }

    public void setScrollEnabled(boolean scrollEnabled) {
        mScrollEnabled = scrollEnabled;
    }

    public void flashScrollIndicators() {
        awakenScrollBars();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        MeasureSpecreplacedertions.replacedertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // Call with the present values in order to re-layout if necessary
        scrollTo(getScrollX(), getScrollY());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mRemoveClippedSubviews) {
            updateClippingRect();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mRemoveClippedSubviews) {
            updateClippingRect();
        }
    }

    @Override
    protected void onScrollChanged(int x, int y, int oldX, int oldY) {
        super.onScrollChanged(x, y, oldX, oldY);
        if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
            if (mRemoveClippedSubviews) {
                updateClippingRect();
            }
            if (mFlinging) {
                mDoneFlinging = false;
            }
            ReactScrollViewHelper.emitScrollEvent(this, mOnScrollDispatchHelper.getXFlingVelocity(), mOnScrollDispatchHelper.getYFlingVelocity());
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!mScrollEnabled) {
            return false;
        }
        if (super.onInterceptTouchEvent(ev)) {
            // 会将滑动时的触摸操作停止
            // NativeGestureUtil.notifyNativeGestureStarted(this, ev);
            ReactScrollViewHelper.emitScrollBeginDragEvent(this);
            mDragging = true;
            enableFpsListener();
            return true;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!mScrollEnabled) {
            return false;
        }
        mVelocityHelper.calculateVelocity(ev);
        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_UP && mDragging) {
            ReactScrollViewHelper.emitScrollEndDragEvent(this, mVelocityHelper.getXVelocity(), mVelocityHelper.getYVelocity());
            mDragging = false;
            disableFpsListener();
        }
        return super.onTouchEvent(ev);
    }

    @Override
    public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
        if (removeClippedSubviews && mClippingRect == null) {
            mClippingRect = new Rect();
        }
        mRemoveClippedSubviews = removeClippedSubviews;
        updateClippingRect();
    }

    @Override
    public boolean getRemoveClippedSubviews() {
        return mRemoveClippedSubviews;
    }

    @Override
    public void updateClippingRect() {
        if (!mRemoveClippedSubviews) {
            return;
        }
        replacedertions.replacedertNotNull(mClippingRect);
        ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
        View contentView = getChildAt(0);
        if (contentView instanceof ReactClippingViewGroup) {
            ((ReactClippingViewGroup) contentView).updateClippingRect();
        }
    }

    @Override
    public void getClippingRect(Rect outClippingRect) {
        outClippingRect.set(replacedertions.replacedertNotNull(mClippingRect));
    }

    @Override
    public void fling(int velocityY) {
        if (mScroller != null) {
            // FB SCROLLVIEW CHANGE
            // We provide our own version of fling that uses a different call to the standard OverScroller
            // which takes into account the possibility of adding new content while the ScrollView is
            // animating. Because we give essentially no max Y for the fling, the fling will continue as long
            // as there is content. See #onOverScrolled() to see the second part of this change which properly
            // aborts the scroller animation when we get to the bottom of the ScrollView content.
            int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
            mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Integer.MAX_VALUE, 0, scrollWindowHeight / 2);
            ViewCompat.postInvalidateOnAnimation(this);
        // END FB SCROLLVIEW CHANGE
        } else {
            super.fling(velocityY);
        }
        if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
            mFlinging = true;
            enableFpsListener();
            ReactScrollViewHelper.emitScrollMomentumBeginEvent(this, 0, velocityY);
            Runnable r = new Runnable() {

                @Override
                public void run() {
                    if (mDoneFlinging) {
                        mFlinging = false;
                        disableFpsListener();
                        ReactScrollViewHelper.emitScrollMomentumEndEvent(RNScrollView.this);
                    } else {
                        mDoneFlinging = true;
                        ViewCompat.postOnAnimationDelayed(RNScrollView.this, this, ReactScrollViewHelper.MOMENTUM_DELAY);
                    }
                }
            };
            ViewCompat.postOnAnimationDelayed(this, r, ReactScrollViewHelper.MOMENTUM_DELAY);
        }
    }

    private void enableFpsListener() {
        if (isScrollPerfLoggingEnabled()) {
            replacedertions.replacedertNotNull(mFpsListener);
            replacedertions.replacedertNotNull(mScrollPerfTag);
            mFpsListener.enable(mScrollPerfTag);
        }
    }

    private void disableFpsListener() {
        if (isScrollPerfLoggingEnabled()) {
            replacedertions.replacedertNotNull(mFpsListener);
            replacedertions.replacedertNotNull(mScrollPerfTag);
            mFpsListener.disable(mScrollPerfTag);
        }
    }

    private boolean isScrollPerfLoggingEnabled() {
        return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty();
    }

    private int getMaxScrollY() {
        int contentHeight = mContentView.getHeight();
        int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        return Math.max(0, contentHeight - viewportHeight);
    }

    @Override
    public void draw(Canvas canvas) {
        if (mEndFillColor != Color.TRANSPARENT) {
            final View content = getChildAt(0);
            if (mEndBackground != null && content != null && content.getBottom() < getHeight()) {
                mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight());
                mEndBackground.draw(canvas);
            }
        }
        super.draw(canvas);
    }

    public void setEndFillColor(int color) {
        if (color != mEndFillColor) {
            mEndFillColor = color;
            mEndBackground = new ColorDrawable(mEndFillColor);
        }
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (mScroller != null) {
            // FB SCROLLVIEW CHANGE
            // This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
            // more information.
            if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
                int scrollRange = getMaxScrollY();
                if (scrollY >= scrollRange) {
                    mScroller.abortAnimation();
                    scrollY = scrollRange;
                }
            }
        // END FB SCROLLVIEW CHANGE
        }
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        mContentView = child;
        mContentView.addOnLayoutChangeListener(this);
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {
        mContentView.removeOnLayoutChangeListener(this);
        mContentView = null;
    }

    /**
     * Called when a mContentView's layout has changed. Fixes the scroll position if it's too large
     * after the content resizes. Without this, the user would see a blank ScrollView when the scroll
     * position is larger than the ScrollView's max scroll position after the content shrinks.
     */
    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        if (mContentView == null) {
            return;
        }
        int currentScrollY = getScrollY();
        int maxScrollY = getMaxScrollY();
        if (currentScrollY > maxScrollY) {
            scrollTo(getScrollX(), maxScrollY);
        }
    }

    @Override
    public void setBackgroundColor(int color) {
        mReactBackgroundManager.setBackgroundColor(color);
    }

    public void setBorderWidth(int position, float width) {
        mReactBackgroundManager.setBorderWidth(position, width);
    }

    public void setBorderColor(int position, float color, float alpha) {
        mReactBackgroundManager.setBorderColor(position, color, alpha);
    }

    public void setBorderRadius(float borderRadius) {
        mReactBackgroundManager.setBorderRadius(borderRadius);
    }

    public void setBorderRadius(float borderRadius, int position) {
        mReactBackgroundManager.setBorderRadius(borderRadius, position);
    }

    public void setBorderStyle(@Nullable String style) {
        mReactBackgroundManager.setBorderStyle(style);
    }
}

19 Source : RNScrollView.java
with MIT License
from TaumuLu

public clreplaced RNScrollView extends ScrollView implements ReactClippingViewGroup, ViewGroup.OnHierarchyChangeListener, View.OnLayoutChangeListener {

    private static Field sScrollerField;

    private static boolean sTriedToGetScrollerField = false;

    private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();

    private final OverScroller mScroller;

    private final VelocityHelper mVelocityHelper = new VelocityHelper();

    @Nullable
    private Rect mClippingRect;

    private boolean mDoneFlinging;

    private boolean mDragging;

    private boolean mFlinging;

    private boolean mRemoveClippedSubviews;

    static private boolean mScrollEnabled = true;

    private boolean mSendMomentumEvents;

    @Nullable
    private FpsListener mFpsListener = null;

    @Nullable
    private String mScrollPerfTag;

    @Nullable
    private Drawable mEndBackground;

    private int mEndFillColor = Color.TRANSPARENT;

    private View mContentView;

    @Nullable
    private ReactViewBackgroundDrawable mReactBackgroundDrawable;

    public RNScrollView(ReactContext context) {
        this(context, null);
    }

    public RNScrollView(ReactContext context, @Nullable FpsListener fpsListener) {
        super(context);
        mFpsListener = fpsListener;
        if (!sTriedToGetScrollerField) {
            sTriedToGetScrollerField = true;
            try {
                sScrollerField = ScrollView.clreplaced.getDeclaredField("mScroller");
                sScrollerField.setAccessible(true);
            } catch (NoSuchFieldException e) {
                Log.w(ReactConstants.TAG, "Failed to get mScroller field for ScrollView! " + "This app will exhibit the bounce-back scrolling bug :(");
            }
        }
        if (sScrollerField != null) {
            try {
                Object scroller = sScrollerField.get(this);
                if (scroller instanceof OverScroller) {
                    mScroller = (OverScroller) scroller;
                } else {
                    Log.w(ReactConstants.TAG, "Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! " + "This app will exhibit the bounce-back scrolling bug :(");
                    mScroller = null;
                }
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
            }
        } else {
            mScroller = null;
        }
        setOnHierarchyChangeListener(this);
        setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
    }

    public void setSendMomentumEvents(boolean sendMomentumEvents) {
        mSendMomentumEvents = sendMomentumEvents;
    }

    public void setScrollPerfTag(String scrollPerfTag) {
        mScrollPerfTag = scrollPerfTag;
    }

    public void setScrollEnabled(boolean scrollEnabled) {
        mScrollEnabled = scrollEnabled;
    // Log.i("setScrollEnabled", "value:" + scrollEnabled + " setValue:" + mScrollEnabled);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        MeasureSpecreplacedertions.replacedertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // Call with the present values in order to re-layout if necessary
        scrollTo(getScrollX(), getScrollY());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mRemoveClippedSubviews) {
            updateClippingRect();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mRemoveClippedSubviews) {
            updateClippingRect();
        }
    }

    @Override
    protected void onScrollChanged(int x, int y, int oldX, int oldY) {
        super.onScrollChanged(x, y, oldX, oldY);
        if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
            if (mRemoveClippedSubviews) {
                updateClippingRect();
            }
            if (mFlinging) {
                mDoneFlinging = false;
            }
            ReactScrollViewHelper.emitScrollEvent(this, mOnScrollDispatchHelper.getXFlingVelocity(), mOnScrollDispatchHelper.getYFlingVelocity());
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // Log.i("onInterceptTouchEvent", "value:" + mScrollEnabled);
        if (!mScrollEnabled) {
            return false;
        }
        if (super.onInterceptTouchEvent(ev)) {
            // 会将滑动时的触摸操作停止
            // NativeGestureUtil.notifyNativeGestureStarted(this, ev);
            ReactScrollViewHelper.emitScrollBeginDragEvent(this);
            mDragging = true;
            enableFpsListener();
            return true;
        }
        return false;
    }

    // @Override
    // public boolean dispatchTouchEvent(MotionEvent ev) {
    // Log.i("dispatchTouchEvent", "value:" + mScrollEnabled);
    // if (!mScrollEnabled) {
    // return false;
    // }
    // return super.dispatchTouchEvent(ev);
    // }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Log.i("onTouchEvent", "value:" + mScrollEnabled);
        if (!mScrollEnabled) {
            return false;
        }
        mVelocityHelper.calculateVelocity(ev);
        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_UP && mDragging) {
            ReactScrollViewHelper.emitScrollEndDragEvent(this, mVelocityHelper.getXVelocity(), mVelocityHelper.getYVelocity());
            mDragging = false;
            disableFpsListener();
        }
        return super.onTouchEvent(ev);
    }

    @Override
    public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
        if (removeClippedSubviews && mClippingRect == null) {
            mClippingRect = new Rect();
        }
        mRemoveClippedSubviews = removeClippedSubviews;
        updateClippingRect();
    }

    @Override
    public boolean getRemoveClippedSubviews() {
        return mRemoveClippedSubviews;
    }

    @Override
    public void updateClippingRect() {
        if (!mRemoveClippedSubviews) {
            return;
        }
        replacedertions.replacedertNotNull(mClippingRect);
        ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect);
        View contentView = getChildAt(0);
        if (contentView instanceof ReactClippingViewGroup) {
            ((ReactClippingViewGroup) contentView).updateClippingRect();
        }
    }

    @Override
    public void getClippingRect(Rect outClippingRect) {
        outClippingRect.set(replacedertions.replacedertNotNull(mClippingRect));
    }

    @Override
    public void fling(int velocityY) {
        if (mScroller != null) {
            // FB SCROLLVIEW CHANGE
            // We provide our own version of fling that uses a different call to the standard OverScroller
            // which takes into account the possibility of adding new content while the ScrollView is
            // animating. Because we give essentially no max Y for the fling, the fling will continue as long
            // as there is content. See #onOverScrolled() to see the second part of this change which properly
            // aborts the scroller animation when we get to the bottom of the ScrollView content.
            int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
            mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, Integer.MAX_VALUE, 0, scrollWindowHeight / 2);
            postInvalidateOnAnimation();
        // END FB SCROLLVIEW CHANGE
        } else {
            super.fling(velocityY);
        }
        if (mSendMomentumEvents || isScrollPerfLoggingEnabled()) {
            mFlinging = true;
            enableFpsListener();
            ReactScrollViewHelper.emitScrollMomentumBeginEvent(this);
            Runnable r = new Runnable() {

                @Override
                public void run() {
                    if (mDoneFlinging) {
                        mFlinging = false;
                        disableFpsListener();
                        ReactScrollViewHelper.emitScrollMomentumEndEvent(RNScrollView.this);
                    } else {
                        mDoneFlinging = true;
                        RNScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
                    }
                }
            };
            postOnAnimationDelayed(r, ReactScrollViewHelper.MOMENTUM_DELAY);
        }
    }

    private void enableFpsListener() {
        if (isScrollPerfLoggingEnabled()) {
            replacedertions.replacedertNotNull(mFpsListener);
            replacedertions.replacedertNotNull(mScrollPerfTag);
            mFpsListener.enable(mScrollPerfTag);
        }
    }

    private void disableFpsListener() {
        if (isScrollPerfLoggingEnabled()) {
            replacedertions.replacedertNotNull(mFpsListener);
            replacedertions.replacedertNotNull(mScrollPerfTag);
            mFpsListener.disable(mScrollPerfTag);
        }
    }

    private boolean isScrollPerfLoggingEnabled() {
        return mFpsListener != null && mScrollPerfTag != null && !mScrollPerfTag.isEmpty();
    }

    private int getMaxScrollY() {
        int contentHeight = mContentView.getHeight();
        int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        return Math.max(0, contentHeight - viewportHeight);
    }

    @Override
    public void draw(Canvas canvas) {
        if (mEndFillColor != Color.TRANSPARENT) {
            final View content = getChildAt(0);
            if (mEndBackground != null && content != null && content.getBottom() < getHeight()) {
                mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight());
                mEndBackground.draw(canvas);
            }
        }
        super.draw(canvas);
    }

    public void setEndFillColor(int color) {
        if (color != mEndFillColor) {
            mEndFillColor = color;
            mEndBackground = new ColorDrawable(mEndFillColor);
        }
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (mScroller != null) {
            // FB SCROLLVIEW CHANGE
            // This is part two of the reimplementation of fling to fix the bounce-back bug. See #fling() for
            // more information.
            if (!mScroller.isFinished() && mScroller.getCurrY() != mScroller.getFinalY()) {
                int scrollRange = getMaxScrollY();
                if (scrollY >= scrollRange) {
                    mScroller.abortAnimation();
                    scrollY = scrollRange;
                }
            }
        // END FB SCROLLVIEW CHANGE
        }
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        mContentView = child;
        mContentView.addOnLayoutChangeListener(this);
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {
        mContentView.removeOnLayoutChangeListener(this);
        mContentView = null;
    }

    /**
     * Called when a mContentView's layout has changed. Fixes the scroll position if it's too large
     * after the content resizes. Without this, the user would see a blank ScrollView when the scroll
     * position is larger than the ScrollView's max scroll position after the content shrinks.
     */
    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        if (mContentView == null) {
            return;
        }
        int currentScrollY = getScrollY();
        int maxScrollY = getMaxScrollY();
        if (currentScrollY > maxScrollY) {
            scrollTo(getScrollX(), maxScrollY);
        }
    }

    public void setBackgroundColor(int color) {
        if (color == Color.TRANSPARENT && mReactBackgroundDrawable == null) {
        // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background
        } else {
            getOrCreateReactViewBackground().setColor(color);
        }
    }

    public void setBorderWidth(int position, float width) {
        getOrCreateReactViewBackground().setBorderWidth(position, width);
    }

    public void setBorderColor(int position, float color, float alpha) {
        getOrCreateReactViewBackground().setBorderColor(position, color, alpha);
    }

    public void setBorderRadius(float borderRadius) {
        getOrCreateReactViewBackground().setRadius(borderRadius);
    }

    public void setBorderRadius(float borderRadius, int position) {
        getOrCreateReactViewBackground().setRadius(borderRadius, position);
    }

    public void setBorderStyle(@Nullable String style) {
        getOrCreateReactViewBackground().setBorderStyle(style);
    }

    private ReactViewBackgroundDrawable getOrCreateReactViewBackground() {
        if (mReactBackgroundDrawable == null) {
            mReactBackgroundDrawable = new ReactViewBackgroundDrawable();
            Drawable backgroundDrawable = getBackground();
            // required so that drawable callback is cleared before we add the
            super.setBackground(null);
            // drawable back as a part of LayerDrawable
            if (backgroundDrawable == null) {
                super.setBackground(mReactBackgroundDrawable);
            } else {
                LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { mReactBackgroundDrawable, backgroundDrawable });
                super.setBackground(layerDrawable);
            }
        }
        return mReactBackgroundDrawable;
    }
}

19 Source : SmartDragLayout.java
with Apache License 2.0
from TanZhiL

/**
 * Description: 智能的拖拽布局,优先滚动整体,整体滚到头,则滚动内部能滚动的View
 * Create by dance, at 2018/12/23
 */
public clreplaced SmartDragLayout extends FrameLayout implements NestedScrollingParent {

    private static final String TAG = "SmartDragLayout";

    private View child;

    OverScroller scroller;

    VelocityTracker tracker;

    ShadowBgAnimator bgAnimator = new ShadowBgAnimator();

    // 是否启用手势拖拽
    boolean enableDrag = true;

    boolean dismissOnTouchOutside = true;

    boolean hreplacedhadowBg = true;

    boolean isUserClose = false;

    // 是否开启三段拖拽
    boolean isThreeDrag = false;

    LayoutStatus status = LayoutStatus.Close;

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

    public SmartDragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SmartDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (enableDrag) {
            scroller = new OverScroller(context);
        }
    }

    int maxY;

    int minY;

    @Override
    public void onViewAdded(View c) {
        super.onViewAdded(c);
        child = c;
    }

    int lastHeight;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        maxY = child.getMeasuredHeight();
        minY = 0;
        int l = getMeasuredWidth() / 2 - child.getMeasuredWidth() / 2;
        if (enableDrag) {
            // horizontal center
            child.layout(l, getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight() + maxY);
            if (status == LayoutStatus.Open) {
                if (isThreeDrag) {
                    // 通过scroll上移
                    scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
                } else {
                    // 通过scroll上移
                    scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
                }
            }
        } else {
            // like bottom gravity
            child.layout(l, getMeasuredHeight() - child.getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight());
        }
        lastHeight = maxY;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        isUserClose = true;
        return super.dispatchTouchEvent(ev);
    }

    float touchX, touchY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (enableDrag && scroller.computeScrollOffset()) {
            touchX = 0;
            touchY = 0;
            return true;
        }
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (enableDrag) {
                    if (tracker != null)
                        tracker.clear();
                    tracker = VelocityTracker.obtain();
                }
                touchX = event.getX();
                touchY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (enableDrag && tracker != null) {
                    tracker.addMovement(event);
                    tracker.computeCurrentVelocity(1000);
                    int dy = (int) (event.getY() - touchY);
                    scrollTo(getScrollX(), getScrollY() - dy);
                    touchY = event.getY();
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // click in child rect
                Rect rect = new Rect();
                child.getGlobalVisibleRect(rect);
                if (!XPopupUtils.isInRect(event.getRawX(), event.getRawY(), rect) && dismissOnTouchOutside) {
                    float distance = (float) Math.sqrt(Math.pow(event.getX() - touchX, 2) + Math.pow(event.getY() - touchY, 2));
                    if (distance < ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
                        performClick();
                    }
                } else {
                }
                if (enableDrag && tracker != null) {
                    float yVelocity = tracker.getYVelocity();
                    if (yVelocity > 1500 && !isThreeDrag) {
                        close();
                    } else {
                        finishScroll();
                    }
                    // tracker.recycle();
                    tracker = null;
                }
                break;
        }
        return true;
    }

    private void finishScroll() {
        if (enableDrag) {
            int threshold = isScrollUp ? (maxY - minY) / 3 : (maxY - minY) * 2 / 3;
            int dy = (getScrollY() > threshold ? maxY : minY) - getScrollY();
            if (isThreeDrag) {
                int per = maxY / 3;
                if (getScrollY() > per * 2.5f) {
                    dy = maxY - getScrollY();
                } else if (getScrollY() <= per * 2.5f && getScrollY() > per * 1.5f) {
                    dy = per * 2 - getScrollY();
                } else if (getScrollY() > per) {
                    dy = per - getScrollY();
                } else {
                    dy = minY - getScrollY();
                }
            }
            scroller.startScroll(getScrollX(), getScrollY(), 0, dy, XPopup.getAnimationDuration());
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    boolean isScrollUp;

    @Override
    public void scrollTo(int x, int y) {
        if (y > maxY)
            y = maxY;
        if (y < minY)
            y = minY;
        float fraction = (y - minY) * 1f / (maxY - minY);
        isScrollUp = y > getScrollY();
        if (hreplacedhadowBg)
            setBackgroundColor(bgAnimator.calculateBgColor(fraction));
        if (listener != null) {
            if (isUserClose && fraction == 0f && status != LayoutStatus.Close) {
                status = LayoutStatus.Close;
                listener.onClose();
            } else if (fraction == 1f && status != LayoutStatus.Open) {
                status = LayoutStatus.Open;
                listener.onOpen();
            }
            listener.onDrag(y, fraction, isScrollUp);
        }
        super.scrollTo(x, y);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isScrollUp = false;
        isUserClose = false;
        setTranslationY(0);
    }

    public void open() {
        post(new Runnable() {

            @Override
            public void run() {
                int dy = maxY - getScrollY();
                smoothScroll(enableDrag && isThreeDrag ? dy / 3 : dy, true);
                status = LayoutStatus.Opening;
            }
        });
    }

    public void close() {
        isUserClose = true;
        post(new Runnable() {

            @Override
            public void run() {
                scroller.abortAnimation();
                smoothScroll(minY - getScrollY(), false);
                status = LayoutStatus.Closing;
            }
        });
    }

    public void smoothScroll(final int dy, final boolean isOpen) {
        post(new Runnable() {

            @Override
            public void run() {
                scroller.startScroll(getScrollX(), getScrollY(), 0, dy, (int) (isOpen ? XPopup.getAnimationDuration() : XPopup.getAnimationDuration() * 0.8f));
                ViewCompat.postInvalidateOnAnimation(SmartDragLayout.this);
            }
        });
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && enableDrag;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        // 必须要取消,否则会导致滑动初次延迟
        scroller.abortAnimation();
    }

    @Override
    public void onStopNestedScroll(View target) {
        finishScroll();
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        scrollTo(getScrollX(), getScrollY() + dyUnconsumed);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0) {
            // scroll up
            int newY = getScrollY() + dy;
            if (newY < maxY) {
                // dy不一定能消费完
                consumed[1] = dy;
            }
            scrollTo(getScrollX(), newY);
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        boolean isDragging = getScrollY() > minY && getScrollY() < maxY;
        if (isDragging && velocityY < -1500 && !isThreeDrag) {
            close();
        }
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    public void isThreeDrag(boolean isThreeDrag) {
        this.isThreeDrag = isThreeDrag;
    }

    public void enableDrag(boolean enableDrag) {
        this.enableDrag = enableDrag;
    }

    public void dismissOnTouchOutside(boolean dismissOnTouchOutside) {
        this.dismissOnTouchOutside = dismissOnTouchOutside;
    }

    public void hreplacedhadowBg(boolean hreplacedhadowBg) {
        this.hreplacedhadowBg = hreplacedhadowBg;
    }

    private OnCloseListener listener;

    public void setOnCloseListener(OnCloseListener listener) {
        this.listener = listener;
    }

    public interface OnCloseListener {

        void onClose();

        void onDrag(int y, float percent, boolean isScrollUp);

        void onOpen();
    }
}

19 Source : GingerScroller.java
with Apache License 2.0
from SwiftyWang

@TargetApi(9)
public clreplaced GingerScroller extends ScrollerProxy {

    protected final OverScroller mScroller;

    private boolean mFirstScroll = false;

    public GingerScroller(Context context) {
        mScroller = new OverScroller(context);
    }

    @Override
    public boolean computeScrollOffset() {
        // Workaround for first scroll returning 0 for the direction of the edge it hits.
        // Simply recompute values.
        if (mFirstScroll) {
            mScroller.computeScrollOffset();
            mFirstScroll = false;
        }
        return mScroller.computeScrollOffset();
    }

    @Override
    public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
    }

    @Override
    public void forceFinished(boolean finished) {
        mScroller.forceFinished(finished);
    }

    @Override
    public boolean isFinished() {
        return mScroller.isFinished();
    }

    @Override
    public int getCurrX() {
        return mScroller.getCurrX();
    }

    @Override
    public int getCurrY() {
        return mScroller.getCurrY();
    }
}

19 Source : SystemGesturesPointerEventListener.java
with Apache License 2.0
from starscryer

/*
 * Listens for system-wide input gestures, firing callbacks when detected.
 * @hide
 */
public clreplaced SystemGesturesPointerEventListener implements PointerEventListener {

    private static final String TAG = "SystemGestures";

    private static final boolean DEBUG = false;

    private static final long SWIPE_TIMEOUT_MS = 500;

    // max per input system
    private static final int MAX_TRACKED_POINTERS = 32;

    private static final int UNTRACKED_POINTER = -1;

    private static final int MAX_FLING_TIME_MILLIS = 5000;

    private static final int SWIPE_NONE = 0;

    private static final int SWIPE_FROM_TOP = 1;

    private static final int SWIPE_FROM_BOTTOM = 2;

    private static final int SWIPE_FROM_RIGHT = 3;

    private static final int SWIPE_FROM_LEFT = 4;

    private final Context mContext;

    private final int mSwipeStartThreshold;

    private final int mSwipeDistanceThreshold;

    private final Callbacks mCallbacks;

    private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];

    private final float[] mDownX = new float[MAX_TRACKED_POINTERS];

    private final float[] mDownY = new float[MAX_TRACKED_POINTERS];

    private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];

    private GestureDetector mGestureDetector;

    private OverScroller mOverscroller;

    private int screenHeight;

    private int screenWidth;

    private int mDownPointers;

    private boolean mSwipeFireable;

    private boolean mDebugFireable;

    private boolean mMouseHoveringAtEdge;

    private long mLastFlingTime;

    public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
        mContext = context;
        mCallbacks = checkNull("callbacks", callbacks);
        mSwipeStartThreshold = 100;
        // = checkNull("context", context).getResources()
        // .getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
        mSwipeDistanceThreshold = mSwipeStartThreshold;
        if (DEBUG)
            Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
    }

    private static <T> T checkNull(String name, T arg) {
        if (arg == null) {
            throw new IllegalArgumentException(name + " must not be null");
        }
        return arg;
    }

    public void systemReady() {
        Handler h = new Handler(Looper.myLooper());
        mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
        mOverscroller = new OverScroller(mContext);
    }

    @Override
    public void onPointerEvent(MotionEvent event) {
        if (mGestureDetector != null && event.isTouchEvent()) {
            mGestureDetector.onTouchEvent(event);
        }
        switch(event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mSwipeFireable = true;
                mDebugFireable = true;
                mDownPointers = 0;
                captureDown(event, 0);
                if (mMouseHoveringAtEdge) {
                    mMouseHoveringAtEdge = false;
                    mCallbacks.onMouseLeaveFromEdge();
                }
                mCallbacks.onDown();
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                captureDown(event, event.getActionIndex());
                if (mDebugFireable) {
                    mDebugFireable = event.getPointerCount() < 5;
                    if (!mDebugFireable) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing debug");
                        mCallbacks.onDebug();
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mSwipeFireable) {
                    final int swipe = detectSwipe(event);
                    mSwipeFireable = swipe == SWIPE_NONE;
                    if (swipe == SWIPE_FROM_TOP) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromTop");
                        mCallbacks.onSwipeFromTop();
                    } else if (swipe == SWIPE_FROM_BOTTOM) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromBottom");
                        int pointerId = event.getPointerId(0);
                        final int i = findIndex(pointerId);
                        int x = (int) mDownX[i];
                        int y = (int) mDownY[i];
                        mCallbacks.onSwipeFromBottom(x, y);
                    } else if (swipe == SWIPE_FROM_RIGHT) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromRight");
                        mCallbacks.onSwipeFromRight();
                    } else if (swipe == SWIPE_FROM_LEFT) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromLeft");
                        mCallbacks.onSwipeFromLeft();
                    }
                }
                break;
            case MotionEvent.ACTION_HOVER_MOVE:
                if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
                    if (!mMouseHoveringAtEdge && event.getY() == 0) {
                        mCallbacks.onMouseHoverAtTop();
                        mMouseHoveringAtEdge = true;
                    } else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
                        mCallbacks.onMouseHoverAtBottom();
                        mMouseHoveringAtEdge = true;
                    } else if (mMouseHoveringAtEdge && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
                        mCallbacks.onMouseLeaveFromEdge();
                        mMouseHoveringAtEdge = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeFireable = false;
                mDebugFireable = false;
                mCallbacks.onUpOrCancel();
                break;
            default:
                if (DEBUG)
                    Slog.d(TAG, "Ignoring " + event);
        }
    }

    private void captureDown(MotionEvent event, int pointerIndex) {
        final int pointerId = event.getPointerId(pointerIndex);
        final int i = findIndex(pointerId);
        if (DEBUG)
            Slog.d(TAG, "pointer " + pointerId + " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
        if (i != UNTRACKED_POINTER) {
            mDownX[i] = event.getX(pointerIndex);
            mDownY[i] = event.getY(pointerIndex);
            mDownTime[i] = event.getEventTime();
            if (DEBUG)
                Slog.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i]);
        }
    }

    private int findIndex(int pointerId) {
        for (int i = 0; i < mDownPointers; i++) {
            if (mDownPointerId[i] == pointerId) {
                return i;
            }
        }
        if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
            return UNTRACKED_POINTER;
        }
        mDownPointerId[mDownPointers++] = pointerId;
        return mDownPointers - 1;
    }

    private int detectSwipe(MotionEvent move) {
        final int historySize = move.getHistorySize();
        final int pointerCount = move.getPointerCount();
        for (int p = 0; p < pointerCount; p++) {
            final int pointerId = move.getPointerId(p);
            final int i = findIndex(pointerId);
            if (i != UNTRACKED_POINTER) {
                for (int h = 0; h < historySize; h++) {
                    final long time = move.getHistoricalEventTime(h);
                    final float x = move.getHistoricalX(p, h);
                    final float y = move.getHistoricalY(p, h);
                    final int swipe = detectSwipe(i, time, x, y);
                    if (swipe != SWIPE_NONE) {
                        return swipe;
                    }
                }
                final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
                if (swipe != SWIPE_NONE) {
                    return swipe;
                }
            }
        }
        return SWIPE_NONE;
    }

    private int detectSwipe(int i, long time, float x, float y) {
        final float fromX = mDownX[i];
        final float fromY = mDownY[i];
        final long elapsed = time - mDownTime[i];
        if (DEBUG)
            Slog.d(TAG, "pointer " + mDownPointerId[i] + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
        if (fromY <= mSwipeStartThreshold && y > fromY + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_TOP;
        }
        if (fromY >= screenHeight - mSwipeStartThreshold && y < fromY - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_BOTTOM;
        }
        if (fromX >= screenWidth - mSwipeStartThreshold && x < fromX - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_RIGHT;
        }
        if (fromX <= mSwipeStartThreshold && x > fromX + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_LEFT;
        }
        return SWIPE_NONE;
    }

    private final clreplaced FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            if (!mOverscroller.isFinished()) {
                mOverscroller.forceFinished(true);
            }
            return true;
        }

        @Override
        public boolean onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY) {
            mOverscroller.computeScrollOffset();
            long now = SystemClock.uptimeMillis();
            if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
                mOverscroller.forceFinished(true);
            }
            mOverscroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            int duration = mOverscroller.getDuration();
            if (duration > MAX_FLING_TIME_MILLIS) {
                duration = MAX_FLING_TIME_MILLIS;
            }
            mLastFlingTime = now;
            mCallbacks.onFling(duration);
            return true;
        }
    }

    interface Callbacks {

        void onSwipeFromTop();

        void onSwipeFromBottom(int x, int y);

        void onSwipeFromRight();

        void onSwipeFromLeft();

        void onFling(int durationMs);

        void onDown();

        void onUpOrCancel();

        void onMouseHoverAtTop();

        void onMouseHoverAtBottom();

        void onMouseLeaveFromEdge();

        void onDebug();
    }
}

19 Source : CalendarView.java
with MIT License
from snollidea

public clreplaced CalendarView extends View {

    /**
     * Velocity threshold for smooth scroll to another month.
     */
    private static final int VELOCITY_THRESHOLD = 2000;

    /**
     * Ratio between different values
     */
    private static final float RATIO_ROW_HEIGHT_WIDTH = 0.098f;

    private static final float RATIO_WIDTH_PADDING_X = 12.0f;

    private static final float RATIO_WIDTH_PADDING_Y = 15.0f;

    private static final float RATIO_WIDTH_TEXT_HEIGHT = 36.0f;

    private static final float RATIO_WIDTH_CIRCLE_RADIUS = 27.0f;

    private static final float RATIO_DURATION_DISTANCE = 0.75f;

    private static final int DEFAULT_DAYS_IN_WEEK = 7;

    /**
     * Duration of resize animation in ms
     */
    private static final int RESIZE_ANIMATION_DURATION = 200;

    /**
     * Used for shifting drawing items
     */
    private int mOffset;

    /**
     * mPaddingX used for left and right padding
     * mPaddingY used for top and bottom padding
     */
    private float mPaddingX;

    private float mPaddingY;

    /**
     * Space between objects in calendar for X and Y axis
     */
    private float mBetweenX;

    private float mBetweenY;

    /**
     * Used for dynamic resize from different parts of code
     */
    private int mViewHeight;

    private int mRowsCount;

    /**
     * Used for measuring text with Paint.getTextBounds method
     */
    private Rect mTextRect = new Rect();

    /**
     * Names of days of the week
     */
    private String[] mWeekDayNames;

    /**
     * Is view in resize animation
     */
    private boolean mIsResize;

    /**
     * First day of the week is; e.g., SUNDAY in the U.S., MONDAY in France.
     */
    private int mFirstDayOfWeek;

    // XML Attributes
    private int mBackgroundColor;

    private int mTextColor;

    private int mTextInsideCircleColor;

    private int mWeekDaysNamesColor;

    private int mCurrentDayCircleColor;

    private int mSelectedDayCircleColor;

    // Listeners
    private OnDateSelectedListener mOnDateSelectedListener;

    private OnMonthChangedListener mOnMonthChangedListener;

    // Interactive
    private GestureDetectorCompat mDetector;

    private VelocityTracker mVelocityTracker;

    private OverScroller mScroller;

    // Drawing
    private Paint mTextInsideCirclePaint;

    private Paint mTextPaint;

    private float mTextHeight;

    private Paint mSelectedDayCirclePaint;

    private Paint mCurrentDayCirclePaint;

    private float mCircleRadius;

    private Paint mEventCirclePaint;

    private Paint mBackgroundPaint;

    private Paint mWeekDaysNamesTextPaint;

    private float mEventCircleRadius;

    // Place where events points are located
    private float mPlaceForPointsWidth;

    private MonthPager mMonthPager;

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

    public CalendarView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (attrs != null) {
            TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CalendarView, 0, 0);
            try {
                mBackgroundColor = typedArray.getColor(R.styleable.CalendarView_backgroundColor, 0xffffffff);
                mTextColor = typedArray.getColor(R.styleable.CalendarView_textColor, Color.BLACK);
                mTextInsideCircleColor = typedArray.getColor(R.styleable.CalendarView_textInsideCircleColor, Color.WHITE);
                mWeekDaysNamesColor = typedArray.getColor(R.styleable.CalendarView_weekDaysNamesColor, Color.GRAY);
                mCurrentDayCircleColor = typedArray.getColor(R.styleable.CalendarView_currentDayCircleColor, Color.BLACK);
                mSelectedDayCircleColor = typedArray.getColor(R.styleable.CalendarView_selectedCircleColor, Color.LTGRAY);
                mFirstDayOfWeek = typedArray.getInt(R.styleable.CalendarView_firstDayOfWeek, Calendar.MONDAY);
            } finally {
                typedArray.recycle();
            }
        }
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Background drawing
        canvas.drawRect(0, 0, getWidth(), getHeight(), mBackgroundPaint);
        // Focused month drawing
        drawMonth(canvas, FOCUSED_MONTH);
        // If mOffset <= 0 previous month out of sight
        if (mOffset > 0) {
            drawMonth(canvas, PREVIOUS_MONTH);
        }
        // If mOffset >= 0 next month out of sight
        if (mOffset < 0) {
            drawMonth(canvas, NEXT_MONTH);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int minWidth = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
        int width = resolveSizeAndState(minWidth, widthMeasureSpec, 1);
        int height;
        if (mIsResize) {
            // mViewHeight is changing by resizeView() method
            height = mViewHeight;
        } else {
            height = (int) (width * RATIO_ROW_HEIGHT_WIDTH * getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH)));
            mPaddingX = width / RATIO_WIDTH_PADDING_X;
            mPaddingY = width / RATIO_WIDTH_PADDING_Y;
            mBetweenX = (width - mPaddingX * 2) / (DEFAULT_DAYS_IN_WEEK - 1);
            mBetweenY = (height / mRowsCount * 6 - mPaddingY * 2) / 5;
            mTextHeight = width / RATIO_WIDTH_TEXT_HEIGHT;
            mCircleRadius = width / RATIO_WIDTH_CIRCLE_RADIUS;
            mEventCircleRadius = mCircleRadius / 7;
            mPlaceForPointsWidth = mBetweenX / 2;
            mTextPaint.setTextSize(mTextHeight);
            mTextInsideCirclePaint.setTextSize(mTextHeight);
            mWeekDaysNamesTextPaint.setTextSize(mTextHeight);
        }
        setMeasuredDimension(width, height);
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent) {
        switch(motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
                } else {
                    mVelocityTracker.clear();
                }
                mVelocityTracker.addMovement(motionEvent);
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(motionEvent);
                mVelocityTracker.computeCurrentVelocity(1000);
                break;
            case MotionEvent.ACTION_UP:
                getParent().requestDisallowInterceptTouchEvent(false);
                mVelocityTracker.computeCurrentVelocity(1000);
                handleGesture(mVelocityTracker.getXVelocity());
                mVelocityTracker.recycle();
                mVelocityTracker.clear();
                mVelocityTracker = null;
                break;
        }
        return this.mDetector.onTouchEvent(motionEvent) || super.onTouchEvent(motionEvent);
    }

    private void init() {
        mMonthPager = new MonthPager(mFirstDayOfWeek);
        mScroller = new OverScroller(getContext());
        mDetector = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {

            @Override
            public boolean onDown(MotionEvent motionEvent) {
                return true;
            }

            @Override
            public boolean onSingleTapUp(MotionEvent motionEvent) {
                if (!mScroller.isFinished()) {
                    return true;
                }
                CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
                float x = motionEvent.getX(), y = motionEvent.getY();
                int day = getDayNumberOfCrd(x, y, calendarMonth.getFirstWeekDay());
                if (day < 1 || day > calendarMonth.getAmountOfDays()) {
                    return true;
                }
                mMonthPager.selectDay(day);
                invalidate();
                dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(day));
                return true;
            }

            @Override
            public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float dx, float dy) {
                getParent().requestDisallowInterceptTouchEvent(true);
                int width = getWidth();
                // Return if MonthPager reached max or min date
                if ((dx > 0 && mMonthPager.isReachedMax()) || (dx < 0 && mMonthPager.isReachedMin())) {
                    return true;
                }
                mOffset -= dx;
                // Set max offset value, if offset has reached one of the edges
                if (mOffset > width) {
                    mOffset = width;
                } else if (mOffset < -width) {
                    mOffset = -width;
                }
                invalidate();
                return true;
            }

            @Override
            public void onLongPress(MotionEvent motionEvent) {
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                return false;
            }
        });
        mRowsCount = getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH));
        mWeekDayNames = CommonUtils.getWeekDaysAbbreviation(mFirstDayOfWeek);
        // Text of days numbers
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        // Text of selected and current day number
        mTextInsideCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextInsideCirclePaint.setColor(mTextInsideCircleColor);
        mSelectedDayCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mSelectedDayCirclePaint.setStyle(Paint.Style.FILL);
        mSelectedDayCirclePaint.setColor(mSelectedDayCircleColor);
        mCurrentDayCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCurrentDayCirclePaint.setStyle(Paint.Style.FILL);
        mCurrentDayCirclePaint.setColor(mCurrentDayCircleColor);
        mEventCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mEventCirclePaint.setStyle(Paint.Style.FILL);
        // Week Name Text
        mWeekDaysNamesTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mWeekDaysNamesTextPaint.setColor(mWeekDaysNamesColor);
        mWeekDaysNamesTextPaint.setTypeface(Typeface.DEFAULT_BOLD);
        // Background
        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setStyle(Paint.Style.FILL);
        mBackgroundPaint.setColor(mBackgroundColor);
    }

    /**
     * @param x The x position of touch event.
     * @param y The y position of touch event.
     * @param firstDayOfWeek First day of the week.
     * @return Selected day of focused month.
     */
    private int getDayNumberOfCrd(float x, float y, int firstDayOfWeek) {
        // Measure text
        mWeekDaysNamesTextPaint.getTextBounds(mWeekDayNames[0], 0, mWeekDayNames[0].length(), mTextRect);
        float weekDaysNamesHeight = mTextRect.top + mBetweenY;
        float widthPerDay = (getWidth() - mPaddingX * 2 + mBetweenX) / DEFAULT_DAYS_IN_WEEK;
        float heightPerDay = (getHeight() - mPaddingY * 2 - weekDaysNamesHeight + mBetweenY) / (mRowsCount - 1);
        x = x - mPaddingX + mBetweenX;
        y = y - mPaddingY + mBetweenY - weekDaysNamesHeight;
        int row = Math.round(x / widthPerDay);
        int column = Math.round(y / heightPerDay);
        return (column - 1) * DEFAULT_DAYS_IN_WEEK + row - firstDayOfWeek;
    }

    /**
     * @param index The index on calendar grid
     * @param text Day number text, for calculation text center, if it is needed. Set null for calculating circle position.
     * @param monthIndex Month index, for calculating offset
     * @return Float array for x[0] and y[1] position
     */
    private float[] calculateCrdForIndex(int index, @Nullable String text, @MonthIndex int monthIndex) {
        int rowIndex = (index - 1) % DEFAULT_DAYS_IN_WEEK;
        int column = (index - 1) / DEFAULT_DAYS_IN_WEEK;
        float x = mPaddingX + (mBetweenX * rowIndex) + (getWidth() * monthIndex);
        float y = mPaddingY + (mBetweenY * column);
        x += mOffset;
        // Calculation of the text center
        if (text != null) {
            // Measure text size
            mTextPaint.getTextBounds(text, 0, text.length(), mTextRect);
            x -= mTextRect.centerX();
            y -= mTextRect.centerY();
        }
        return new float[] { x, y };
    }

    private void drawMonth(Canvas canvas, @MonthIndex int monthIndex) {
        CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(monthIndex);
        // Selected day circle drawing
        if (monthIndex == FOCUSED_MONTH) {
            float[] crdCircle = calculateCrdForIndex(calendarMonth.getDayIndex(mMonthPager.getSelectedDay()), null, monthIndex);
            canvas.drawCircle(crdCircle[0], crdCircle[1], mCircleRadius, mSelectedDayCirclePaint);
        }
        // Current day circle drawing
        if (mMonthPager.isOnCurrentMonth(monthIndex)) {
            float[] crdCircle = calculateCrdForIndex(calendarMonth.getDayIndex(mMonthPager.getCurrentDay()), null, monthIndex);
            canvas.drawCircle(crdCircle[0], crdCircle[1], mCircleRadius, mCurrentDayCirclePaint);
        }
        // Week days names drawing
        for (int i = 1; i <= DEFAULT_DAYS_IN_WEEK; i++) {
            float[] crd = calculateCrdForIndex(i, mWeekDayNames[i - 1], monthIndex);
            canvas.drawText(mWeekDayNames[i - 1], crd[0], crd[1], mWeekDaysNamesTextPaint);
        }
        // Numbers of days drawing
        for (int day = 1; day <= calendarMonth.getAmountOfDays(); day++) {
            int index = calendarMonth.getDayIndex(day);
            float[] crd = calculateCrdForIndex(index, Integer.toString(day), monthIndex);
            boolean isCurrentDay = mMonthPager.isOnCurrentMonth(monthIndex) && day == mMonthPager.getCurrentDay();
            boolean isSelectedDay = monthIndex == FOCUSED_MONTH && day == mMonthPager.getSelectedDay();
            canvas.drawText(Integer.toString(day), crd[0], crd[1], isCurrentDay || isSelectedDay ? mTextInsideCirclePaint : mTextPaint);
            // Events drawing
            List<CalendarEvent> events = calendarMonth.getEventOfDay(day);
            if (events != null && !isCurrentDay && !isSelectedDay) {
                drawEventsOfDay(canvas, events, crd, day);
            }
        }
    }

    private void drawEventsOfDay(Canvas canvas, List<CalendarEvent> events, float[] crd, int day) {
        // Measure text of events day number
        String dayText = Integer.toString(day);
        mTextPaint.getTextBounds(dayText, 0, dayText.length(), mTextRect);
        float offsetForCenter = mPlaceForPointsWidth / 2 - mTextRect.centerX();
        // Space between events points for X axis
        float betweenPoints = mPlaceForPointsWidth / (events.size() + 1);
        for (int i = 0; i < events.size(); i++) {
            mEventCirclePaint.setColor(events.get(i).getColor());
            canvas.drawCircle(crd[0] - offsetForCenter + betweenPoints * (i + 1), crd[1] + mCircleRadius / 2, mEventCircleRadius, mEventCirclePaint);
        }
    }

    private void handleGesture(float velocity) {
        if (velocity == 0 && mOffset == 0) {
            return;
        }
        if ((velocity > VELOCITY_THRESHOLD || mOffset > getWidth() / 2) && (!mMonthPager.isReachedMin())) {
            if (!canGoBack()) {
                handleGesture(0);
                return;
            }
            mMonthPager.goBack();
            int distance = getWidth() - mOffset;
            // Invalidate offset, because of changed focused month
            mOffset = mOffset - getWidth();
            mScroller.startScroll(mOffset, 0, distance, 0, (int) (Math.abs(distance) * RATIO_DURATION_DISTANCE));
            dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
            resizeView(getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH)));
            ViewCompat.postInvalidateOnAnimation(this);
        } else if ((velocity < -VELOCITY_THRESHOLD || mOffset < -getWidth() / 2) && (!mMonthPager.isReachedMax())) {
            if (!canGoForward()) {
                handleGesture(0);
                return;
            }
            mMonthPager.goForward();
            int distance = -getWidth() - mOffset;
            // Invalidate offset, because of changed focused month
            mOffset = mOffset + getWidth();
            mScroller.startScroll(mOffset, 0, distance, 0, (int) (Math.abs(distance) * RATIO_DURATION_DISTANCE));
            dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
            resizeView(getMonthRowsCount(mMonthPager.getCalendarMonth(FOCUSED_MONTH)));
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            // Smooth scroll to focused month
            int distance = -mOffset;
            mScroller.startScroll(mOffset, 0, distance, 0, // More slowly scroll (x2 duration)
            (int) (Math.abs(distance) * RATIO_DURATION_DISTANCE * 2));
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private boolean canGoForward() {
        return mOffset <= 0;
    }

    private boolean canGoBack() {
        return mOffset >= 0;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            mOffset = mScroller.getCurrX();
            invalidate();
            if (mOffset == mScroller.getFinalX()) {
                mScroller.forceFinished(true);
                // First day of month selected, after changing month
                CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
                dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(mMonthPager.getSelectedDay()));
            }
        }
    }

    /**
     * @param calendarMonth Target month.
     * @return Number of required rows, for drawing target month.
     */
    private int getMonthRowsCount(CalendarMonth calendarMonth) {
        float rowsCount = (float) (calendarMonth.getAmountOfDays() + calendarMonth.getFirstWeekDay()) / DEFAULT_DAYS_IN_WEEK;
        // + 1 for week days names row
        return (int) Math.ceil(rowsCount) + 1;
    }

    /**
     * Change view height, for next month
     * @param targetRowsCount Number of next month rows
     */
    private void resizeView(int targetRowsCount) {
        // If current rows count are equals to target rows count resize not required
        if (mRowsCount == targetRowsCount) {
            return;
        }
        clreplaced ResizeAnimation extends Animation {

            private int mTargetHeight;

            private View mView;

            private int mStartHeight;

            private ResizeAnimation(View view, int targetHeight, int startHeight) {
                mView = view;
                mTargetHeight = targetHeight;
                mStartHeight = startHeight;
            }

            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                int newHeight = (int) (mStartHeight + (mTargetHeight - mStartHeight) * interpolatedTime);
                mViewHeight = newHeight;
                mView.getLayoutParams().height = newHeight;
                mView.requestLayout();
                if (interpolatedTime == 1.0f) {
                    // Animation is over
                    mIsResize = false;
                }
            }

            @Override
            public void initialize(int width, int height, int parentWidth, int parentHeight) {
                super.initialize(width, height, parentWidth, parentHeight);
            }
        }
        ResizeAnimation resizeAnimation = new ResizeAnimation(this, getHeight() * targetRowsCount / mRowsCount, getHeight());
        resizeAnimation.setDuration(RESIZE_ANIMATION_DURATION);
        startAnimation(resizeAnimation);
        mIsResize = true;
        mRowsCount = targetRowsCount;
    }

    // Perform listeners
    private void dispatchOnDateSelected(Calendar calendar, List<CalendarEvent> eventsOfDay) {
        if (mOnDateSelectedListener != null) {
            mOnDateSelectedListener.onDateSelected(calendar, eventsOfDay);
        }
    }

    private void dispatchOnMonthChanged(Calendar calendar) {
        if (mOnMonthChangedListener != null) {
            mOnMonthChangedListener.onMonthChanged(calendar);
        }
    }

    // Public methods
    public void updateEvents() {
        mMonthPager.updateEvents();
    }

    public void setFirstDayOfWeek(int dayOfWeek) {
        if (dayOfWeek < 1 || dayOfWeek > 7) {
            throw new IllegalArgumentException("Day must be from Java Calendar clreplaced");
        }
        mFirstDayOfWeek = dayOfWeek;
        mWeekDayNames = CommonUtils.getWeekDaysAbbreviation(mFirstDayOfWeek);
        mMonthPager.setFirstDayOfWeek(dayOfWeek);
        invalidate();
    }

    public void setMinimumDate(long timeInMillis) {
        mMonthPager.setMinimumDate(timeInMillis);
        dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
        invalidate();
    }

    public void setMaximumDate(long timeInMillis) {
        mMonthPager.setMaximumDate(timeInMillis);
        dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
        invalidate();
    }

    public void setOnDateSelectedListener(OnDateSelectedListener onDateSelectedListener) {
        mOnDateSelectedListener = onDateSelectedListener;
        CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
        dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(mMonthPager.getSelectedDay()));
    }

    public void setOnMonthChangedListener(OnMonthChangedListener onMonthChangedListener) {
        mOnMonthChangedListener = onMonthChangedListener;
        dispatchOnMonthChanged(mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar());
    }

    public void setOnLoadEventsListener(OnLoadEventsListener onLoadEventsListener) {
        mMonthPager.setOnLoadEventsListener(onLoadEventsListener);
        CalendarMonth calendarMonth = mMonthPager.getCalendarMonth(FOCUSED_MONTH);
        dispatchOnDateSelected(calendarMonth.getCalendar(), calendarMonth.getEventOfDay(mMonthPager.getSelectedDay()));
    }

    public void setBackgroundColor(@ColorInt int color) {
        mBackgroundColor = color;
        mBackgroundPaint.setColor(mBackgroundColor);
        invalidate();
    }

    public int getBackgroundColor() {
        return mBackgroundColor;
    }

    public void setTextColor(@ColorInt int color) {
        mTextColor = color;
        mTextPaint.setColor(mTextColor);
        invalidate();
    }

    public int getTextColor() {
        return mTextColor;
    }

    public void setTextInsideCircleColor(@ColorInt int color) {
        mTextInsideCircleColor = color;
        mTextInsideCirclePaint.setColor(mTextInsideCircleColor);
        invalidate();
    }

    public int getTextInsideCircleColor() {
        return mTextInsideCircleColor;
    }

    public void setWeekDaysNamesColor(@ColorInt int color) {
        mWeekDaysNamesColor = color;
        mWeekDaysNamesTextPaint.setColor(mWeekDaysNamesColor);
        invalidate();
    }

    public int getWeekDaysNamesColor() {
        return mWeekDaysNamesColor;
    }

    public void setCurrentDayCircleColor(@ColorInt int color) {
        mCurrentDayCircleColor = color;
        mCurrentDayCirclePaint.setColor(mCurrentDayCircleColor);
        invalidate();
    }

    public int getCurrentDayCircleColor() {
        return mCurrentDayCircleColor;
    }

    public void setSelectedDayCircleColor(@ColorInt int color) {
        mSelectedDayCircleColor = color;
        mSelectedDayCirclePaint.setColor(mSelectedDayCircleColor);
        invalidate();
    }

    public int getSelectedDayCircleColor() {
        return mSelectedDayCircleColor;
    }

    public Calendar getFocusedMonthCalendar() {
        return mMonthPager.getCalendarMonth(FOCUSED_MONTH).getCalendar();
    }
}

19 Source : CoordinatorLinearLayout.java
with MIT License
from Skykai521

/**
 * Created by sky on 17/3/1.
 */
public clreplaced CoordinatorLinearLayout extends LinearLayout implements CoordinatorListener {

    public static int DEFAULT_DURATION = 500;

    private int state = WHOLE_STATE;

    private int topBarHeight;

    private int topViewHeight;

    private int minScrollToTop;

    private int minScrollToWhole;

    private int maxScrollDistance;

    private float lastPositionY;

    private boolean beingDragged;

    private Context context;

    private OverScroller scroller;

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

    public CoordinatorLinearLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CoordinatorLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
    }

    private void init() {
        scroller = new OverScroller(context);
    }

    public void setTopViewParam(int topViewHeight, int topBarHeight) {
        this.topViewHeight = topViewHeight;
        this.topBarHeight = topBarHeight;
        this.maxScrollDistance = this.topViewHeight - this.topBarHeight;
        this.minScrollToTop = this.topBarHeight;
        this.minScrollToWhole = maxScrollDistance - this.topBarHeight;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                int y = (int) ev.getY();
                lastPositionY = y;
                if (state == COLLAPSE_STATE && y < topBarHeight) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final int y = (int) ev.getRawY();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                lastPositionY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaY = (int) (lastPositionY - y);
                if (state == COLLAPSE_STATE && deltaY < 0) {
                    beingDragged = true;
                    setScrollY(maxScrollDistance + deltaY);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (beingDragged) {
                    onSwitch();
                    return true;
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onCoordinateScroll(int x, int y, int deltaX, int deltaY, boolean isScrollToTop) {
        if (y < topViewHeight && state == WHOLE_STATE && getScrollY() < getScrollRange()) {
            beingDragged = true;
            setScrollY(topViewHeight - y);
            return true;
        } else if (isScrollToTop && state == COLLAPSE_STATE && deltaY < 0) {
            beingDragged = true;
            setScrollY(maxScrollDistance + deltaY);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void onSwitch() {
        if (state == WHOLE_STATE) {
            if (getScrollY() >= minScrollToTop) {
                switchToTop();
            } else {
                switchToWhole();
            }
        } else if (state == COLLAPSE_STATE) {
            if (getScrollY() <= minScrollToWhole) {
                switchToWhole();
            } else {
                switchToTop();
            }
        }
    }

    @Override
    public boolean isBeingDragged() {
        return beingDragged;
    }

    public void switchToWhole() {
        if (!scroller.isFinished()) {
            scroller.abortAnimation();
        }
        scroller.startScroll(0, getScrollY(), 0, -getScrollY(), DEFAULT_DURATION);
        postInvalidate();
        state = WHOLE_STATE;
        beingDragged = false;
    }

    public void switchToTop() {
        if (!scroller.isFinished()) {
            scroller.abortAnimation();
        }
        scroller.startScroll(0, getScrollY(), 0, getScrollRange() - getScrollY(), DEFAULT_DURATION);
        postInvalidate();
        state = COLLAPSE_STATE;
        beingDragged = false;
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            setScrollY(scroller.getCurrY());
            postInvalidate();
        }
    }

    private int getScrollRange() {
        return maxScrollDistance;
    }
}

19 Source : MainHeaderBehavior.java
with Apache License 2.0
from SheHuan

public clreplaced MainHeaderBehavior extends ViewOffsetBehavior<View> {

    private static final int STATE_OPENED = 0;

    private static final int STATE_CLOSED = 1;

    private static final int DURATION_SHORT = 300;

    private static final int DURATION_LONG = 600;

    private int mCurState = STATE_OPENED;

    private OnHeaderStateListener mHeaderStateListener;

    private OverScroller mOverScroller;

    // CoordinatorLayout
    private WeakReference<CoordinatorLayout> mParent;

    // CoordinatorLayout的子View,即header
    private WeakReference<View> mChild;

    // 界面整体向上滑动,达到列表可滑动的临界点
    private boolean upReach;

    // 列表向上滑动后,再向下滑动,达到界面整体可滑动的临界点
    private boolean downReach;

    // 列表上一个全部可见的item位置
    private int lastPosition = -1;

    private FlingRunnable mFlingRunnable;

    private Context mContext;

    // tab上移结束后是否悬浮在固定位置
    private boolean tabSuspension = false;

    public MainHeaderBehavior() {
        init();
    }

    public MainHeaderBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    private void init() {
        mOverScroller = new OverScroller(mContext);
    }

    @Override
    protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        super.layoutChild(parent, child, layoutDirection);
        mParent = new WeakReference<>(parent);
        mChild = new WeakReference<>(child);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        if (tabSuspension) {
            return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && !isClosed();
        }
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) {
        lastPosition = -1;
        return !isClosed();
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent ev) {
        switch(ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downReach = false;
                upReach = false;
                break;
            case MotionEvent.ACTION_UP:
                handleActionUp(child);
                break;
        }
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    /**
     * @param coordinatorLayout
     * @param child             代表header
     * @param target            代表RecyclerView
     * @param dx
     * @param dy                上滑 dy>0, 下滑dy<0
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        // 制造滑动视察,使header的移动比手指滑动慢
        float scrollY = dy / 4.0f;
        if (target instanceof NestedLinearLayout) {
            // 处理header滑动
            float finalY = child.getTranslationY() - scrollY;
            if (finalY < getHeaderOffset()) {
                finalY = getHeaderOffset();
            } else if (finalY > 0) {
                finalY = 0;
            }
            child.setTranslationY(finalY);
            consumed[1] = dy;
        } else if (target instanceof RecyclerView) {
            // 处理列表滑动
            RecyclerView list = (RecyclerView) target;
            int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
            // header closed状态下,列表上滑再下滑到第一个item全部显示,此时不让CoordinatorLayout整体下滑,
            // 手指重新抬起再下滑才可以整体滑动
            if (pos == 0 && pos < lastPosition) {
                downReach = true;
            }
            if (pos == 0 && canScroll(child, scrollY)) {
                // 如果列表第一个item全部可见、或者header已展开,则让CoordinatorLayout消费掉事件
                float finalY = child.getTranslationY() - scrollY;
                // header已经closed,整体不能继续上滑,手指抬起重新上滑列表开始滚动
                if (finalY < getHeaderOffset()) {
                    finalY = getHeaderOffset();
                    upReach = true;
                } else if (finalY > 0) {
                    // header已经opened,整体不能继续下滑
                    finalY = 0;
                }
                child.setTranslationY(finalY);
                // 让CoordinatorLayout消费掉事件,实现整体滑动
                consumed[1] = dy;
            }
            lastPosition = pos;
        }
    }

    /**
     * 是否可以整体滑动
     *
     * @return
     */
    private boolean canScroll(View child, float scrollY) {
        if (scrollY > 0 && child.getTranslationY() > getHeaderOffset()) {
            return true;
        }
        if (child.getTranslationY() == getHeaderOffset() && upReach) {
            return true;
        }
        if (scrollY < 0 && !downReach) {
            return true;
        }
        return false;
    }

    private int getHeaderOffset() {
        return mContext.getResources().getDimensionPixelOffset(R.dimen.header_offset);
    }

    private void handleActionUp(View child) {
        if (mFlingRunnable != null) {
            child.removeCallbacks(mFlingRunnable);
            mFlingRunnable = null;
        }
        // 手指抬起时,header上滑距离超过总距离三分之一,则整体自动上滑到关闭状态
        if (child.getTranslationY() < getHeaderOffset() / 3.0f) {
            scrollToClose(DURATION_SHORT);
        } else {
            scrollToOpen(DURATION_SHORT);
        }
    }

    private void onFlingFinished(View layout) {
        changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);
    }

    /**
     * 直接展开
     */
    public void openHeader() {
        openHeader(DURATION_LONG);
    }

    private void openHeader(int duration) {
        if (isClosed() && mChild.get() != null) {
            if (mFlingRunnable != null) {
                mChild.get().removeCallbacks(mFlingRunnable);
                mFlingRunnable = null;
            }
            scrollToOpen(duration);
        }
    }

    public void closeHeader() {
        closeHeader(DURATION_LONG);
    }

    private void closeHeader(int duration) {
        if (!isClosed() && mChild.get() != null) {
            if (mFlingRunnable != null) {
                mChild.get().removeCallbacks(mFlingRunnable);
                mFlingRunnable = null;
            }
            scrollToClose(duration);
        }
    }

    private boolean isClosed(View child) {
        return child.getTranslationY() == getHeaderOffset();
    }

    public boolean isClosed() {
        return mCurState == STATE_CLOSED;
    }

    private void changeState(int newState) {
        if (mCurState != newState) {
            mCurState = newState;
            if (mHeaderStateListener == null) {
                return;
            }
            if (mCurState == STATE_OPENED) {
                mHeaderStateListener.onHeaderOpened();
            } else {
                mHeaderStateListener.onHeaderClosed();
            }
        }
    }

    private void scrollToClose(int duration) {
        int curTranslationY = (int) mChild.get().getTranslationY();
        int dy = getHeaderOffset() - curTranslationY;
        mOverScroller.startScroll(0, curTranslationY, 0, dy, duration);
        start();
    }

    private void scrollToOpen(int duration) {
        float curTranslationY = mChild.get().getTranslationY();
        mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY, duration);
        start();
    }

    private void start() {
        if (mOverScroller.computeScrollOffset()) {
            mFlingRunnable = new FlingRunnable(mParent.get(), mChild.get());
            ViewCompat.postOnAnimation(mChild.get(), mFlingRunnable);
        } else {
            onFlingFinished(mChild.get());
        }
    }

    private clreplaced FlingRunnable implements Runnable {

        private final CoordinatorLayout mParent;

        private final View mLayout;

        FlingRunnable(CoordinatorLayout parent, View layout) {
            mParent = parent;
            mLayout = layout;
        }

        @Override
        public void run() {
            if (mLayout != null && mOverScroller != null) {
                if (mOverScroller.computeScrollOffset()) {
                    mLayout.setTranslationY(mOverScroller.getCurrY());
                    ViewCompat.postOnAnimation(mLayout, this);
                } else {
                    onFlingFinished(mLayout);
                }
            }
        }
    }

    public void setTabSuspension(boolean tabSuspension) {
        this.tabSuspension = tabSuspension;
    }

    public void setHeaderStateListener(OnHeaderStateListener headerStateListener) {
        mHeaderStateListener = headerStateListener;
    }

    public interface OnHeaderStateListener {

        void onHeaderClosed();

        void onHeaderOpened();
    }
}

19 Source : NestedScrollChildSample.java
with Apache License 2.0
from RubiTree

/**
 * >> Description <<
 * 虽然没有报错,但这不是可以运行的代码,这是剔除 NestedScrollView 中关于 parent 的部分,得到的可以认为是官方的
 * NestedScrollingChild 接口的实现建议,关键是在在触摸和滚动时怎么调用 NestedScrollingChild 的方法,也就是下
 * 面 onInterceptTouchEvent() 、 onTouchEvent() 、 computeScroll() 中不到 200 行的代码
 * <p>
 * >> Attention <<
 * 这里为了让主线逻辑更加清晰,省略了多点触控相关的代码,实际开发如果需要,可以直接参考 NestedScrollView 中的写
 * 法,也不会很麻烦
 * <p>
 * >> Others <<
 * <p>
 * Created by RubiTree ; On 2019-01-08.
 */
public clreplaced NestedScrollChildSample extends FrameLayout implements NestedScrollingChild3 {

    private OverScroller mScroller;

    /**
     * True if the user is currently dragging this ScrollView around. This is
     * not the same as 'is being flinged', which can be checked by
     * mScroller.isFinished() (flinging begins when the user lifts his finger).
     */
    private boolean mIsBeingDragged = false;

    /**
     * Determines speed during touch scrolling
     */
    private VelocityTracker mVelocityTracker;

    private int mTouchSlop;

    private int mMinimumVelocity;

    private int mMaximumVelocity;

    /**
     * Used during scrolling to retrieve the new offset within the window.
     */
    private final int[] mScrollOffset = new int[2];

    private int mNestedYOffset;

    private final int[] mScrollConsumed = new int[2];

    private int mLastScrollerY;

    private int mLastMotionY;

    private final NestedScrollingChildHelper mChildHelper;

    /*--------------------------------------------------------------------------------------------*/
    public NestedScrollChildSample(@NonNull Context context) {
        this(context, null);
    }

    public NestedScrollChildSample(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollChildSample(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    /*--------------------------------------------------------------------------------------------*/
    // NestedScrollingChild3
    @Override
    public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
        mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
    }

    // NestedScrollingChild2
    @Override
    public boolean startNestedScroll(int axes, int type) {
        return mChildHelper.startNestedScroll(axes, type);
    }

    @Override
    public void stopNestedScroll(int type) {
        mChildHelper.stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return mChildHelper.hasNestedScrollingParent(type);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

    // NestedScrollingChild
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void stopNestedScroll() {
        stopNestedScroll(ViewCompat.TYPE_TOUCH);
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    /*--------------------------------------------------------------------------------------------*/
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch(action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                {
                    mLastMotionY = (int) ev.getY();
                    mVelocityTracker.addMovement(ev);
                    /*
                 * If being flinged and user touches the screen, initiate drag;
                 * otherwise don't. mScroller.isFinished should be false when
                 * being flinged. We need to call computeScrollOffset() first so that
                 * isFinished() is correct.
                 */
                    mScroller.computeScrollOffset();
                    mIsBeingDragged = isSelfScrolling();
                    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    if (!mIsBeingDragged) {
                        final int y = (int) ev.getY();
                        final int yDiff = Math.abs(y - mLastMotionY);
                        if (yDiff > mTouchSlop) {
                            mIsBeingDragged = true;
                            mLastMotionY = y;
                            mVelocityTracker.addMovement(ev);
                            mNestedYOffset = 0;
                            requestParentDisallowInterceptTouchEvent();
                        }
                    }
                    break;
                }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                trySpringBack();
                stopNestedScroll(ViewCompat.TYPE_TOUCH);
                break;
        }
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int actionMasked = ev.getActionMasked();
        MotionEvent vtev = MotionEvent.obtain(ev);
        if (actionMasked == MotionEvent.ACTION_DOWN)
            mNestedYOffset = 0;
        vtev.offsetLocation(0, mNestedYOffset);
        switch(actionMasked) {
            case MotionEvent.ACTION_DOWN:
                {
                    if ((mIsBeingDragged = isSelfScrolling()))
                        requestParentDisallowInterceptTouchEvent();
                    // If being flinged and user touches, stop the fling. isFinished will be false if being flinged.
                    if (isSelfScrolling())
                        abortAnimatedScroll();
                    mLastMotionY = (int) ev.getY();
                    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                final int y = (int) ev.getY();
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    requestParentDisallowInterceptTouchEvent();
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];
                    final int oldY = getScrollY();
                    // Calling overScrollByCompat will call onOverScrolled, which calls onScrollChanged if applicable.
                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, getScrollRange(), 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }
                    final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    mScrollConsumed[1] = 0;
                    dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed);
                    mLastMotionY -= mScrollOffset[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                    deltaY -= mScrollConsumed[1];
                    if (canOverscroll())
                        showOverScrollEdgeEffect();
                }
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity();
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    if (!dispatchNestedPreFling(0, -initialVelocity)) {
                        dispatchNestedFling(0, -initialVelocity, true);
                        fling(-initialVelocity);
                    }
                } else {
                    trySpringBack();
                }
                endDrag();
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0)
                    trySpringBack();
                endDrag();
                break;
        }
        if (mVelocityTracker != null)
            mVelocityTracker.addMovement(vtev);
        vtev.recycle();
        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.isFinished())
            return;
        mScroller.computeScrollOffset();
        final int y = mScroller.getCurrY();
        int unconsumed = y - mLastScrollerY;
        mLastScrollerY = y;
        // Nested Scrolling Pre Preplaced
        mScrollConsumed[1] = 0;
        dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH);
        unconsumed -= mScrollConsumed[1];
        if (unconsumed != 0) {
            // Internal Scroll
            final int oldScrollY = getScrollY();
            overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(), 0, 0, false);
            final int scrolledByMe = getScrollY() - oldScrollY;
            unconsumed -= scrolledByMe;
            // Nested Scrolling Post Preplaced
            mScrollConsumed[1] = 0;
            dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, null, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
            unconsumed -= mScrollConsumed[1];
        }
        if (unconsumed != 0) {
            if (canOverscroll())
                showOverScrollEdgeEffect();
            abortAnimatedScroll();
        }
        if (isSelfScrolling())
            ViewCompat.postInvalidateOnAnimation(this);
    }

    private void abortAnimatedScroll() {
        mScroller.abortAnimation();
        stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
    }

    public void fling(int velocityY) {
        mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
    }

    private void endDrag() {
        mIsBeingDragged = false;
        stopNestedScroll(ViewCompat.TYPE_TOUCH);
    }

    /*--------------------------------------------------------------------------------------------*/
    // Fake
    private int getScrollRange() {
        return 0;
    }

    // Fake
    // scroll self
    private boolean overScrollByCompat(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
        return true;
    }

    // Fake
    private void showOverScrollEdgeEffect() {
    }

    private boolean isSelfScrolling() {
        return !mScroller.isFinished();
    }

    private void requestParentDisallowInterceptTouchEvent() {
        final ViewParent parent = getParent();
        if (parent != null)
            parent.requestDisallowInterceptTouchEvent(true);
    }

    private void trySpringBack() {
        if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private boolean canOverscroll() {
        final int mode = getOverScrollMode();
        return mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && getScrollRange() > 0);
    }
}

19 Source : ResolverDrawerLayout.java
with MIT License
from RikkaApps

public clreplaced ResolverDrawerLayout extends ViewGroup {

    private static final String TAG = "ResolverDrawerLayout";

    /**
     * Max width of the whole drawer layout
     */
    private int mMaxWidth;

    /**
     * Max total visible height of views not marked always-show when in the closed/initial state
     */
    private int mMaxCollapsedHeight;

    /**
     * Max total visible height of views not marked always-show when in the closed/initial state
     * when a default option is present
     */
    private int mMaxCollapsedHeightSmall;

    private boolean mSmallCollapsed;

    /**
     * Move views down from the top by this much in px
     */
    private float mCollapseOffset;

    /**
     * Track fractions of pixels from drag calculations. Without this, the view offsets get
     * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
     */
    private float mDragRemainder = 0.0f;

    private int mCollapsibleHeight;

    private int mUncollapsibleHeight;

    private int mAlwaysShowHeight;

    /**
     * The height in pixels of reserved space added to the top of the collapsed UI;
     * e.g. chooser targets
     */
    private int mCollapsibleHeightReserved;

    private int mTopOffset;

    private boolean mShowAtTop;

    private boolean mIsDragging;

    private boolean mOpenOnClick;

    private boolean mOpenOnLayout;

    private boolean mDismissOnScrollerFinished;

    private final int mTouchSlop;

    private final float mMinFlingVelocity;

    private final OverScroller mScroller;

    private final VelocityTracker mVelocityTracker;

    private Drawable mScrollIndicatorDrawable;

    private OnDismissedListener mOnDismissedListener;

    private RunOnDismissedListener mRunOnDismissedListener;

    private OnCollapsedChangedListener mOnCollapsedChangedListener;

    private boolean mDismissLocked;

    private float mInitialTouchX;

    private float mInitialTouchY;

    private float mLastTouchY;

    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;

    private final Rect mTempRect = new Rect();

    private AbsListView mNestedScrollingChild;

    private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = new ViewTreeObserver.OnTouchModeChangeListener() {

        @Override
        public void onTouchModeChanged(boolean isInTouchMode) {
            if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
                smoothScrollTo(0, 0);
            }
        }
    };

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

    public ResolverDrawerLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, defStyleAttr, 0);
        mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
        mMaxCollapsedHeight = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
        mMaxCollapsedHeightSmall = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, mMaxCollapsedHeight);
        mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
        a.recycle();
        mScrollIndicatorDrawable = context.getDrawable(R.drawable.scroll_indicator_material);
        mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, android.R.interpolator.decelerate_quint));
        mVelocityTracker = VelocityTracker.obtain();
        final ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    public void setSmallCollapsed(boolean smallCollapsed) {
        mSmallCollapsed = smallCollapsed;
        requestLayout();
    }

    public boolean isSmallCollapsed() {
        return mSmallCollapsed;
    }

    public boolean isCollapsed() {
        return mCollapseOffset > 0;
    }

    public void setShowAtTop(boolean showOnTop) {
        mShowAtTop = showOnTop;
        invalidate();
        requestLayout();
    }

    public boolean getShowAtTop() {
        return mShowAtTop;
    }

    public void setCollapsed(boolean collapsed) {
        if (!isLaidOut()) {
            mOpenOnLayout = collapsed;
        } else {
            smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
        }
    }

    public void setCollapsibleHeightReserved(int heightPixels) {
        final int oldReserved = mCollapsibleHeightReserved;
        mCollapsibleHeightReserved = heightPixels;
        final int dReserved = mCollapsibleHeightReserved - oldReserved;
        if (dReserved != 0 && mIsDragging) {
            mLastTouchY -= dReserved;
        }
        final int oldCollapsibleHeight = mCollapsibleHeight;
        mCollapsibleHeight = Math.max(mCollapsibleHeight, getMaxCollapsedHeight());
        if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
            return;
        }
        invalidate();
    }

    public void setDismissLocked(boolean locked) {
        mDismissLocked = locked;
    }

    private boolean isMoving() {
        return mIsDragging || !mScroller.isFinished();
    }

    private boolean isDragging() {
        return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
    }

    private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
        if (oldCollapsibleHeight == mCollapsibleHeight) {
            return false;
        }
        if (getShowAtTop()) {
            // Keep the drawer fully open.
            mCollapseOffset = 0;
            return false;
        }
        if (isLaidOut()) {
            final boolean isCollapsedOld = mCollapseOffset != 0;
            if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight && mCollapseOffset == oldCollapsibleHeight)) {
                // Stay closed even at the new height.
                mCollapseOffset = mCollapsibleHeight;
            } else {
                mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
            }
            final boolean isCollapsedNew = mCollapseOffset != 0;
            if (isCollapsedOld != isCollapsedNew) {
                onCollapsedChanged(isCollapsedNew);
            }
        } else {
            // Start out collapsed at first unless we restored state for otherwise
            mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
        }
        return true;
    }

    private int getMaxCollapsedHeight() {
        return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight) + mCollapsibleHeightReserved;
    }

    public void setOnDismissedListener(OnDismissedListener listener) {
        mOnDismissedListener = listener;
    }

    private boolean isDismissable() {
        return mOnDismissedListener != null && !mDismissLocked;
    }

    public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
        mOnCollapsedChangedListener = listener;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            mVelocityTracker.clear();
        }
        mVelocityTracker.addMovement(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    final float x = ev.getX();
                    final float y = ev.getY();
                    mInitialTouchX = x;
                    mInitialTouchY = mLastTouchY = y;
                    mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                {
                    final float x = ev.getX();
                    final float y = ev.getY();
                    final float dy = y - mInitialTouchY;
                    if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                        mActivePointerId = ev.getPointerId(0);
                        mIsDragging = true;
                        mLastTouchY = Math.max(mLastTouchY - mTouchSlop, Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
                    }
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                {
                    onSecondaryPointerUp(ev);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                {
                    resetTouch();
                }
                break;
        }
        if (mIsDragging) {
            abortAnimation();
        }
        return mIsDragging || mOpenOnClick;
    }

    private boolean isNestedChildScrolled() {
        return mNestedScrollingChild != null && mNestedScrollingChild.getChildCount() > 0 && (mNestedScrollingChild.getFirstVisiblePosition() > 0 || mNestedScrollingChild.getChildAt(0).getTop() < 0);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        mVelocityTracker.addMovement(ev);
        boolean handled = false;
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    final float x = ev.getX();
                    final float y = ev.getY();
                    mInitialTouchX = x;
                    mInitialTouchY = mLastTouchY = y;
                    mActivePointerId = ev.getPointerId(0);
                    final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
                    handled = isDismissable() || mCollapsibleHeight > 0;
                    mIsDragging = hitView && handled;
                    abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                {
                    int index = ev.findPointerIndex(mActivePointerId);
                    if (index < 0) {
                        Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
                        index = 0;
                        mActivePointerId = ev.getPointerId(0);
                        mInitialTouchX = ev.getX();
                        mInitialTouchY = mLastTouchY = ev.getY();
                    }
                    final float x = ev.getX(index);
                    final float y = ev.getY(index);
                    if (!mIsDragging) {
                        final float dy = y - mInitialTouchY;
                        if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
                            handled = mIsDragging = true;
                            mLastTouchY = Math.max(mLastTouchY - mTouchSlop, Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
                        }
                    }
                    if (mIsDragging) {
                        final float dy = y - mLastTouchY;
                        if (dy > 0 && isNestedChildScrolled()) {
                            mNestedScrollingChild.smoothScrollBy((int) -dy, 0);
                        } else {
                            performDrag(dy);
                        }
                    }
                    mLastTouchY = y;
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                {
                    final int pointerIndex = ev.getActionIndex();
                    final int pointerId = ev.getPointerId(pointerIndex);
                    mActivePointerId = pointerId;
                    mInitialTouchX = ev.getX(pointerIndex);
                    mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                {
                    onSecondaryPointerUp(ev);
                }
                break;
            case MotionEvent.ACTION_UP:
                {
                    final boolean wasDragging = mIsDragging;
                    mIsDragging = false;
                    if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && findChildUnder(ev.getX(), ev.getY()) == null) {
                        if (isDismissable()) {
                            dispatchOnDismissed();
                            resetTouch();
                            return true;
                        }
                    }
                    if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
                        smoothScrollTo(0, 0);
                        return true;
                    }
                    mVelocityTracker.computeCurrentVelocity(1000);
                    final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    if (Math.abs(yvel) > mMinFlingVelocity) {
                        if (getShowAtTop()) {
                            if (isDismissable() && yvel < 0) {
                                abortAnimation();
                                dismiss();
                            } else {
                                smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
                            }
                        } else {
                            if (isDismissable() && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
                                smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
                                mDismissOnScrollerFinished = true;
                            } else {
                                if (isNestedChildScrolled()) {
                                    mNestedScrollingChild.smoothScrollToPosition(0);
                                }
                                smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
                            }
                        }
                    } else {
                        smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
                    }
                    resetTouch();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                {
                    if (mIsDragging) {
                        smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
                    }
                    resetTouch();
                    return true;
                }
        }
        return handled;
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mInitialTouchX = ev.getX(newPointerIndex);
            mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
        }
    }

    private void resetTouch() {
        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
        mIsDragging = false;
        mOpenOnClick = false;
        mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
        mVelocityTracker.clear();
    }

    private void dismiss() {
        mRunOnDismissedListener = new RunOnDismissedListener();
        post(mRunOnDismissedListener);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            final boolean keepGoing = !mScroller.isFinished();
            performDrag(mScroller.getCurrY() - mCollapseOffset);
            if (keepGoing) {
                postInvalidateOnAnimation();
            } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
                dismiss();
            }
        }
    }

    private void abortAnimation() {
        mScroller.abortAnimation();
        mRunOnDismissedListener = null;
        mDismissOnScrollerFinished = false;
    }

    private float performDrag(float dy) {
        if (getShowAtTop()) {
            return 0;
        }
        final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mCollapsibleHeight + mUncollapsibleHeight));
        if (newPos != mCollapseOffset) {
            dy = newPos - mCollapseOffset;
            mDragRemainder += dy - (int) dy;
            if (mDragRemainder >= 1.0f) {
                mDragRemainder -= 1.0f;
                dy += 1.0f;
            } else if (mDragRemainder <= -1.0f) {
                mDragRemainder += 1.0f;
                dy -= 1.0f;
            }
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (!lp.ignoreOffset) {
                    child.offsetTopAndBottom((int) dy);
                }
            }
            final boolean isCollapsedOld = mCollapseOffset != 0;
            mCollapseOffset = newPos;
            mTopOffset += dy;
            final boolean isCollapsedNew = newPos != 0;
            if (isCollapsedOld != isCollapsedNew) {
                onCollapsedChanged(isCollapsedNew);
            }
            onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
            postInvalidateOnAnimation();
            return dy;
        }
        return 0;
    }

    private void onCollapsedChanged(boolean isCollapsed) {
        /*notifyViewAccessibilityStateChangedIfNeeded(
                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);*/
        if (mScrollIndicatorDrawable != null) {
            setWillNotDraw(!isCollapsed);
        }
        if (mOnCollapsedChangedListener != null) {
            mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
        }
    }

    void dispatchOnDismissed() {
        if (mOnDismissedListener != null) {
            mOnDismissedListener.onDismissed();
        }
        if (mRunOnDismissedListener != null) {
            removeCallbacks(mRunOnDismissedListener);
            mRunOnDismissedListener = null;
        }
    }

    private void smoothScrollTo(int yOffset, float velocity) {
        abortAnimation();
        final int sy = (int) mCollapseOffset;
        int dy = yOffset - sy;
        if (dy == 0) {
            return;
        }
        final int height = getHeight();
        final int halfHeight = height / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
        final float distance = halfHeight + halfHeight * distanceInfluenceForSnapDuration(distanceRatio);
        int duration = 0;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageDelta = (float) Math.abs(dy) / height;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, 300);
        mScroller.startScroll(0, sy, 0, dy, duration);
        postInvalidateOnAnimation();
    }

    private float distanceInfluenceForSnapDuration(float f) {
        // center the values about 0.
        f -= 0.5f;
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    /**
     * Note: this method doesn't take Z into account for overlapping views
     * since it is only used in contexts where this doesn't affect the outcome.
     */
    private View findChildUnder(float x, float y) {
        return findChildUnder(this, x, y);
    }

    private static View findChildUnder(ViewGroup parent, float x, float y) {
        final int childCount = parent.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = parent.getChildAt(i);
            if (isChildUnder(child, x, y)) {
                return child;
            }
        }
        return null;
    }

    private View findListChildUnder(float x, float y) {
        View v = findChildUnder(x, y);
        while (v != null) {
            x -= v.getX();
            y -= v.getY();
            if (v instanceof AbsListView) {
                // One more after this.
                return findChildUnder((ViewGroup) v, x, y);
            }
            v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
        }
        return v;
    }

    /**
     * This only checks clipping along the bottom edge.
     */
    private boolean isListChildUnderClipped(float x, float y) {
        final View listChild = findListChildUnder(x, y);
        return listChild != null && isDescendantClipped(listChild);
    }

    private boolean isDescendantClipped(View child) {
        mTempRect.set(0, 0, child.getWidth(), child.getHeight());
        offsetDescendantRectToMyCoords(child, mTempRect);
        View directChild;
        if (child.getParent() == this) {
            directChild = child;
        } else {
            View v = child;
            ViewParent p = child.getParent();
            while (p != this) {
                v = (View) p;
                p = v.getParent();
            }
            directChild = v;
        }
        // ResolverDrawerLayout lays out vertically in child order;
        // the next view and forward is what to check against.
        int clipEdge = getHeight() - getPaddingBottom();
        final int childCount = getChildCount();
        for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
            final View nextChild = getChildAt(i);
            if (nextChild.getVisibility() == GONE) {
                continue;
            }
            clipEdge = Math.min(clipEdge, nextChild.getTop());
        }
        return mTempRect.bottom > clipEdge;
    }

    private static boolean isChildUnder(View child, float x, float y) {
        final float left = child.getX();
        final float top = child.getY();
        final float right = left + child.getWidth();
        final float bottom = top + child.getHeight();
        return x >= left && y >= top && x < right && y < bottom;
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        super.requestChildFocus(child, focused);
        if (!isInTouchMode() && isDescendantClipped(focused)) {
            smoothScrollTo(0, 0);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
        abortAnimation();
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
            if (child instanceof AbsListView) {
                mNestedScrollingChild = (AbsListView) child;
            }
            return true;
        }
        return false;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        super.onNestedScrollAccepted(child, target, axes);
    }

    @Override
    public void onStopNestedScroll(View child) {
        super.onStopNestedScroll(child);
        if (mScroller.isFinished()) {
            smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        if (dyUnconsumed < 0) {
            performDrag(-dyUnconsumed);
        }
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0) {
            consumed[1] = (int) -performDrag(-dy);
        }
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
            smoothScrollTo(0, velocityY);
            return true;
        }
        return false;
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
            if (getShowAtTop()) {
                if (isDismissable() && velocityY > 0) {
                    abortAnimation();
                    dismiss();
                } else {
                    smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
                }
            } else {
                if (isDismissable() && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
                    smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
                    mDismissOnScrollerFinished = true;
                } else {
                    smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
        if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
            return true;
        }
        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
            smoothScrollTo(0, 0);
            return true;
        }
        return false;
    }

    @Override
    public CharSequence getAccessibilityClreplacedName() {
        // Since we support scrolling, make this ViewGroup look like a
        // ScrollView. This is kind of a hack until we have support for
        // specifying auto-scroll behavior.
        return android.widget.ScrollView.clreplaced.getName();
    }

    /*@Override
    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfoInternal(info);

        if (isEnabled()) {
            if (mCollapseOffset != 0) {
                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
                info.setScrollable(true);
            }
        }

        // This view should never get accessibility focus, but it's interactive
        // via nested scrolling, so we can't hide it completely.
        info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
    }

    @Override
    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
        if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
            // This view should never get accessibility focus.
            return false;
        }

        if (super.performAccessibilityActionInternal(action, arguments)) {
            return true;
        }

        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && mCollapseOffset != 0) {
            smoothScrollTo(0, 0);
            return true;
        }

        return false;
    }*/
    @Override
    public void onDrawForeground(Canvas canvas) {
        if (mScrollIndicatorDrawable != null) {
            mScrollIndicatorDrawable.draw(canvas);
        }
        super.onDrawForeground(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
        int widthSize = sourceWidth;
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // Single-use layout; just ignore the mode and use available space.
        // Clamp to maxWidth.
        if (mMaxWidth >= 0) {
            widthSize = Math.min(widthSize, mMaxWidth);
        }
        final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
        final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
        // Currently we allot more height than is really needed so that the entirety of the
        // sheet may be pulled up.
        // TODO: Restrict the height here to be the right value.
        int heightUsed = 0;
        // Measure always-show children first.
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.alwaysShow && child.getVisibility() != GONE) {
                if (lp.maxHeight != -1) {
                    final int remainingHeight = heightSize - heightUsed;
                    measureChildWithMargins(child, widthSpec, 0, MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
                } else {
                    measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
                }
                heightUsed += child.getMeasuredHeight();
            }
        }
        mAlwaysShowHeight = heightUsed;
        // And now the rest.
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (!lp.alwaysShow && child.getVisibility() != GONE) {
                if (lp.maxHeight != -1) {
                    final int remainingHeight = heightSize - heightUsed;
                    measureChildWithMargins(child, widthSpec, 0, MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
                } else {
                    measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
                }
                heightUsed += child.getMeasuredHeight();
            }
        }
        final int oldCollapsibleHeight = mCollapsibleHeight;
        mCollapsibleHeight = Math.max(0, heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
        mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
        updateCollapseOffset(oldCollapsibleHeight, !isDragging());
        if (getShowAtTop()) {
            mTopOffset = 0;
        } else {
            mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
        }
        setMeasuredDimension(sourceWidth, heightSize);
    }

    /**
     * @return The space reserved by views with 'alwaysShow=true'
     */
    public int getAlwaysShowHeight() {
        return mAlwaysShowHeight;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = getWidth();
        View indicatorHost = null;
        int ypos = mTopOffset;
        int leftEdge = getPaddingLeft();
        int rightEdge = width - getPaddingRight();
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.hasNestedScrollIndicator) {
                indicatorHost = child;
            }
            if (child.getVisibility() == GONE) {
                continue;
            }
            int top = ypos + lp.topMargin;
            if (lp.ignoreOffset) {
                top -= mCollapseOffset;
            }
            final int bottom = top + child.getMeasuredHeight();
            final int childWidth = child.getMeasuredWidth();
            final int widthAvailable = rightEdge - leftEdge;
            final int left = leftEdge + (widthAvailable - childWidth) / 2;
            final int right = left + childWidth;
            child.layout(left, top, right, bottom);
            ypos = bottom + lp.bottomMargin;
        }
        if (mScrollIndicatorDrawable != null) {
            if (indicatorHost != null) {
                final int left = indicatorHost.getLeft();
                final int right = indicatorHost.getRight();
                final int bottom = indicatorHost.getTop();
                final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
                mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
                setWillNotDraw(!isCollapsed());
            } else {
                mScrollIndicatorDrawable = null;
                setWillNotDraw(true);
            }
        }
    }

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

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        if (p instanceof LayoutParams) {
            return new LayoutParams((LayoutParams) p);
        } else if (p instanceof MarginLayoutParams) {
            return new LayoutParams((MarginLayoutParams) p);
        }
        return new LayoutParams(p);
    }

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

    @Override
    protected Parcelable onSaveInstanceState() {
        final SavedState ss = new SavedState(super.onSaveInstanceState());
        ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        final SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        mOpenOnLayout = ss.open;
    }

    public static clreplaced LayoutParams extends MarginLayoutParams {

        public boolean alwaysShow;

        public boolean ignoreOffset;

        public boolean hasNestedScrollIndicator;

        public int maxHeight;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout_Layout);
            alwaysShow = a.getBoolean(R.styleable.ResolverDrawerLayout_Layout_layout_alwaysShow, false);
            ignoreOffset = a.getBoolean(R.styleable.ResolverDrawerLayout_Layout_layout_ignoreOffset, false);
            hasNestedScrollIndicator = a.getBoolean(R.styleable.ResolverDrawerLayout_Layout_layout_hasNestedScrollIndicator, false);
            maxHeight = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_Layout_layout_maxHeight, -1);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(LayoutParams source) {
            super(source);
            this.alwaysShow = source.alwaysShow;
            this.ignoreOffset = source.ignoreOffset;
            this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
            this.maxHeight = source.maxHeight;
        }

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

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

    static clreplaced SavedState extends BaseSavedState {

        boolean open;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            open = in.readInt() != 0;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(open ? 1 : 0);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    /**
     * Listener for sheet dismissed events.
     */
    public interface OnDismissedListener {

        /**
         * Callback when the sheet is dismissed by the user.
         */
        void onDismissed();
    }

    /**
     * Listener for sheet collapsed / expanded events.
     */
    public interface OnCollapsedChangedListener {

        /**
         * Callback when the sheet is either fully expanded or collapsed.
         *
         * @param isCollapsed true when collapsed, false when expanded.
         */
        void onCollapsedChanged(boolean isCollapsed);
    }

    private clreplaced RunOnDismissedListener implements Runnable {

        @Override
        public void run() {
            dispatchOnDismissed();
        }
    }
}

19 Source : SwipeMenuLayout.java
with MIT License
from Omega-R

public abstract clreplaced SwipeMenuLayout extends FrameLayout {

    public static final int DEFAULT_SCROLLER_DURATION = 250;

    public static final float DEFAULT_AUTO_OPEN_PERCENT = 0.5f;

    protected float mAutoOpenPercent = DEFAULT_AUTO_OPEN_PERCENT;

    protected int mScrollerDuration = DEFAULT_SCROLLER_DURATION;

    protected int mScaledTouchSlop;

    protected int mLastX;

    protected int mLastY;

    protected int mDownX;

    protected int mDownY;

    @Nullable
    protected View mContentView;

    @Nullable
    protected Swiper mBeginSwiper;

    @Nullable
    protected Swiper mEndSwiper;

    @Nullable
    protected Swiper mCurrentSwiper;

    protected boolean shouldResetSwiper;

    protected boolean mDragging;

    protected boolean swipeEnable = true;

    protected OverScroller mScroller;

    protected Interpolator mInterpolator;

    protected VelocityTracker mVelocityTracker;

    protected int mScaledMinimumFlingVelocity;

    protected int mScaledMaximumFlingVelocity;

    protected SwipeSwitchListener mSwipeSwitchListener;

    protected SwipeFractionListener mSwipeFractionListener;

    protected NumberFormat mDecimalFormat = new DecimalFormat("#.00", new DecimalFormatSymbols(Locale.US));

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

    public SwipeMenuLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    /**
     * Not in the place, the swipe menu is swiping
     * @return int the place or not
     */
    public abstract boolean isNotInPlace();

    public void init() {
        ViewConfiguration mViewConfig = ViewConfiguration.get(getContext());
        mScaledTouchSlop = mViewConfig.getScaledTouchSlop();
        mScroller = new OverScroller(getContext(), mInterpolator);
        mScaledMinimumFlingVelocity = mViewConfig.getScaledMinimumFlingVelocity();
        mScaledMaximumFlingVelocity = mViewConfig.getScaledMaximumFlingVelocity();
    }

    public void smoothOpenMenu(SwipeDirection direction) {
        switch(direction) {
            case LEFT:
                mCurrentSwiper = mBeginSwiper;
                break;
            case RIGHT:
                mCurrentSwiper = mEndSwiper;
                break;
        }
        if (mCurrentSwiper == null)
            throw new IllegalArgumentException("No menu!");
        smoothOpenMenu();
    }

    public void smoothCloseBeginMenu(SwipeDirection direction) {
        switch(direction) {
            case LEFT:
                mCurrentSwiper = mBeginSwiper;
                break;
            case RIGHT:
                mCurrentSwiper = mEndSwiper;
                break;
        }
        if (mCurrentSwiper == null)
            throw new IllegalArgumentException("No menu!");
        smoothCloseMenu();
    }

    public abstract void smoothOpenMenu(int duration);

    public void smoothOpenMenu() {
        smoothOpenMenu(mScrollerDuration);
    }

    public abstract void smoothCloseMenu(int duration);

    public void smoothCloseMenu() {
        smoothCloseMenu(mScrollerDuration);
    }

    public void setSwipeEnable(boolean swipeEnable) {
        this.swipeEnable = swipeEnable;
    }

    public boolean isSwipeEnable() {
        return swipeEnable;
    }

    public abstract void setSwipeEnable(SwipeDirection direction, boolean swipeEnable);

    public abstract boolean isSwipeEnable(SwipeDirection direction);

    public void setSwipeListener(SwipeSwitchListener swipeSwitchListener) {
        mSwipeSwitchListener = swipeSwitchListener;
    }

    public void setSwipeFractionListener(SwipeFractionListener swipeFractionListener) {
        mSwipeFractionListener = swipeFractionListener;
    }

    abstract int getMoveLen(MotionEvent event);

    abstract int getLen();

    /**
     * compute finish duration
     *
     * @param ev       up event
     * @param velocity velocity
     * @return finish duration
     */
    int getSwipeDuration(MotionEvent ev, int velocity) {
        int moveLen = getMoveLen(ev);
        final int len = getLen();
        final int halfLen = len / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(moveLen) / len);
        final float distance = halfLen + halfLen * distanceInfluenceForSnapDuration(distanceRatio);
        int duration;
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageDelta = (float) Math.abs(moveLen) / len;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, mScrollerDuration);
        return duration;
    }

    float distanceInfluenceForSnapDuration(float f) {
        // center the values about 0.
        f -= 0.5f;
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (isNotInPlace()) {
            smoothCloseMenu(0);
        }
    }
}

19 Source : Viewport.java
with Apache License 2.0
from niedev

/**
 * This is the default implementation for the viewport.
 * This implementation so for a normal viewport
 * where there is a horizontal x-axis and a
 * vertical y-axis.
 * This viewport is compatible with
 *  - {@link com.jjoe64.graphview.series.BarGraphSeries}
 *  - {@link com.jjoe64.graphview.series.LineGraphSeries}
 *  - {@link com.jjoe64.graphview.series.PointsGraphSeries}
 *
 * @author jjoe64
 */
public clreplaced Viewport {

    /**
     * this reference value is used to generate the
     * vertical labels. It is used when the y axis bounds
     * is set manual and humanRoundingY=false. it will be the minValueY value.
     */
    protected double referenceY = Double.NaN;

    /**
     * this reference value is used to generate the
     * horizontal labels. It is used when the x axis bounds
     * is set manual and humanRoundingX=false. it will be the minValueX value.
     */
    protected double referenceX = Double.NaN;

    /**
     * flag whether the vertical scaling is activated
     */
    protected boolean scalableY;

    /**
     * minimal viewport used for scaling and scrolling.
     * this is used if the data that is available is
     * less then the viewport that we want to be able to display.
     *
     * Double.NaN to disable this value
     */
    private RectD mMinimalViewport = new RectD(Double.NaN, Double.NaN, Double.NaN, Double.NaN);

    /**
     * the reference number to generate the labels
     * @return  by default 0, only when manual bounds and no human rounding
     *          is active, the min x value is returned
     */
    protected double getReferenceX() {
        // if the bounds is manual then we take the
        // original manual min y value as reference
        if (isXAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingX()) {
            if (Double.isNaN(referenceX)) {
                referenceX = getMinX(false);
            }
            return referenceX;
        } else {
            // starting from 0 so that the steps have nice numbers
            return 0;
        }
    }

    /**
     * listener to notify when x bounds changed after
     * scaling or scrolling.
     * This can be used to load more detailed data.
     */
    public interface OnXAxisBoundsChangedListener {

        /**
         * Called after scaling or scrolling with
         * the new bounds
         * @param minX min x value
         * @param maxX max x value
         */
        void onXAxisBoundsChanged(double minX, double maxX, Reason reason);

        public enum Reason {

            SCROLL, SCALE
        }
    }

    /**
     * listener for the scale gesture
     */
    private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {

        /**
         * called by android
         * @param detector detector
         * @return always true
         */
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // --- horizontal scaling ---
            double viewportWidth = mCurrentViewport.width();
            if (mMaxXAxisSize != 0) {
                if (viewportWidth > mMaxXAxisSize) {
                    viewportWidth = mMaxXAxisSize;
                }
            }
            double center = mCurrentViewport.left + viewportWidth / 2;
            float scaleSpanX;
            if (android.os.Build.VERSION.SDK_INT >= 11 && scalableY) {
                scaleSpanX = detector.getCurrentSpanX() / detector.getPreviousSpanX();
            } else {
                scaleSpanX = detector.getScaleFactor();
            }
            viewportWidth /= scaleSpanX;
            mCurrentViewport.left = center - viewportWidth / 2;
            mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
            // viewportStart must not be < minX
            double minX = getMinX(true);
            if (!Double.isNaN(mMinimalViewport.left)) {
                minX = Math.min(minX, mMinimalViewport.left);
            }
            if (mCurrentViewport.left < minX) {
                mCurrentViewport.left = minX;
                mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
            }
            // viewportStart + viewportSize must not be > maxX
            double maxX = getMaxX(true);
            if (!Double.isNaN(mMinimalViewport.right)) {
                maxX = Math.max(maxX, mMinimalViewport.right);
            }
            if (viewportWidth == 0) {
                mCurrentViewport.right = maxX;
            }
            double overlap = mCurrentViewport.left + viewportWidth - maxX;
            if (overlap > 0) {
                // scroll left
                if (mCurrentViewport.left - overlap > minX) {
                    mCurrentViewport.left -= overlap;
                    mCurrentViewport.right = mCurrentViewport.left + viewportWidth;
                } else {
                    // maximal scale
                    mCurrentViewport.left = minX;
                    mCurrentViewport.right = maxX;
                }
            }
            // --- vertical scaling ---
            if (scalableY && android.os.Build.VERSION.SDK_INT >= 11 && detector.getCurrentSpanY() != 0f && detector.getPreviousSpanY() != 0f) {
                boolean hreplacedecondScale = mGraphView.mSecondScale != null;
                double viewportHeight = mCurrentViewport.height() * -1;
                if (mMaxYAxisSize != 0) {
                    if (viewportHeight > mMaxYAxisSize) {
                        viewportHeight = mMaxYAxisSize;
                    }
                }
                center = mCurrentViewport.bottom + viewportHeight / 2;
                viewportHeight /= detector.getCurrentSpanY() / detector.getPreviousSpanY();
                mCurrentViewport.bottom = center - viewportHeight / 2;
                mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
                // ignore bounds when second scale
                if (!hreplacedecondScale) {
                    // viewportStart must not be < minY
                    double minY = getMinY(true);
                    if (!Double.isNaN(mMinimalViewport.bottom)) {
                        minY = Math.min(minY, mMinimalViewport.bottom);
                    }
                    if (mCurrentViewport.bottom < minY) {
                        mCurrentViewport.bottom = minY;
                        mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
                    }
                    // viewportStart + viewportSize must not be > maxY
                    double maxY = getMaxY(true);
                    if (!Double.isNaN(mMinimalViewport.top)) {
                        maxY = Math.max(maxY, mMinimalViewport.top);
                    }
                    if (viewportHeight == 0) {
                        mCurrentViewport.top = maxY;
                    }
                    overlap = mCurrentViewport.bottom + viewportHeight - maxY;
                    if (overlap > 0) {
                        // scroll left
                        if (mCurrentViewport.bottom - overlap > minY) {
                            mCurrentViewport.bottom -= overlap;
                            mCurrentViewport.top = mCurrentViewport.bottom + viewportHeight;
                        } else {
                            // maximal scale
                            mCurrentViewport.bottom = minY;
                            mCurrentViewport.top = maxY;
                        }
                    }
                } else {
                    // ---- second scale ---
                    viewportHeight = mGraphView.mSecondScale.mCurrentViewport.height() * -1;
                    center = mGraphView.mSecondScale.mCurrentViewport.bottom + viewportHeight / 2;
                    viewportHeight /= detector.getCurrentSpanY() / detector.getPreviousSpanY();
                    mGraphView.mSecondScale.mCurrentViewport.bottom = center - viewportHeight / 2;
                    mGraphView.mSecondScale.mCurrentViewport.top = mGraphView.mSecondScale.mCurrentViewport.bottom + viewportHeight;
                }
            }
            // adjustSteps viewport, labels, etc.
            mGraphView.onDataChanged(true, false);
            ViewCompat.postInvalidateOnAnimation(mGraphView);
            return true;
        }

        /**
         * called when scaling begins
         *
         * @param detector detector
         * @return true if it is scalable
         */
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            // cursor mode
            if (mGraphView.isCursorMode()) {
                return false;
            }
            if (mIsScalable) {
                mScalingActive = true;
                return true;
            } else {
                return false;
            }
        }

        /**
         * called when sacling ends
         * This will re-adjustSteps the viewport.
         *
         * @param detector detector
         */
        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            mScalingActive = false;
            // notify
            if (mOnXAxisBoundsChangedListener != null) {
                mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCALE);
            }
            ViewCompat.postInvalidateOnAnimation(mGraphView);
        }
    };

    /**
     * simple gesture listener to track scroll events
     */
    private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {

        @Override
        public boolean onDown(MotionEvent e) {
            // cursor mode
            if (mGraphView.isCursorMode()) {
                return true;
            }
            if (!mIsScrollable || mScalingActive)
                return false;
            // Initiates the decay phase of any active edge effects.
            releaseEdgeEffects();
            // Aborts any active scroll animations and invalidates.
            mScroller.forceFinished(true);
            ViewCompat.postInvalidateOnAnimation(mGraphView);
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // cursor mode
            if (mGraphView.isCursorMode()) {
                return true;
            }
            if (!mIsScrollable || mScalingActive)
                return false;
            // Scrolling uses math based on the viewport (as opposed to math using pixels).
            /**
             * Pixel offset is the offset in screen pixels, while viewport offset is the
             * offset within the current viewport. For additional information on surface sizes
             * and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
             * additional information about the viewport, see the comments for
             * {@link mCurrentViewport}.
             */
            double viewportOffsetX = distanceX * mCurrentViewport.width() / mGraphView.getGraphContentWidth();
            double viewportOffsetY = distanceY * mCurrentViewport.height() / mGraphView.getGraphContentHeight();
            // respect minimal viewport
            double completeRangeLeft = mCompleteRange.left;
            if (!Double.isNaN(mMinimalViewport.left)) {
                completeRangeLeft = Math.min(completeRangeLeft, mMinimalViewport.left);
            }
            double completeRangeRight = mCompleteRange.right;
            if (!Double.isNaN(mMinimalViewport.right)) {
                completeRangeRight = Math.max(completeRangeRight, mMinimalViewport.right);
            }
            double completeRangeWidth = completeRangeRight - completeRangeLeft;
            double completeRangeBottom = mCompleteRange.bottom;
            if (!Double.isNaN(mMinimalViewport.bottom)) {
                completeRangeBottom = Math.min(completeRangeBottom, mMinimalViewport.bottom);
            }
            double completeRangeTop = mCompleteRange.top;
            if (!Double.isNaN(mMinimalViewport.top)) {
                completeRangeTop = Math.max(completeRangeTop, mMinimalViewport.top);
            }
            double completeRangeHeight = completeRangeTop - completeRangeBottom;
            int completeWidth = (int) ((completeRangeWidth / mCurrentViewport.width()) * (double) mGraphView.getGraphContentWidth());
            int completeHeight = (int) ((completeRangeHeight / mCurrentViewport.height()) * (double) mGraphView.getGraphContentHeight());
            int scrolledX = (int) (completeWidth * (mCurrentViewport.left + viewportOffsetX - completeRangeLeft) / completeRangeWidth);
            int scrolledY = (int) (completeHeight * (mCurrentViewport.bottom + viewportOffsetY - completeRangeBottom) / completeRangeHeight * -1);
            boolean canScrollX = mCurrentViewport.left > completeRangeLeft || mCurrentViewport.right < completeRangeRight;
            boolean canScrollY = mCurrentViewport.bottom > completeRangeBottom || mCurrentViewport.top < completeRangeTop;
            boolean hreplacedecondScale = mGraphView.mSecondScale != null;
            // second scale
            double viewportOffsetY2 = 0d;
            if (hreplacedecondScale) {
                viewportOffsetY2 = distanceY * mGraphView.mSecondScale.mCurrentViewport.height() / mGraphView.getGraphContentHeight();
                canScrollY |= mGraphView.mSecondScale.mCurrentViewport.bottom > mGraphView.mSecondScale.mCompleteRange.bottom || mGraphView.mSecondScale.mCurrentViewport.top < mGraphView.mSecondScale.mCompleteRange.top;
            }
            canScrollY &= scrollableY;
            if (canScrollX) {
                if (viewportOffsetX < 0) {
                    double tooMuch = mCurrentViewport.left + viewportOffsetX - completeRangeLeft;
                    if (tooMuch < 0) {
                        viewportOffsetX -= tooMuch;
                    }
                } else {
                    double tooMuch = mCurrentViewport.right + viewportOffsetX - completeRangeRight;
                    if (tooMuch > 0) {
                        viewportOffsetX -= tooMuch;
                    }
                }
                mCurrentViewport.left += viewportOffsetX;
                mCurrentViewport.right += viewportOffsetX;
                // notify
                if (mOnXAxisBoundsChangedListener != null) {
                    mOnXAxisBoundsChangedListener.onXAxisBoundsChanged(getMinX(false), getMaxX(false), OnXAxisBoundsChangedListener.Reason.SCROLL);
                }
            }
            if (canScrollY) {
                // if we have the second axis we ignore the max/min range
                if (!hreplacedecondScale) {
                    if (viewportOffsetY < 0) {
                        double tooMuch = mCurrentViewport.bottom + viewportOffsetY - completeRangeBottom;
                        if (tooMuch < 0) {
                            viewportOffsetY -= tooMuch;
                        }
                    } else {
                        double tooMuch = mCurrentViewport.top + viewportOffsetY - completeRangeTop;
                        if (tooMuch > 0) {
                            viewportOffsetY -= tooMuch;
                        }
                    }
                }
                mCurrentViewport.top += viewportOffsetY;
                mCurrentViewport.bottom += viewportOffsetY;
                // second scale
                if (hreplacedecondScale) {
                    mGraphView.mSecondScale.mCurrentViewport.top += viewportOffsetY2;
                    mGraphView.mSecondScale.mCurrentViewport.bottom += viewportOffsetY2;
                }
            }
            if (canScrollX && scrolledX < 0) {
                mEdgeEffectLeft.onPull(scrolledX / (float) mGraphView.getGraphContentWidth());
            }
            if (!hreplacedecondScale && canScrollY && scrolledY < 0) {
                mEdgeEffectBottom.onPull(scrolledY / (float) mGraphView.getGraphContentHeight());
            }
            if (canScrollX && scrolledX > completeWidth - mGraphView.getGraphContentWidth()) {
                mEdgeEffectRight.onPull((scrolledX - completeWidth + mGraphView.getGraphContentWidth()) / (float) mGraphView.getGraphContentWidth());
            }
            if (!hreplacedecondScale && canScrollY && scrolledY > completeHeight - mGraphView.getGraphContentHeight()) {
                mEdgeEffectTop.onPull((scrolledY - completeHeight + mGraphView.getGraphContentHeight()) / (float) mGraphView.getGraphContentHeight());
            }
            // adjustSteps viewport, labels, etc.
            mGraphView.onDataChanged(true, false);
            ViewCompat.postInvalidateOnAnimation(mGraphView);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // fling((int) -velocityX, (int) -velocityY);
            return true;
        }
    };

    /**
     * the state of the axis bounds
     */
    public enum AxisBoundsStatus {

        /**
         * initial means that the bounds gets
         * auto adjusted if they are not manual.
         * After adjusting the status comes to
         * #AUTO_ADJUSTED.
         */
        INITIAL,
        /**
         * after the bounds got auto-adjusted,
         * this status will set.
         */
        AUTO_ADJUSTED,
        /**
         * means that the bounds are fix (manually) and
         * are not to be auto-adjusted.
         */
        FIX
    }

    /**
     * paint to draw background
     */
    private Paint mPaint;

    /**
     * reference to the graphview
     */
    private final GraphView mGraphView;

    /**
     * this holds the current visible viewport
     * left = minX, right = maxX
     * bottom = minY, top = maxY
     */
    protected RectD mCurrentViewport = new RectD();

    /**
     * maximum allowed viewport size (horizontal)
     * 0 means use the bounds of the actual data that is
     * available
     */
    protected double mMaxXAxisSize = 0;

    /**
     * maximum allowed viewport size (vertical)
     * 0 means use the bounds of the actual data that is
     * available
     */
    protected double mMaxYAxisSize = 0;

    /**
     * this holds the whole range of the data
     * left = minX, right = maxX
     * bottom = minY, top = maxY
     */
    protected RectD mCompleteRange = new RectD();

    /**
     * flag whether scaling is currently active
     */
    protected boolean mScalingActive;

    /**
     * flag whether the viewport is scrollable
     */
    private boolean mIsScrollable;

    /**
     * flag whether the viewport is scalable
     */
    private boolean mIsScalable;

    /**
     * flag whether the viewport is scalable
     * on the Y axis
     */
    private boolean scrollableY;

    /**
     * gesture detector to detect scrolling
     */
    protected GestureDetector mGestureDetector;

    /**
     * detect scaling
     */
    protected ScaleGestureDetector mScaleGestureDetector;

    /**
     * not used - for fling
     */
    protected OverScroller mScroller;

    /**
     * not used
     */
    private EdgeEffectCompat mEdgeEffectTop;

    /**
     * not used
     */
    private EdgeEffectCompat mEdgeEffectBottom;

    /**
     * glow effect when scrolling left
     */
    private EdgeEffectCompat mEdgeEffectLeft;

    /**
     * glow effect when scrolling right
     */
    private EdgeEffectCompat mEdgeEffectRight;

    /**
     * state of the x axis
     */
    protected AxisBoundsStatus mXAxisBoundsStatus;

    /**
     * state of the y axis
     */
    protected AxisBoundsStatus mYAxisBoundsStatus;

    /**
     * flag whether the x axis bounds are manual
     */
    private boolean mXAxisBoundsManual;

    /**
     * flag whether the y axis bounds are manual
     */
    private boolean mYAxisBoundsManual;

    /**
     * background color of the viewport area
     * it is recommended to use a semi-transparent color
     */
    private int mBackgroundColor;

    /**
     * listener to notify when x bounds changed after
     * scaling or scrolling.
     * This can be used to load more detailed data.
     */
    protected OnXAxisBoundsChangedListener mOnXAxisBoundsChangedListener;

    /**
     * optional draw a border between the labels
     * and the viewport
     */
    private boolean mDrawBorder;

    /**
     * color of the border
     * @see #setDrawBorder(boolean)
     */
    private Integer mBorderColor;

    /**
     * custom paint to use for the border
     * @see #setDrawBorder(boolean)
     */
    private Paint mBorderPaint;

    /**
     * creates the viewport
     *
     * @param graphView graphview
     */
    Viewport(GraphView graphView) {
        mScroller = new OverScroller(graphView.getContext());
        mEdgeEffectTop = new EdgeEffectCompat(graphView.getContext());
        mEdgeEffectBottom = new EdgeEffectCompat(graphView.getContext());
        mEdgeEffectLeft = new EdgeEffectCompat(graphView.getContext());
        mEdgeEffectRight = new EdgeEffectCompat(graphView.getContext());
        mGestureDetector = new GestureDetector(graphView.getContext(), mGestureListener);
        mScaleGestureDetector = new ScaleGestureDetector(graphView.getContext(), mScaleGestureListener);
        mGraphView = graphView;
        mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        mBackgroundColor = Color.TRANSPARENT;
        mPaint = new Paint();
    }

    /**
     * will be called on a touch event.
     * needed to use scaling and scrolling
     *
     * @param event
     * @return true if it was consumed
     */
    public boolean onTouchEvent(MotionEvent event) {
        boolean b = mScaleGestureDetector.onTouchEvent(event);
        b |= mGestureDetector.onTouchEvent(event);
        if (mGraphView.isCursorMode()) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                mGraphView.getCursorMode().onDown(event);
                b |= true;
            }
            if (event.getAction() == MotionEvent.ACTION_MOVE) {
                mGraphView.getCursorMode().onMove(event);
                b |= true;
            }
            if (event.getAction() == MotionEvent.ACTION_UP) {
                b |= mGraphView.getCursorMode().onUp(event);
            }
        }
        return b;
    }

    /**
     * change the state of the x axis.
     * normally you do not call this method.
     * If you want to set manual axis use
     * {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
     *
     * @param s state
     */
    public void setXAxisBoundsStatus(AxisBoundsStatus s) {
        mXAxisBoundsStatus = s;
    }

    /**
     * change the state of the y axis.
     * normally you do not call this method.
     * If you want to set manual axis use
     * {@link #setXAxisBoundsManual(boolean)} and {@link #setYAxisBoundsManual(boolean)}
     *
     * @param s state
     */
    public void setYAxisBoundsStatus(AxisBoundsStatus s) {
        mYAxisBoundsStatus = s;
    }

    /**
     * @return whether the viewport is scrollable
     */
    public boolean isScrollable() {
        return mIsScrollable;
    }

    /**
     * @param mIsScrollable whether is viewport is scrollable
     */
    public void setScrollable(boolean mIsScrollable) {
        this.mIsScrollable = mIsScrollable;
    }

    /**
     * @return the x axis state
     */
    public AxisBoundsStatus getXAxisBoundsStatus() {
        return mXAxisBoundsStatus;
    }

    /**
     * @return the y axis state
     */
    public AxisBoundsStatus getYAxisBoundsStatus() {
        return mYAxisBoundsStatus;
    }

    /**
     * caches the complete range (minX, maxX, minY, maxY)
     * by iterating all series and all datapoints and
     * stores it into #mCompleteRange
     *
     * for the x-range it will respect the series on the
     * second scale - not for y-values
     */
    public void calcCompleteRange() {
        List<Series> series = mGraphView.getSeries();
        List<Series> seriesInclusiveSecondScale = new ArrayList<>(mGraphView.getSeries());
        if (mGraphView.mSecondScale != null) {
            seriesInclusiveSecondScale.addAll(mGraphView.mSecondScale.getSeries());
        }
        mCompleteRange.set(0d, 0d, 0d, 0d);
        if (!seriesInclusiveSecondScale.isEmpty() && !seriesInclusiveSecondScale.get(0).isEmpty()) {
            double d = seriesInclusiveSecondScale.get(0).getLowestValueX();
            for (Series s : seriesInclusiveSecondScale) {
                if (!s.isEmpty() && d > s.getLowestValueX()) {
                    d = s.getLowestValueX();
                }
            }
            mCompleteRange.left = d;
            d = seriesInclusiveSecondScale.get(0).getHighestValueX();
            for (Series s : seriesInclusiveSecondScale) {
                if (!s.isEmpty() && d < s.getHighestValueX()) {
                    d = s.getHighestValueX();
                }
            }
            mCompleteRange.right = d;
            if (!series.isEmpty() && !series.get(0).isEmpty()) {
                d = series.get(0).getLowestValueY();
                for (Series s : series) {
                    if (!s.isEmpty() && d > s.getLowestValueY()) {
                        d = s.getLowestValueY();
                    }
                }
                mCompleteRange.bottom = d;
                d = series.get(0).getHighestValueY();
                for (Series s : series) {
                    if (!s.isEmpty() && d < s.getHighestValueY()) {
                        d = s.getHighestValueY();
                    }
                }
                mCompleteRange.top = d;
            }
        }
        // calc current viewport bounds
        if (mYAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
            mYAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        }
        if (mYAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
            mCurrentViewport.top = mCompleteRange.top;
            mCurrentViewport.bottom = mCompleteRange.bottom;
        }
        if (mXAxisBoundsStatus == AxisBoundsStatus.AUTO_ADJUSTED) {
            mXAxisBoundsStatus = AxisBoundsStatus.INITIAL;
        }
        if (mXAxisBoundsStatus == AxisBoundsStatus.INITIAL) {
            mCurrentViewport.left = mCompleteRange.left;
            mCurrentViewport.right = mCompleteRange.right;
        } else if (mXAxisBoundsManual && !mYAxisBoundsManual && mCompleteRange.width() != 0) {
            // get highest/lowest of current viewport
            // lowest
            double d = Double.MAX_VALUE;
            for (Series s : series) {
                Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
                while (values.hasNext()) {
                    double v = values.next().getY();
                    if (d > v) {
                        d = v;
                    }
                }
            }
            if (d != Double.MAX_VALUE) {
                mCurrentViewport.bottom = d;
            }
            // highest
            d = Double.MIN_VALUE;
            for (Series s : series) {
                Iterator<DataPointInterface> values = s.getValues(mCurrentViewport.left, mCurrentViewport.right);
                while (values.hasNext()) {
                    double v = values.next().getY();
                    if (d < v) {
                        d = v;
                    }
                }
            }
            if (d != Double.MIN_VALUE) {
                mCurrentViewport.top = d;
            }
        }
        // fixes blank screen when range is zero
        if (mCurrentViewport.left == mCurrentViewport.right)
            mCurrentViewport.right++;
        if (mCurrentViewport.top == mCurrentViewport.bottom)
            mCurrentViewport.top++;
    }

    /**
     * @param completeRange     if true => minX of the complete range of all series
     *                          if false => minX of the current visible viewport
     * @return the min x value
     */
    public double getMinX(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.left;
        } else {
            return mCurrentViewport.left;
        }
    }

    /**
     * @param completeRange     if true => maxX of the complete range of all series
     *                          if false => maxX of the current visible viewport
     * @return the max x value
     */
    public double getMaxX(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.right;
        } else {
            return mCurrentViewport.right;
        }
    }

    /**
     * @param completeRange     if true => minY of the complete range of all series
     *                          if false => minY of the current visible viewport
     * @return the min y value
     */
    public double getMinY(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.bottom;
        } else {
            return mCurrentViewport.bottom;
        }
    }

    /**
     * @param completeRange     if true => maxY of the complete range of all series
     *                          if false => maxY of the current visible viewport
     * @return the max y value
     */
    public double getMaxY(boolean completeRange) {
        if (completeRange) {
            return mCompleteRange.top;
        } else {
            return mCurrentViewport.top;
        }
    }

    /**
     * set the maximal y value for the current viewport.
     * Make sure to set the y bounds to manual via
     * {@link #setYAxisBoundsManual(boolean)}
     * @param y max / highest value
     */
    public void setMaxY(double y) {
        mCurrentViewport.top = y;
    }

    /**
     * set the minimal y value for the current viewport.
     * Make sure to set the y bounds to manual via
     * {@link #setYAxisBoundsManual(boolean)}
     * @param y min / lowest value
     */
    public void setMinY(double y) {
        mCurrentViewport.bottom = y;
    }

    /**
     * set the maximal x value for the current viewport.
     * Make sure to set the x bounds to manual via
     * {@link #setXAxisBoundsManual(boolean)}
     * @param x max / highest value
     */
    public void setMaxX(double x) {
        mCurrentViewport.right = x;
    }

    /**
     * set the minimal x value for the current viewport.
     * Make sure to set the x bounds to manual via
     * {@link #setXAxisBoundsManual(boolean)}
     * @param x min / lowest value
     */
    public void setMinX(double x) {
        mCurrentViewport.left = x;
    }

    /**
     * release the glowing effects
     */
    private void releaseEdgeEffects() {
        mEdgeEffectLeft.onRelease();
        mEdgeEffectRight.onRelease();
        mEdgeEffectTop.onRelease();
        mEdgeEffectBottom.onRelease();
    }

    /**
     * not used currently
     *
     * @param velocityX
     * @param velocityY
     */
    private void fling(int velocityX, int velocityY) {
        velocityY = 0;
        releaseEdgeEffects();
        // Flings use math in pixels (as opposed to math based on the viewport).
        int maxX = (int) ((mCurrentViewport.width() / mCompleteRange.width()) * (float) mGraphView.getGraphContentWidth()) - mGraphView.getGraphContentWidth();
        int maxY = (int) ((mCurrentViewport.height() / mCompleteRange.height()) * (float) mGraphView.getGraphContentHeight()) - mGraphView.getGraphContentHeight();
        int startX = (int) ((mCurrentViewport.left - mCompleteRange.left) / mCompleteRange.width()) * maxX;
        int startY = (int) ((mCurrentViewport.top - mCompleteRange.top) / mCompleteRange.height()) * maxY;
        mScroller.forceFinished(true);
        mScroller.fling(startX, startY, velocityX, velocityY, 0, maxX, 0, maxY, mGraphView.getGraphContentWidth() / 2, mGraphView.getGraphContentHeight() / 2);
        ViewCompat.postInvalidateOnAnimation(mGraphView);
    }

    /**
     * not used currently
     */
    public void computeScroll() {
    }

    /**
     * Draws the overscroll "glow" at the four edges of the chart region, if necessary.
     *
     * @see EdgeEffectCompat
     */
    private void drawEdgeEffectsUnclipped(Canvas canvas) {
        // The methods below rotate and translate the canvas as needed before drawing the glow,
        // since EdgeEffectCompat always draws a top-glow at 0,0.
        boolean needsInvalidate = false;
        if (!mEdgeEffectTop.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop());
            mEdgeEffectTop.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
            if (mEdgeEffectTop.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeEffectBottom.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
            canvas.rotate(180, mGraphView.getGraphContentWidth() / 2, 0);
            mEdgeEffectBottom.setSize(mGraphView.getGraphContentWidth(), mGraphView.getGraphContentHeight());
            if (mEdgeEffectBottom.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeEffectLeft.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight());
            canvas.rotate(-90, 0, 0);
            mEdgeEffectLeft.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
            if (mEdgeEffectLeft.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeEffectRight.isFinished()) {
            final int restoreCount = canvas.save();
            canvas.translate(mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop());
            canvas.rotate(90, 0, 0);
            mEdgeEffectRight.setSize(mGraphView.getGraphContentHeight(), mGraphView.getGraphContentWidth());
            if (mEdgeEffectRight.draw(canvas)) {
                needsInvalidate = true;
            }
            canvas.restoreToCount(restoreCount);
        }
        if (needsInvalidate) {
            ViewCompat.postInvalidateOnAnimation(mGraphView);
        }
    }

    /**
     * will be first called in order to draw
     * the canvas
     * Used to draw the background
     *
     * @param c canvas.
     */
    public void drawFirst(Canvas c) {
        // draw background
        if (mBackgroundColor != Color.TRANSPARENT) {
            mPaint.setColor(mBackgroundColor);
            c.drawRect(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mPaint);
        }
        if (mDrawBorder) {
            Paint p;
            if (mBorderPaint != null) {
                p = mBorderPaint;
            } else {
                p = mPaint;
                p.setColor(getBorderColor());
            }
            c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
            c.drawLine(mGraphView.getGraphContentLeft(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
            // on the right side if we have second scale
            if (mGraphView.mSecondScale != null) {
                c.drawLine(mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop(), mGraphView.getGraphContentLeft() + mGraphView.getGraphContentWidth(), mGraphView.getGraphContentTop() + mGraphView.getGraphContentHeight(), p);
            }
        }
    }

    /**
     * draws the glowing edge effect
     *
     * @param c canvas
     */
    public void draw(Canvas c) {
        drawEdgeEffectsUnclipped(c);
    }

    /**
     * @return background of the viewport area
     */
    public int getBackgroundColor() {
        return mBackgroundColor;
    }

    /**
     * @param mBackgroundColor  background of the viewport area
     *                          use transparent to have no background
     */
    public void setBackgroundColor(int mBackgroundColor) {
        this.mBackgroundColor = mBackgroundColor;
    }

    /**
     * @return whether the viewport is scalable
     */
    public boolean isScalable() {
        return mIsScalable;
    }

    /**
     * active the scaling/zooming feature
     * notice: sets the x axis bounds to manual
     *
     * @param mIsScalable whether the viewport is scalable
     */
    public void setScalable(boolean mIsScalable) {
        this.mIsScalable = mIsScalable;
        if (mIsScalable) {
            mIsScrollable = true;
            // set viewport to manual
            setXAxisBoundsManual(true);
        }
    }

    /**
     * @return whether the x axis bounds are manual.
     * @see #setMinX(double)
     * @see #setMaxX(double)
     */
    public boolean isXAxisBoundsManual() {
        return mXAxisBoundsManual;
    }

    /**
     * @param mXAxisBoundsManual whether the x axis bounds are manual.
     * @see #setMinX(double)
     * @see #setMaxX(double)
     */
    public void setXAxisBoundsManual(boolean mXAxisBoundsManual) {
        this.mXAxisBoundsManual = mXAxisBoundsManual;
        if (mXAxisBoundsManual) {
            mXAxisBoundsStatus = AxisBoundsStatus.FIX;
        }
    }

    /**
     * @return whether the y axis bound are manual
     */
    public boolean isYAxisBoundsManual() {
        return mYAxisBoundsManual;
    }

    /**
     * @param mYAxisBoundsManual whether the y axis bounds are manual
     * @see #setMaxY(double)
     * @see #setMinY(double)
     */
    public void setYAxisBoundsManual(boolean mYAxisBoundsManual) {
        this.mYAxisBoundsManual = mYAxisBoundsManual;
        if (mYAxisBoundsManual) {
            mYAxisBoundsStatus = AxisBoundsStatus.FIX;
        }
    }

    /**
     * forces the viewport to scroll to the end
     * of the range by keeping the current viewport size.
     *
     * Important: Only takes effect if x axis bounds are manual.
     *
     * @see #setXAxisBoundsManual(boolean)
     */
    public void scrollToEnd() {
        if (mXAxisBoundsManual) {
            double size = mCurrentViewport.width();
            mCurrentViewport.right = mCompleteRange.right;
            mCurrentViewport.left = mCompleteRange.right - size;
            mGraphView.onDataChanged(true, false);
        } else {
            Log.w("GraphView", "scrollToEnd works only with manual x axis bounds");
        }
    }

    /**
     * @return the listener when there is one registered.
     */
    public OnXAxisBoundsChangedListener getOnXAxisBoundsChangedListener() {
        return mOnXAxisBoundsChangedListener;
    }

    /**
     * set a listener to notify when x bounds changed after
     * scaling or scrolling.
     * This can be used to load more detailed data.
     *
     * @param l the listener to use
     */
    public void setOnXAxisBoundsChangedListener(OnXAxisBoundsChangedListener l) {
        mOnXAxisBoundsChangedListener = l;
    }

    /**
     * optional draw a border between the labels
     * and the viewport
     *
     * @param drawBorder true to draw the border
     */
    public void setDrawBorder(boolean drawBorder) {
        this.mDrawBorder = drawBorder;
    }

    /**
     * the border color used. will be ignored when
     * a custom paint is set.
     *
     * @see #setDrawBorder(boolean)
     * @return border color. by default the grid color is used
     */
    public int getBorderColor() {
        if (mBorderColor != null) {
            return mBorderColor;
        }
        return mGraphView.getGridLabelRenderer().getGridColor();
    }

    /**
     * the border color used. will be ignored when
     * a custom paint is set.
     *
     * @param borderColor null to reset
     */
    public void setBorderColor(Integer borderColor) {
        this.mBorderColor = borderColor;
    }

    /**
     * custom paint to use for the border. border color
     * will be ignored
     *
     * @see #setDrawBorder(boolean)
     * @param borderPaint
     */
    public void setBorderPaint(Paint borderPaint) {
        this.mBorderPaint = borderPaint;
    }

    /**
     * activate/deactivate the vertical scrolling
     *
     * @param scrollableY true to activate
     */
    public void setScrollableY(boolean scrollableY) {
        this.scrollableY = scrollableY;
    }

    /**
     * the reference number to generate the labels
     * @return  by default 0, only when manual bounds and no human rounding
     *          is active, the min y value is returned
     */
    protected double getReferenceY() {
        // if the bounds is manual then we take the
        // original manual min y value as reference
        if (isYAxisBoundsManual() && !mGraphView.getGridLabelRenderer().isHumanRoundingY()) {
            if (Double.isNaN(referenceY)) {
                referenceY = getMinY(false);
            }
            return referenceY;
        } else {
            // starting from 0 so that the steps have nice numbers
            return 0;
        }
    }

    /**
     * activate or deactivate the vertical zooming/scaling functionallity.
     * This will automatically activate the vertical scrolling and the
     * horizontal scaling/scrolling feature.
     *
     * @param scalableY true to activate
     */
    public void setScalableY(boolean scalableY) {
        if (scalableY) {
            this.scrollableY = true;
            setScalable(true);
            if (android.os.Build.VERSION.SDK_INT < 11) {
                Log.w("GraphView", "Vertical scaling requires minimum Android 3.0 (API Level 11)");
            }
        }
        this.scalableY = scalableY;
    }

    /**
     * maximum allowed viewport size (horizontal)
     * 0 means use the bounds of the actual data that is
     * available
     */
    public double getMaxXAxisSize() {
        return mMaxXAxisSize;
    }

    /**
     * maximum allowed viewport size (vertical)
     * 0 means use the bounds of the actual data that is
     * available
     */
    public double getMaxYAxisSize() {
        return mMaxYAxisSize;
    }

    /**
     * Set the max viewport size (horizontal)
     * This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
     * could force the user to only be able to see 2 hours of data at a time.
     * Default value is 0 (disabled)
     *
     * @param mMaxXAxisViewportSize maximum size of viewport
     */
    public void setMaxXAxisSize(double mMaxXAxisViewportSize) {
        this.mMaxXAxisSize = mMaxXAxisViewportSize;
    }

    /**
     * Set the max viewport size (vertical)
     * This can prevent the user from zooming out too much. E.g. with a 24 hours graph, it
     * could force the user to only be able to see 2 hours of data at a time.
     * Default value is 0 (disabled)
     *
     * @param mMaxYAxisViewportSize maximum size of viewport
     */
    public void setMaxYAxisSize(double mMaxYAxisViewportSize) {
        this.mMaxYAxisSize = mMaxYAxisViewportSize;
    }

    /**
     * minimal viewport used for scaling and scrolling.
     * this is used if the data that is available is
     * less then the viewport that we want to be able to display.
     *
     * if Double.NaN is used, then this value is ignored
     *
     * @param minX
     * @param maxX
     * @param minY
     * @param maxY
     */
    public void setMinimalViewport(double minX, double maxX, double minY, double maxY) {
        mMinimalViewport.set(minX, maxY, maxX, minY);
    }
}

19 Source : OverScrollerCompat.java
with Apache License 2.0
from niedev

/**
 * @see android.view.ScaleGestureDetector#getCurrentSpanY()
 */
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static float getCurrVelocity(OverScroller overScroller) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        return overScroller.getCurrVelocity();
    } else {
        return 0;
    }
}

19 Source : LinkageScrollLayout.java
with Apache License 2.0
from MFC-TEC

/**
 * Layout container for two view(TopView and BottomView) that can be scrolled by user,
 * allowing the total height of two child to be larger than the physical display.
 * LinkageScrollLayout is a {@link ViewGroup}, which place two View in it,
 * <P>Children in the container must implement ILinkageScrollHandler{@link ILinkageScrollHandler}, or
 * the layout container will not work properly.</P>
 *
 * @author lorienzhang
 */
public clreplaced LinkageScrollLayout extends ViewGroup {

    public static final String TAG = "LinkageScrollLayout";

    /**
     * multi finger touch
     */
    private int mActivePointerId = INVALID_POINTER;

    /**
     * top view,first child view
     */
    private View mTopView;

    /**
     * bottom view,second child view
     */
    private View mBottomView;

    /**
     * top view,scroll handler
     */
    private LinkageScrollHandler mTopHandler;

    /**
     * bottom view,scroll handler
     */
    private LinkageScrollHandler mBottomHandler;

    private int mLastScrollY;

    private PosIndicator mPosIndicator;

    private int mTopViewHeight;

    private int mBottomViewHeight;

    private int mVisualHeight;

    private int mHeight;

    /**
     * Last MotionEvent
     */
    private MotionEvent mLastMotionEvent;

    private boolean mHreplacedendCancelEvent;

    private OverScroller mScroller;

    /**
     * to track child view's velocity
     */
    private OverScroller mTrackScroller;

    private int mTouchSlop;

    private VelocityTracker mVelocityTracker;

    private int mMaximumVelocity, mMinimumVelocity;

    private boolean isControl;

    /**
     * scroll listener
     */
    private LinkageScrollListenerHolder mLinkageScrollListener = LinkageScrollListenerHolder.create();

    private LinkageScrollEvent mBottomViewScrollEvent = new LinkageScrollEvent() {

        @Override
        public void onContentScrollToTop() {
            if (!mPosIndicator.isInStartPos()) {
                return;
            }
            if (mTrackScroller.computeScrollOffset()) {
                int curVelocity = (int) mTrackScroller.getCurrVelocity();
                fling(curVelocity);
                mTrackScroller.abortAnimation();
            }
        }

        @Override
        public void onContentScrollToBottom() {
        }
    };

    private LinkageScrollEvent mTopViewScrollEvent = new LinkageScrollEvent() {

        @Override
        public void onContentScrollToTop() {
        }

        @Override
        public void onContentScrollToBottom() {
            if (!mPosIndicator.isInEndPos()) {
                return;
            }
            if (mTrackScroller.computeScrollOffset()) {
                int curVelocity = (int) mTrackScroller.getCurrVelocity();
                fling(-curVelocity);
                mTrackScroller.abortAnimation();
            }
        }
    };

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

    public LinkageScrollLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(Context context) {
        mPosIndicator = new PosIndicator();
        mScroller = new OverScroller(context);
        mTrackScroller = new OverScroller(context);
        mVelocityTracker = VelocityTracker.obtain();
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mPosIndicator.setTouchSlop(mTouchSlop);
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        final int childCount = getChildCount();
        if (childCount != 2) {
            throw new RuntimeException("child count in LinkageScrollLayout must no more 2");
        }
        mTopView = getChildAt(0);
        mBottomView = getChildAt(1);
        if (!(mBottomView instanceof ILinkageScrollHandler) || !(mTopView instanceof ILinkageScrollHandler)) {
            throw new RuntimeException("child in LinkageScrollLayout must implement IContentHandler");
        }
        mBottomHandler = ((ILinkageScrollHandler) mBottomView).provideScrollHandler();
        mTopHandler = ((ILinkageScrollHandler) mTopView).provideScrollHandler();
        if (mTopHandler == null || mBottomHandler == null) {
            throw new RuntimeException("LinkageScrollHandler provided by child must not be null");
        }
        ((ILinkageScrollHandler) mBottomView).setOnContentViewScrollEvent(mBottomViewScrollEvent);
        ((ILinkageScrollHandler) mTopView).setOnContentViewScrollEvent(mTopViewScrollEvent);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTopView != null) {
            mTopView.measure(widthMeasureSpec, heightMeasureSpec);
            mTopViewHeight = mTopView.getMeasuredHeight();
        }
        if (mBottomView != null) {
            mBottomView.measure(widthMeasureSpec, heightMeasureSpec);
            mBottomViewHeight = mBottomView.getMeasuredHeight();
        }
        // height of visual height
        mVisualHeight = getMeasuredHeight();
        mHeight = mTopViewHeight + mBottomViewHeight;
        int widthSize = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        setMeasuredDimension(widthSize, mHeight);
        Log.d(TAG, "#onMeasure# topHeight: " + mTopViewHeight + ", bottomHeight: " + mBottomViewHeight + ", layoutHeight: " + mHeight);
        // init position indicator
        mPosIndicator.initStartAndEndPos(0, mVisualHeight);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // set current position
        mPosIndicator.setCurrentPos(mTopViewHeight);
        Log.d(TAG, "#onSizeChanged# current position: " + mTopViewHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int curPos = mPosIndicator.getCurrentPos();
            int left = l;
            int top = curPos - mTopViewHeight;
            int right = r;
            int bottom = curPos;
            if (mTopView != null) {
                mTopView.layout(left, top, right, bottom);
                Log.d(TAG, "#onLayout# layout top: top: " + top + ", bottom: " + bottom);
            }
            top = curPos;
            bottom = top + mBottomViewHeight;
            if (mBottomView != null) {
                mBottomView.layout(left, top, right, bottom);
                Log.d(TAG, "#onLayout# layout bottom: top: " + top + ", bottom: " + bottom);
            }
        }
    }

    /**
     * motion event preplaced to children
     *
     * @param event
     * @return
     */
    private boolean dispatchTouchEventSupper(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            int curScrollY = mScroller.getCurrY();
            int offsetY = curScrollY - mLastScrollY;
            mLastScrollY = curScrollY;
            if (offsetY != 0) {
                moveChildrenToNewPos(offsetY);
                int velocity = (int) mScroller.getCurrVelocity();
                if (mPosIndicator.isInStartPos()) {
                    // bad case:不同机型表现效果不一样。
                    // oneplus效果OK, 华为mate 9 webview的velocity不是很匹配
                    // 这里需要优化,包括判断条件
                    if (mBottomView instanceof WebView) {
                        velocity /= 2;
                    }
                    mBottomHandler.flingContentVertically(mBottomView, velocity);
                    mScroller.abortAnimation();
                }
                if (mPosIndicator.isInEndPos()) {
                    // bad case:不同机型表现效果不一样。
                    // oneplus的效果OK, 华为mate9webview的velocity不是很匹配
                    if (mTopView instanceof WebView) {
                        velocity /= 2;
                    }
                    mTopHandler.flingContentVertically(mTopView, -velocity);
                    mScroller.abortAnimation();
                }
            }
            invalidate();
        }
    }

    /**
     * fling children
     *
     * @param velocityY
     */
    private void fling(int velocityY) {
        // Log.d(TAG, "#fling# velocity]Y: " + velocityY);
        mScroller.fling(0, 0, 0, velocityY, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE);
        mLastScrollY = 0;
        invalidate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        int actionIndex = ev.getActionIndex();
        mVelocityTracker.addMovement(ev);
        switch(ev.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = ev.getPointerId(actionIndex);
                mPosIndicator.onDown(x, y);
                mHreplacedendCancelEvent = false;
                mVelocityTracker.clear();
                mVelocityTracker.addMovement(ev);
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                if (!mTrackScroller.isFinished()) {
                    mTrackScroller.abortAnimation();
                }
                // down 停止content view scroll
                mTopHandler.stopContentScroll(mTopView);
                mBottomHandler.stopContentScroll(mBottomView);
                return dispatchTouchEventSupper(ev);
            case MotionEvent.ACTION_POINTER_DOWN:
                mActivePointerId = ev.getPointerId(actionIndex);
                x = ev.getX(actionIndex);
                y = ev.getY(actionIndex);
                mPosIndicator.onPointerDown(x, y);
                return dispatchTouchEventSupper(ev);
            case MotionEvent.ACTION_POINTER_UP:
                final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                    x = (int) ev.getX(newPointerIndex);
                    y = (int) ev.getY(newPointerIndex);
                    mPosIndicator.onPointerUp(x, y);
                }
                return dispatchTouchEventSupper(ev);
            case MotionEvent.ACTION_MOVE:
                mLastMotionEvent = ev;
                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex < 0) {
                    return false;
                }
                x = ev.getX(activePointerIndex);
                y = ev.getY(activePointerIndex);
                mPosIndicator.onMove(x, y);
                if (mPosIndicator.isDragging()) {
                    if (mPosIndicator.isMoveUp()) {
                        // move up
                        if (mPosIndicator.isInStartPos()) {
                            if (isControl) {
                                mBottomHandler.scrollContentBy(mBottomView, (int) -mPosIndicator.getOffsetY());
                            } else {
                                return dispatchTouchEventSupper(ev);
                            }
                        } else if (mPosIndicator.isInEndPos()) {
                            if (mTopView.canScrollVertically(1)) {
                                if (isControl) {
                                    mTopHandler.scrollContentBy(mTopView, (int) -mPosIndicator.getOffsetY());
                                } else {
                                    return dispatchTouchEventSupper(ev);
                                }
                            } else {
                                moveChildrenToNewPos(mPosIndicator.getOffsetY());
                                isControl = true;
                            }
                        } else {
                            moveChildrenToNewPos(mPosIndicator.getOffsetY());
                            isControl = true;
                        }
                    } else {
                        // move down
                        if (mPosIndicator.isInStartPos()) {
                            if (mBottomView.canScrollVertically(-1)) {
                                if (isControl) {
                                    mBottomHandler.scrollContentBy(mBottomView, (int) -mPosIndicator.getOffsetY());
                                } else {
                                    return dispatchTouchEventSupper(ev);
                                }
                            } else {
                                moveChildrenToNewPos(mPosIndicator.getOffsetY());
                                isControl = true;
                            }
                        } else if (mPosIndicator.isInEndPos()) {
                            if (isControl) {
                                mTopHandler.scrollContentBy(mBottomView, (int) -mPosIndicator.getOffsetY());
                            } else {
                                return dispatchTouchEventSupper(ev);
                            }
                        } else {
                            moveChildrenToNewPos(mPosIndicator.getOffsetY());
                            isControl = true;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mPosIndicator.onRelease(x, y);
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocityY = (int) mVelocityTracker.getYVelocity();
                if (isControl) {
                    isControl = false;
                    if (Math.abs(velocityY) > mMinimumVelocity) {
                        if (mPosIndicator.isInStartPos()) {
                            mBottomHandler.flingContentVertically(mBottomView, -velocityY);
                        } else {
                            fling(velocityY);
                        }
                    }
                } else {
                    if (Math.abs(velocityY) > mMinimumVelocity) {
                        // 跟踪子view velocity
                        trackChildVelocity(velocityY);
                    }
                    return dispatchTouchEventSupper(ev);
                }
                break;
        }
        return true;
    }

    /**
     * 跟踪子view的velocity
     */
    private void trackChildVelocity(int velocityY) {
        mTrackScroller.fling(0, 0, 0, Math.abs(velocityY), 0, 0, 0, Integer.MAX_VALUE);
    }

    /**
     * send cancel event to children
     */
    private void sendCancelEvent() {
        Log.d(TAG, "#sendCancelEvent#");
        MotionEvent event = mLastMotionEvent;
        MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, event.getX(), event.getY(), event.getMetaState());
        dispatchTouchEventSupper(e);
    }

    /**
     * 将子view移到新的position
     *
     * @param offsetY
     */
    private void moveChildrenToNewPos(float offsetY) {
        // send cancel event to children
        if (!mHreplacedendCancelEvent && mPosIndicator.isUnderTouch() && mPosIndicator.hasMovedAfterPressedDown()) {
            mHreplacedendCancelEvent = true;
            sendCancelEvent();
        }
        if (Math.abs(offsetY) <= 0) {
            return;
        }
        int toPos = mPosIndicator.getCurrentPos() + (int) offsetY;
        // 边界检查:startPos <= toPos <= endPos
        toPos = mPosIndicator.checkPosBoundary(toPos);
        mPosIndicator.setCurrentPos(toPos);
        int distanceY = toPos - mPosIndicator.getLastPos();
        offsetChildren(distanceY);
        if (mPosIndicator.hasJustLeftEndPos()) {
            if (mLinkageScrollListener.hasHandler()) {
                mLinkageScrollListener.onBottomJustIn(mPosIndicator);
            }
        }
        if (mPosIndicator.hasJustLeftStartPos()) {
            if (mLinkageScrollListener.hasHandler()) {
                mLinkageScrollListener.onTopJustIn(mPosIndicator);
            }
        }
        if (mLinkageScrollListener.hasHandler()) {
            mLinkageScrollListener.onPositionChanged(mPosIndicator);
        }
        if (mPosIndicator.hasJustBackStartPos()) {
            if (mLinkageScrollListener.hasHandler()) {
                mLinkageScrollListener.onTopJustOut(mPosIndicator);
            }
        }
        if (mPosIndicator.hasJustBackEndPos()) {
            if (mLinkageScrollListener.hasHandler()) {
                mLinkageScrollListener.onBottomJustOut(mPosIndicator);
            }
        }
    }

    /**
     * move容器中子view的位置
     *
     * @param deltaY
     */
    private void offsetChildren(int deltaY) {
        mTopView.offsetTopAndBottom(deltaY);
        mBottomView.offsetTopAndBottom(deltaY);
    }

    /**
     * 注册容器位置信息更新的接口
     */
    public void addLinkageScrollListener(LinkageScrollListener handler) {
        mLinkageScrollListener.addHandler(mLinkageScrollListener, handler);
    }
}

19 Source : HeaderBehavior.java
with Apache License 2.0
from material-components

/**
 * The {@link Behavior} for a view that sits vertically above scrolling a view. See {@link
 * HeaderScrollingViewBehavior}.
 */
abstract clreplaced HeaderBehavior<V extends View> extends ViewOffsetBehavior<V> {

    private static final int INVALID_POINTER = -1;

    @Nullable
    private Runnable flingRunnable;

    OverScroller scroller;

    private boolean isBeingDragged;

    private int activePointerId = INVALID_POINTER;

    private int lastMotionY;

    private int touchSlop = -1;

    @Nullable
    private VelocityTracker velocityTracker;

    public HeaderBehavior() {
    }

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

    @Override
    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
        if (touchSlop < 0) {
            touchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
        }
        // Shortcut since we're being dragged
        if (ev.getActionMasked() == MotionEvent.ACTION_MOVE && isBeingDragged) {
            if (activePointerId == INVALID_POINTER) {
                // If we don't have a valid id, the touch down wasn't on content.
                return false;
            }
            int pointerIndex = ev.findPointerIndex(activePointerId);
            if (pointerIndex == -1) {
                return false;
            }
            int y = (int) ev.getY(pointerIndex);
            int yDiff = Math.abs(y - lastMotionY);
            if (yDiff > touchSlop) {
                lastMotionY = y;
                return true;
            }
        }
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            activePointerId = INVALID_POINTER;
            int x = (int) ev.getX();
            int y = (int) ev.getY();
            isBeingDragged = canDragView(child) && parent.isPointInChildBounds(child, x, y);
            if (isBeingDragged) {
                lastMotionY = y;
                activePointerId = ev.getPointerId(0);
                ensureVelocityTracker();
                // There is an animation in progress. Stop it and catch the view.
                if (scroller != null && !scroller.isFinished()) {
                    scroller.abortAnimation();
                    return true;
                }
            }
        }
        if (velocityTracker != null) {
            velocityTracker.addMovement(ev);
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent ev) {
        boolean consumeUp = false;
        switch(ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(activePointerId);
                if (activePointerIndex == -1) {
                    return false;
                }
                final int y = (int) ev.getY(activePointerIndex);
                int dy = lastMotionY - y;
                lastMotionY = y;
                // We're being dragged so scroll the ABL
                scroll(parent, child, dy, getMaxDragOffset(child), 0);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                int newIndex = ev.getActionIndex() == 0 ? 1 : 0;
                activePointerId = ev.getPointerId(newIndex);
                lastMotionY = (int) (ev.getY(newIndex) + 0.5f);
                break;
            case MotionEvent.ACTION_UP:
                if (velocityTracker != null) {
                    consumeUp = true;
                    velocityTracker.addMovement(ev);
                    velocityTracker.computeCurrentVelocity(1000);
                    float yvel = velocityTracker.getYVelocity(activePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
            // $FALLTHROUGH
            case MotionEvent.ACTION_CANCEL:
                isBeingDragged = false;
                activePointerId = INVALID_POINTER;
                if (velocityTracker != null) {
                    velocityTracker.recycle();
                    velocityTracker = null;
                }
                break;
        }
        if (velocityTracker != null) {
            velocityTracker.addMovement(ev);
        }
        return isBeingDragged || consumeUp;
    }

    int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
        return setHeaderTopBottomOffset(parent, header, newOffset, Integer.MIN_VALUE, Integer.MAX_VALUE);
    }

    int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset, int minOffset, int maxOffset) {
        final int curOffset = getTopAndBottomOffset();
        int consumed = 0;
        if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
            // If we have some scrolling range, and we're currently within the min and max
            // offsets, calculate a new offset
            newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
            if (curOffset != newOffset) {
                setTopAndBottomOffset(newOffset);
                // Update how much dy we have consumed
                consumed = curOffset - newOffset;
            }
        }
        return consumed;
    }

    int getTopBottomOffsetForScrollingSibling() {
        return getTopAndBottomOffset();
    }

    final int scroll(CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset) {
        return setHeaderTopBottomOffset(coordinatorLayout, header, getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
    }

    final boolean fling(CoordinatorLayout coordinatorLayout, @NonNull V layout, int minOffset, int maxOffset, float velocityY) {
        if (flingRunnable != null) {
            layout.removeCallbacks(flingRunnable);
            flingRunnable = null;
        }
        if (scroller == null) {
            scroller = new OverScroller(layout.getContext());
        }
        scroller.fling(0, // curr
        getTopAndBottomOffset(), 0, // velocity.
        Math.round(velocityY), 0, // x
        0, minOffset, // y
        maxOffset);
        if (scroller.computeScrollOffset()) {
            flingRunnable = new FlingRunnable(coordinatorLayout, layout);
            ViewCompat.postOnAnimation(layout, flingRunnable);
            return true;
        } else {
            onFlingFinished(coordinatorLayout, layout);
            return false;
        }
    }

    /**
     * Called when a fling has finished, or the fling was initiated but there wasn't enough velocity
     * to start it.
     */
    void onFlingFinished(CoordinatorLayout parent, V layout) {
    // no-op
    }

    /**
     * Return true if the view can be dragged.
     */
    boolean canDragView(V view) {
        return false;
    }

    /**
     * Returns the maximum px offset when {@code view} is being dragged.
     */
    int getMaxDragOffset(@NonNull V view) {
        return -view.getHeight();
    }

    int getScrollRangeForDragFling(@NonNull V view) {
        return view.getHeight();
    }

    private void ensureVelocityTracker() {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
    }

    private clreplaced FlingRunnable implements Runnable {

        private final CoordinatorLayout parent;

        private final V layout;

        FlingRunnable(CoordinatorLayout parent, V layout) {
            this.parent = parent;
            this.layout = layout;
        }

        @Override
        public void run() {
            if (layout != null && scroller != null) {
                if (scroller.computeScrollOffset()) {
                    setHeaderTopBottomOffset(parent, layout, scroller.getCurrY());
                    // Post ourselves so that we run on the next animation
                    ViewCompat.postOnAnimation(layout, this);
                } else {
                    onFlingFinished(parent, layout);
                }
            }
        }
    }
}

19 Source : SystemGesturesPointerEventListener.java
with Apache License 2.0
from lulululbj

/*
 * Listens for system-wide input gestures, firing callbacks when detected.
 * @hide
 */
public clreplaced SystemGesturesPointerEventListener implements PointerEventListener {

    private static final String TAG = "SystemGestures";

    private static final boolean DEBUG = false;

    private static final long SWIPE_TIMEOUT_MS = 500;

    // max per input system
    private static final int MAX_TRACKED_POINTERS = 32;

    private static final int UNTRACKED_POINTER = -1;

    private static final int MAX_FLING_TIME_MILLIS = 5000;

    private static final int SWIPE_NONE = 0;

    private static final int SWIPE_FROM_TOP = 1;

    private static final int SWIPE_FROM_BOTTOM = 2;

    private static final int SWIPE_FROM_RIGHT = 3;

    private static final int SWIPE_FROM_LEFT = 4;

    private final Context mContext;

    private final int mSwipeStartThreshold;

    private final int mSwipeDistanceThreshold;

    private final Callbacks mCallbacks;

    private final int[] mDownPointerId = new int[MAX_TRACKED_POINTERS];

    private final float[] mDownX = new float[MAX_TRACKED_POINTERS];

    private final float[] mDownY = new float[MAX_TRACKED_POINTERS];

    private final long[] mDownTime = new long[MAX_TRACKED_POINTERS];

    private GestureDetector mGestureDetector;

    private OverScroller mOverscroller;

    int screenHeight;

    int screenWidth;

    private int mDownPointers;

    private boolean mSwipeFireable;

    private boolean mDebugFireable;

    private boolean mMouseHoveringAtEdge;

    private long mLastFlingTime;

    public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
        mContext = context;
        mCallbacks = checkNull("callbacks", callbacks);
        mSwipeStartThreshold = checkNull("context", context).getResources().getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
        mSwipeDistanceThreshold = mSwipeStartThreshold;
        if (DEBUG)
            Slog.d(TAG, "mSwipeStartThreshold=" + mSwipeStartThreshold + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
    }

    private static <T> T checkNull(String name, T arg) {
        if (arg == null) {
            throw new IllegalArgumentException(name + " must not be null");
        }
        return arg;
    }

    public void systemReady() {
        Handler h = new Handler(Looper.myLooper());
        mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
        mOverscroller = new OverScroller(mContext);
    }

    @Override
    public void onPointerEvent(MotionEvent event) {
        if (mGestureDetector != null && event.isTouchEvent()) {
            mGestureDetector.onTouchEvent(event);
        }
        switch(event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mSwipeFireable = true;
                mDebugFireable = true;
                mDownPointers = 0;
                captureDown(event, 0);
                if (mMouseHoveringAtEdge) {
                    mMouseHoveringAtEdge = false;
                    mCallbacks.onMouseLeaveFromEdge();
                }
                mCallbacks.onDown();
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                captureDown(event, event.getActionIndex());
                if (mDebugFireable) {
                    mDebugFireable = event.getPointerCount() < 5;
                    if (!mDebugFireable) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing debug");
                        mCallbacks.onDebug();
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mSwipeFireable) {
                    final int swipe = detectSwipe(event);
                    mSwipeFireable = swipe == SWIPE_NONE;
                    if (swipe == SWIPE_FROM_TOP) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromTop");
                        mCallbacks.onSwipeFromTop();
                    } else if (swipe == SWIPE_FROM_BOTTOM) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromBottom");
                        mCallbacks.onSwipeFromBottom();
                    } else if (swipe == SWIPE_FROM_RIGHT) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromRight");
                        mCallbacks.onSwipeFromRight();
                    } else if (swipe == SWIPE_FROM_LEFT) {
                        if (DEBUG)
                            Slog.d(TAG, "Firing onSwipeFromLeft");
                        mCallbacks.onSwipeFromLeft();
                    }
                }
                break;
            case MotionEvent.ACTION_HOVER_MOVE:
                if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
                    if (!mMouseHoveringAtEdge && event.getY() == 0) {
                        mCallbacks.onMouseHoverAtTop();
                        mMouseHoveringAtEdge = true;
                    } else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
                        mCallbacks.onMouseHoverAtBottom();
                        mMouseHoveringAtEdge = true;
                    } else if (mMouseHoveringAtEdge && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
                        mCallbacks.onMouseLeaveFromEdge();
                        mMouseHoveringAtEdge = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeFireable = false;
                mDebugFireable = false;
                mCallbacks.onUpOrCancel();
                break;
            default:
                if (DEBUG)
                    Slog.d(TAG, "Ignoring " + event);
        }
    }

    private void captureDown(MotionEvent event, int pointerIndex) {
        final int pointerId = event.getPointerId(pointerIndex);
        final int i = findIndex(pointerId);
        if (DEBUG)
            Slog.d(TAG, "pointer " + pointerId + " down pointerIndex=" + pointerIndex + " trackingIndex=" + i);
        if (i != UNTRACKED_POINTER) {
            mDownX[i] = event.getX(pointerIndex);
            mDownY[i] = event.getY(pointerIndex);
            mDownTime[i] = event.getEventTime();
            if (DEBUG)
                Slog.d(TAG, "pointer " + pointerId + " down x=" + mDownX[i] + " y=" + mDownY[i]);
        }
    }

    private int findIndex(int pointerId) {
        for (int i = 0; i < mDownPointers; i++) {
            if (mDownPointerId[i] == pointerId) {
                return i;
            }
        }
        if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
            return UNTRACKED_POINTER;
        }
        mDownPointerId[mDownPointers++] = pointerId;
        return mDownPointers - 1;
    }

    private int detectSwipe(MotionEvent move) {
        final int historySize = move.getHistorySize();
        final int pointerCount = move.getPointerCount();
        for (int p = 0; p < pointerCount; p++) {
            final int pointerId = move.getPointerId(p);
            final int i = findIndex(pointerId);
            if (i != UNTRACKED_POINTER) {
                for (int h = 0; h < historySize; h++) {
                    final long time = move.getHistoricalEventTime(h);
                    final float x = move.getHistoricalX(p, h);
                    final float y = move.getHistoricalY(p, h);
                    final int swipe = detectSwipe(i, time, x, y);
                    if (swipe != SWIPE_NONE) {
                        return swipe;
                    }
                }
                final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
                if (swipe != SWIPE_NONE) {
                    return swipe;
                }
            }
        }
        return SWIPE_NONE;
    }

    private int detectSwipe(int i, long time, float x, float y) {
        final float fromX = mDownX[i];
        final float fromY = mDownY[i];
        final long elapsed = time - mDownTime[i];
        if (DEBUG)
            Slog.d(TAG, "pointer " + mDownPointerId[i] + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
        if (fromY <= mSwipeStartThreshold && y > fromY + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_TOP;
        }
        if (fromY >= screenHeight - mSwipeStartThreshold && y < fromY - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_BOTTOM;
        }
        if (fromX >= screenWidth - mSwipeStartThreshold && x < fromX - mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_RIGHT;
        }
        if (fromX <= mSwipeStartThreshold && x > fromX + mSwipeDistanceThreshold && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_LEFT;
        }
        return SWIPE_NONE;
    }

    private final clreplaced FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            if (!mOverscroller.isFinished()) {
                mOverscroller.forceFinished(true);
            }
            return true;
        }

        @Override
        public boolean onFling(MotionEvent down, MotionEvent up, float velocityX, float velocityY) {
            mOverscroller.computeScrollOffset();
            long now = SystemClock.uptimeMillis();
            if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
                mOverscroller.forceFinished(true);
            }
            mOverscroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            int duration = mOverscroller.getDuration();
            if (duration > MAX_FLING_TIME_MILLIS) {
                duration = MAX_FLING_TIME_MILLIS;
            }
            mLastFlingTime = now;
            mCallbacks.onFling(duration);
            return true;
        }
    }

    interface Callbacks {

        void onSwipeFromTop();

        void onSwipeFromBottom();

        void onSwipeFromRight();

        void onSwipeFromLeft();

        void onFling(int durationMs);

        void onDown();

        void onUpOrCancel();

        void onMouseHoverAtTop();

        void onMouseHoverAtBottom();

        void onMouseLeaveFromEdge();

        void onDebug();
    }
}

19 Source : SwipeHelper.java
with Apache License 2.0
from luckybilly

/**
 * This clreplaced is copy and modified from ViewDragHelper
 *  1. mCapturedView removed. use mClampedDistanceX and mClampedDistanceY instead
 *  2. Callback removed. use {@link SwipeConsumer} to consume the motion event
 * @author billy.qi
 */
public clreplaced SwipeHelper {

    private static final String TAG = "SwipeHelper";

    /**
     * A null/invalid pointer ID.
     */
    public static final int INVALID_POINTER = -1;

    public static final int POINTER_NESTED_SCROLL = -2;

    public static final int POINTER_NESTED_FLY = -3;

    /**
     * A view is not currently being dragged or animating as a result of a fling/snap.
     */
    public static final int STATE_IDLE = 0;

    /**
     * A view is currently being dragged. The position is currently changing as a result
     * of user input or simulated user input.
     */
    public static final int STATE_DRAGGING = 1;

    /**
     * A view is currently settling into place as a result of a fling or
     * predefined non-interactive motion.
     */
    public static final int STATE_SETTLING = 2;

    public static final int STATE_NONE_TOUCH = 3;

    private final ViewConfiguration viewConfiguration;

    // ms
    private int maxSettleDuration = 600;

    // Current drag state; idle, dragging or settling
    private int mDragState;

    // Distance to travel before a drag may begin
    private int mTouchSlop;

    // Last known position/pointer tracking
    private int mActivePointerId = INVALID_POINTER;

    private float[] mInitialMotionX;

    private float[] mInitialMotionY;

    private float[] mLastMotionX;

    private float[] mLastMotionY;

    private int mPointersDown;

    private VelocityTracker mVelocityTracker;

    private float mMaxVelocity;

    private float mMinVelocity;

    private OverScroller mScroller;

    private final SwipeConsumer mSwipeConsumer;

    private boolean mReleaseInProgress;

    private final ViewGroup mParentView;

    private int mClampedDistanceX;

    private int mClampedDistanceY;

    /**
     * Default interpolator defining the animation curve for mScroller
     */
    private static final Interpolator sInterpolator = new Interpolator() {

        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

    /**
     * Factory method to create a new SwipeHelper.
     *
     * @param forParent Parent view to monitor
     * @param consumer Callback to provide information and receive events
     * @param interpolator interpolator for animation
     * @return a new SwipeHelper instance
     */
    public static SwipeHelper create(ViewGroup forParent, SwipeConsumer consumer, Interpolator interpolator) {
        return new SwipeHelper(forParent.getContext(), forParent, consumer, interpolator);
    }

    public static SwipeHelper create(ViewGroup forParent, SwipeConsumer consumer) {
        return create(forParent, consumer, null);
    }

    /**
     * Factory method to create a new SwipeHelper.
     *
     * @param forParent Parent view to monitor
     * @param sensitivity Multiplier for how sensitive the helper should be about detecting
     *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
     * @param consumer Callback to provide information and receive events
     * @param interpolator interpolator for animation
     * @return a new SwipeHelper instance
     */
    public static SwipeHelper create(ViewGroup forParent, float sensitivity, SwipeConsumer consumer, Interpolator interpolator) {
        final SwipeHelper helper = create(forParent, consumer, interpolator);
        helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
        return helper;
    }

    public static SwipeHelper create(ViewGroup forParent, float sensitivity, SwipeConsumer cb) {
        return create(forParent, sensitivity, cb, null);
    }

    public void setSensitivity(float sensitivity) {
        mTouchSlop = (int) (viewConfiguration.getScaledTouchSlop() * (1 / sensitivity));
    }

    /**
     * Apps should use SwipeHelper.create() to get a new instance.
     * This will allow VDH to use internal compatibility implementations for different
     * platform versions.
     *
     * @param context Context to initialize config-dependent params from
     * @param forParent Parent view to monitor
     * @param interpolator interpolator for animation
     */
    private SwipeHelper(Context context, ViewGroup forParent, SwipeConsumer cb, Interpolator interpolator) {
        if (forParent == null) {
            throw new IllegalArgumentException("Parent view may not be null");
        }
        if (cb == null) {
            throw new IllegalArgumentException("Callback may not be null");
        }
        mParentView = forParent;
        mSwipeConsumer = cb;
        viewConfiguration = ViewConfiguration.get(context);
        mTouchSlop = viewConfiguration.getScaledTouchSlop();
        mMaxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        mMinVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        setInterpolator(context, interpolator);
    }

    public void setInterpolator(Context context, Interpolator interpolator) {
        if (interpolator == null) {
            interpolator = sInterpolator;
        }
        if (mScroller != null) {
            abort();
            mScroller = null;
        }
        mScroller = new OverScroller(context, interpolator);
    }

    /**
     * Set the minimum velocity that will be detected as having a magnitude greater than zero
     * in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
     *
     * @param minVel Minimum velocity to detect
     * @return this
     */
    public SwipeHelper setMinVelocity(float minVel) {
        mMinVelocity = minVel;
        return this;
    }

    /**
     * Return the currently configured minimum velocity. Any flings with a magnitude less
     * than this value in pixels per second. Callback methods accepting a velocity will receive
     * zero as a velocity value if the real detected velocity was below this threshold.
     *
     * @return the minimum velocity that will be detected
     */
    public float getMinVelocity() {
        return mMinVelocity;
    }

    /**
     * Retrieve the current drag state of this helper. This will return one of
     * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING} or {@link #STATE_NONE_TOUCH}.
     * @return The current drag state
     */
    public int getDragState() {
        return mDragState;
    }

    /**
     * @return The ID of the pointer currently dragging
     *         or {@link #INVALID_POINTER}.
     */
    public int getActivePointerId() {
        return mActivePointerId;
    }

    /**
     * @return The minimum distance in pixels that the user must travel to initiate a drag
     */
    public int getTouchSlop() {
        return mTouchSlop;
    }

    /**
     * The result of a call to this method is equivalent to
     * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
     */
    public void cancel() {
        mActivePointerId = INVALID_POINTER;
        clearMotionHistory();
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
     * {@link #cancel()}, but also abort all motion in progress and snap to the end of any
     * animation.
     */
    public void abort() {
        cancel();
        if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
            final int oldX = mScroller.getCurrX();
            final int oldY = mScroller.getCurrY();
            mScroller.abortAnimation();
            final int newX = mScroller.getCurrX();
            final int newY = mScroller.getCurrY();
            mSwipeConsumer.onSwipeDistanceChanged(newX, newY, newX - oldX, newY - oldY);
        }
        setDragState(STATE_IDLE);
    }

    /**
     * Animate the view <code>child</code> to the given (left, top) position.
     * If this method returns true, the caller should invoke {@link #continueSettling()}
     * on each subsequent frame to continue the motion until it returns false. If this method
     * returns false there is no further work to do to complete the movement.
     *
     * @param startX start x position
     * @param startY start y position
     * @param finalX Final x position
     * @param finalY Final y position
     * @return true if animation should continue through {@link #continueSettling()} calls
     */
    public boolean smoothSlideTo(int startX, int startY, int finalX, int finalY) {
        mClampedDistanceX = startX;
        mClampedDistanceY = startY;
        return smoothSlideTo(finalX, finalY);
    }

    public boolean smoothSlideTo(int finalX, int finalY) {
        boolean continueSliding;
        if (mVelocityTracker != null) {
            continueSliding = smoothSettleCapturedViewTo(finalX, finalY, (int) mVelocityTracker.getXVelocity(mActivePointerId), (int) mVelocityTracker.getYVelocity(mActivePointerId));
        } else {
            continueSliding = smoothSettleCapturedViewTo(finalX, finalY, 0, 0);
        }
        mActivePointerId = INVALID_POINTER;
        return continueSliding;
    }

    /**
     * Settle the captured view at the given (left, top) position.
     * The appropriate velocity from prior motion will be taken into account.
     * If this method returns true, the caller should invoke {@link #continueSettling()}
     * on each subsequent frame to continue the motion until it returns false. If this method
     * returns false there is no further work to do to complete the movement.
     *
     * @param finalX Settled left edge position for the captured view
     * @param finalY Settled top edge position for the captured view
     * @return true if animation should continue through {@link #continueSettling()} calls
     */
    public boolean settleCapturedViewAt(int finalX, int finalY) {
        if (!mReleaseInProgress) {
            throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased");
        }
        return smoothSettleCapturedViewTo(finalX, finalY, (int) mVelocityTracker.getXVelocity(mActivePointerId), (int) mVelocityTracker.getYVelocity(mActivePointerId));
    }

    /**
     * Settle the captured view at the given (left, top) position.
     *
     * @param finalX Target left position for the captured view
     * @param finalY Target top position for the captured view
     * @param xvel Horizontal velocity
     * @param yvel Vertical velocity
     * @return true if animation should continue through {@link #continueSettling()} calls
     */
    private boolean smoothSettleCapturedViewTo(int finalX, int finalY, int xvel, int yvel) {
        final int startX = mClampedDistanceX;
        final int startTop = mClampedDistanceY;
        final int dx = finalX - startX;
        final int dy = finalY - startTop;
        mScroller.abortAnimation();
        if (dx == 0 && dy == 0) {
            setDragState(STATE_SETTLING);
            mSwipeConsumer.onSwipeDistanceChanged(finalX, finalY, dx, dy);
            setDragState(STATE_IDLE);
            return false;
        }
        final int duration = computeSettleDuration(dx, dy, xvel, yvel);
        mScroller.startScroll(startX, startTop, dx, dy, duration);
        setDragState(STATE_SETTLING);
        return true;
    }

    private int computeSettleDuration(int dx, int dy, int xvel, int yvel) {
        xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
        yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
        final int absDx = Math.abs(dx);
        final int absDy = Math.abs(dy);
        final int absXVel = Math.abs(xvel);
        final int absYVel = Math.abs(yvel);
        final int addedVel = absXVel + absYVel;
        final int addedDistance = absDx + absDy;
        final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx / addedDistance;
        final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy / addedDistance;
        int xduration = computeAxisDuration(dx, xvel, mSwipeConsumer.getHorizontalRange(dx, dy));
        int yduration = computeAxisDuration(dy, yvel, mSwipeConsumer.getVerticalRange(dx, dy));
        return (int) (xduration * xweight + yduration * yweight);
    }

    private int computeAxisDuration(int delta, int velocity, int motionRange) {
        if (delta == 0) {
            return 0;
        }
        final int width = mParentView.getWidth();
        final int halfWidth = width >> 1;
        final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
        int duration;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float range = (float) Math.abs(delta) / motionRange;
            duration = (int) (range * maxSettleDuration);
        }
        return Math.min(duration, maxSettleDuration);
    }

    /**
     * Clamp the magnitude of value for absMin and absMax.
     * If the value is below the minimum, it will be clamped to zero.
     * If the value is above the maximum, it will be clamped to the maximum.
     *
     * @param value Value to clamp
     * @param absMin Absolute value of the minimum significant value to return
     * @param absMax Absolute value of the maximum value to return
     * @return The clamped value with the same sign as <code>value</code>
     */
    private int clampMag(int value, int absMin, int absMax) {
        final int absValue = Math.abs(value);
        if (absValue < absMin) {
            return 0;
        }
        if (absValue > absMax) {
            return value > 0 ? absMax : -absMax;
        }
        return value;
    }

    /**
     * Clamp the magnitude of value for absMin and absMax.
     * If the value is below the minimum, it will be clamped to zero.
     * If the value is above the maximum, it will be clamped to the maximum.
     *
     * @param value Value to clamp
     * @param absMin Absolute value of the minimum significant value to return
     * @param absMax Absolute value of the maximum value to return
     * @return The clamped value with the same sign as <code>value</code>
     */
    private float clampMag(float value, float absMin, float absMax) {
        final float absValue = Math.abs(value);
        if (absValue < absMin) {
            return 0;
        }
        if (absValue > absMax) {
            return value > 0 ? absMax : -absMax;
        }
        return value;
    }

    private float distanceInfluenceForSnapDuration(float f) {
        // center the values about 0.
        f -= 0.5f;
        f *= 0.3f * (float) Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    public boolean continueSettling() {
        if (mDragState == STATE_SETTLING) {
            boolean keepGoing = mScroller.computeScrollOffset();
            final int x = mScroller.getCurrX();
            final int y = mScroller.getCurrY();
            final int dx = x - mClampedDistanceX;
            final int dy = y - mClampedDistanceY;
            if (dx != 0) {
                mClampedDistanceX = x;
            }
            if (dy != 0) {
                mClampedDistanceY = y;
            }
            if (dx != 0 || dy != 0) {
                mSwipeConsumer.onSwipeDistanceChanged(x, y, dx, dy);
            }
            if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
                // Close enough. The interpolator/scroller might think we're still moving
                // but the user sure doesn't.
                mScroller.abortAnimation();
                keepGoing = false;
            }
            if (!keepGoing) {
                setDragState(STATE_IDLE);
            }
        }
        return mDragState == STATE_SETTLING;
    }

    /**
     * Like all callback events this must happen on the UI thread, but release
     * involves some extra semantics. During a release (mReleaseInProgress)
     * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
     * @param xvel x velocity
     * @param yvel y velocity
     */
    public void dispatchViewReleased(float xvel, float yvel) {
        mReleaseInProgress = true;
        mSwipeConsumer.onSwipeReleased(xvel, yvel);
        mReleaseInProgress = false;
        if (mDragState == STATE_DRAGGING) {
            // onViewReleased didn't call a method that would have changed this. Go idle.
            setDragState(STATE_IDLE);
        }
    }

    private void clearMotionHistory() {
        if (mInitialMotionX == null) {
            return;
        }
        Arrays.fill(mInitialMotionX, 0);
        Arrays.fill(mInitialMotionY, 0);
        Arrays.fill(mLastMotionX, 0);
        Arrays.fill(mLastMotionY, 0);
        mPointersDown = 0;
    }

    private void clearMotionHistory(int pointerId) {
        if (mInitialMotionX == null || !isPointerDown(pointerId)) {
            return;
        }
        mInitialMotionX[pointerId] = 0;
        mInitialMotionY[pointerId] = 0;
        mLastMotionX[pointerId] = 0;
        mLastMotionY[pointerId] = 0;
        mPointersDown &= ~(1 << pointerId);
    }

    private void ensureMotionHistorySizeForId(int pointerId) {
        if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
            float[] imx = new float[pointerId + 1];
            float[] imy = new float[pointerId + 1];
            float[] lmx = new float[pointerId + 1];
            float[] lmy = new float[pointerId + 1];
            if (mInitialMotionX != null) {
                System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
                System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
                System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
                System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
            }
            mInitialMotionX = imx;
            mInitialMotionY = imy;
            mLastMotionX = lmx;
            mLastMotionY = lmy;
        }
    }

    private void saveInitialMotion(float x, float y, int pointerId) {
        ensureMotionHistorySizeForId(pointerId);
        mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
        mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
        mPointersDown |= 1 << pointerId;
    }

    private void saveLastMotion(MotionEvent ev) {
        final int pointerCount = ev.getPointerCount();
        for (int i = 0; i < pointerCount; i++) {
            final int pointerId = ev.getPointerId(i);
            // If pointer is invalid then skip saving on ACTION_MOVE.
            if (!isValidPointerForActionMove(pointerId)) {
                continue;
            }
            final float x = ev.getX(i);
            final float y = ev.getY(i);
            mLastMotionX[pointerId] = x;
            mLastMotionY[pointerId] = y;
        }
    }

    /**
     * Check if the given pointer ID represents a pointer that is currently down (to the best
     * of the SwipeHelper's knowledge).
     *
     * <p>The state used to report this information is populated by the methods
     * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
     * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not
     * been called for all relevant MotionEvents to track, the information reported
     * by this method may be stale or incorrect.</p>
     *
     * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
     * @return true if the pointer with the given ID is still down
     */
    public boolean isPointerDown(int pointerId) {
        return (mPointersDown & 1 << pointerId) != 0;
    }

    void setDragState(int state) {
        if (mDragState != state) {
            mDragState = state;
            mSwipeConsumer.onStateChanged(state);
        // if (mDragState == STATE_IDLE) {
        // mClampedDistanceX = mClampedDistanceY = 0;
        // }
        }
    }

    /**
     * Attempt to capture the view with the given pointer ID. The callback will be involved.
     * This will put us into the "dragging" state. If we've already captured this view with
     * this pointer this method will immediately return true without consulting the callback.
     *
     * @param pointerId Pointer to capture with
     * @return true if capture was successful
     */
    private boolean trySwipe(int pointerId, boolean settling, float downX, float downY, float dx, float dy) {
        return trySwipe(pointerId, settling, downX, downY, dx, dy, true);
    }

    private boolean trySwipe(int pointerId, boolean settling, float downX, float downY, float dx, float dy, boolean touchMode) {
        if (mActivePointerId == pointerId) {
            // Already done!
            return true;
        }
        boolean swipe;
        if (settling || mDragState == STATE_SETTLING) {
            swipe = mSwipeConsumer.tryAcceptSettling(pointerId, downX, downY);
        } else {
            swipe = mSwipeConsumer.tryAcceptMoving(pointerId, downX, downY, dx, dy);
        }
        if (swipe) {
            mActivePointerId = pointerId;
            float initX = 0;
            float initY = 0;
            if (pointerId >= 0 && pointerId < mInitialMotionX.length && pointerId < mInitialMotionY.length) {
                initX = mInitialMotionX[pointerId];
                initY = mInitialMotionY[pointerId];
            }
            mSwipeConsumer.onSwipeAccepted(pointerId, settling, initX, initY);
            mClampedDistanceX = mSwipeConsumer.clampDistanceHorizontal(0, 0);
            mClampedDistanceY = mSwipeConsumer.clampDistanceVertical(0, 0);
            setDragState(touchMode ? STATE_DRAGGING : STATE_NONE_TOUCH);
            return true;
        }
        return false;
    }

    /**
     * Check if this event as provided to the parent view's onInterceptTouchEvent should
     * cause the parent to intercept the touch event stream.
     *
     * @param ev MotionEvent provided to onInterceptTouchEvent
     * @return true if the parent view should return true from onInterceptTouchEvent
     */
    public boolean shouldInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        final int actionIndex = ev.getActionIndex();
        if (action == MotionEvent.ACTION_DOWN) {
            // Reset things for a new event stream, just in case we didn't get
            // the whole previous stream.
            cancel();
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    final float x = ev.getX();
                    final float y = ev.getY();
                    final int pointerId = ev.getPointerId(0);
                    saveInitialMotion(x, y, pointerId);
                    // Catch a settling view if possible.
                    if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
                        trySwipe(pointerId, true, x, y, 0, 0);
                    }
                    break;
                }
            case MotionEvent.ACTION_POINTER_DOWN:
                {
                    final int pointerId = ev.getPointerId(actionIndex);
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    saveInitialMotion(x, y, pointerId);
                    // A SwipeHelper can only manipulate one view at a time.
                    if (mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH) {
                        // Catch a settling view if possible.
                        trySwipe(pointerId, true, x, y, 0, 0);
                    }
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    if (mInitialMotionX == null || mInitialMotionY == null) {
                        break;
                    }
                    // First to cross a touch slop over a draggable view wins. Also report edge drags.
                    final int pointerCount = ev.getPointerCount();
                    for (int i = 0; i < pointerCount; i++) {
                        final int pointerId = ev.getPointerId(i);
                        // If pointer is invalid then skip the ACTION_MOVE.
                        if (!isValidPointerForActionMove(pointerId)) {
                            continue;
                        }
                        final float x = ev.getX(i);
                        final float y = ev.getY(i);
                        float downX = mInitialMotionX[pointerId];
                        float downY = mInitialMotionY[pointerId];
                        final float dx = x - downX;
                        final float dy = y - downY;
                        final boolean pastSlop = checkTouchSlop(dx, dy);
                        if (pastSlop) {
                            final int hDragRange = mSwipeConsumer.getHorizontalRange(dx, dy);
                            final int vDragRange = mSwipeConsumer.getVerticalRange(dx, dy);
                            if (hDragRange == 0 && vDragRange == 0) {
                                continue;
                            }
                        }
                        if (pastSlop && trySwipe(pointerId, false, downX, downY, dx, dy)) {
                            break;
                        }
                    }
                    saveLastMotion(ev);
                    break;
                }
            case MotionEvent.ACTION_POINTER_UP:
                {
                    final int pointerId = ev.getPointerId(actionIndex);
                    clearMotionHistory(pointerId);
                    break;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                {
                    cancel();
                    break;
                }
            default:
        }
        return mDragState == STATE_DRAGGING;
    }

    /**
     * Process a touch event received by the parent view. This method will dispatch callback events
     * as needed before returning. The parent view's onTouchEvent implementation should call this.
     *
     * @param ev The touch event received by the parent view
     */
    public void processTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        final int actionIndex = ev.getActionIndex();
        if (action == MotionEvent.ACTION_DOWN && mDragState != STATE_DRAGGING) {
            // Reset things for a new event stream, just in case we didn't get
            // the whole previous stream.
            cancel();
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    final float x = ev.getX();
                    final float y = ev.getY();
                    final int pointerId = ev.getPointerId(0);
                    saveInitialMotion(x, y, pointerId);
                    // Since the parent is already directly processing this touch event,
                    // there is no reason to delay for a slop before dragging.
                    // Start immediately if possible.
                    if (mDragState != STATE_DRAGGING) {
                        trySwipe(pointerId, mDragState == STATE_SETTLING || mDragState == STATE_NONE_TOUCH, x, y, 0, 0);
                    }
                    break;
                }
            case MotionEvent.ACTION_POINTER_DOWN:
                {
                    final int pointerId = ev.getPointerId(actionIndex);
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    saveInitialMotion(x, y, pointerId);
                    if (mDragState == STATE_DRAGGING) {
                        trySwipe(pointerId, true, x, y, 0, 0);
                    }
                    break;
                }
            case MotionEvent.ACTION_MOVE:
                {
                    if (mDragState == STATE_DRAGGING) {
                        // If pointer is invalid then skip the ACTION_MOVE.
                        if (!isValidPointerForActionMove(mActivePointerId)) {
                            break;
                        }
                        final int index = ev.findPointerIndex(mActivePointerId);
                        if (index < 0) {
                            break;
                        }
                        final float x = ev.getX(index);
                        final float y = ev.getY(index);
                        final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                        final int idy = (int) (y - mLastMotionY[mActivePointerId]);
                        dragTo(mClampedDistanceX + idx, mClampedDistanceY + idy, idx, idy);
                        saveLastMotion(ev);
                    } else {
                        // Check to see if any pointer is now over a draggable view.
                        final int pointerCount = ev.getPointerCount();
                        for (int i = 0; i < pointerCount; i++) {
                            final int pointerId = ev.getPointerId(i);
                            // If pointer is invalid then skip the ACTION_MOVE.
                            if (!isValidPointerForActionMove(pointerId)) {
                                continue;
                            }
                            final float x = ev.getX(i);
                            final float y = ev.getY(i);
                            float downX = mInitialMotionX[pointerId];
                            float downY = mInitialMotionY[pointerId];
                            final float dx = x - downX;
                            final float dy = y - downY;
                            if (checkTouchSlop(dx, dy) && trySwipe(pointerId, false, downX, downY, dx, dy)) {
                                break;
                            }
                        }
                        saveLastMotion(ev);
                    }
                    break;
                }
            case MotionEvent.ACTION_POINTER_UP:
                {
                    final int pointerId = ev.getPointerId(actionIndex);
                    if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
                        // Try to find another pointer that's still holding on to the captured view.
                        int newActivePointer = INVALID_POINTER;
                        final int pointerCount = ev.getPointerCount();
                        for (int i = 0; i < pointerCount; i++) {
                            final int id = ev.getPointerId(i);
                            if (id == mActivePointerId) {
                                // This one's going away, skip.
                                continue;
                            }
                            if (!isValidPointerForActionMove(id)) {
                                continue;
                            }
                            if (trySwipe(id, true, mInitialMotionX[id], mInitialMotionX[id], 0, 0)) {
                                newActivePointer = mActivePointerId;
                                break;
                            }
                        }
                        if (newActivePointer == INVALID_POINTER) {
                            // We didn't find another pointer still touching the view, release it.
                            releaseViewForPointerUp();
                        }
                    }
                    clearMotionHistory(pointerId);
                    break;
                }
            case MotionEvent.ACTION_UP:
                {
                    if (mDragState == STATE_DRAGGING) {
                        releaseViewForPointerUp();
                    }
                    cancel();
                    break;
                }
            case MotionEvent.ACTION_CANCEL:
                {
                    if (mDragState == STATE_DRAGGING) {
                        dispatchViewReleased(0, 0);
                    }
                    cancel();
                    break;
                }
            default:
        }
    }

    /**
     * Check if we've crossed a reasonable touch slop for the given child view.
     * If the child cannot be dragged along the horizontal or vertical axis, motion
     * along that axis will not count toward the slop check.
     *
     * @param dx Motion since initial position along X axis
     * @param dy Motion since initial position along Y axis
     * @return true if the touch slop has been crossed
     */
    private boolean checkTouchSlop(float dx, float dy) {
        final boolean checkHorizontal = mSwipeConsumer.getHorizontalRange(dx, dy) > 0;
        final boolean checkVertical = mSwipeConsumer.getVerticalRange(dx, dy) > 0;
        if (checkHorizontal && checkVertical) {
            return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
        } else if (checkHorizontal) {
            return Math.abs(dx) > mTouchSlop;
        } else if (checkVertical) {
            return Math.abs(dy) > mTouchSlop;
        }
        return false;
    }

    private void releaseViewForPointerUp() {
        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
        final float xvel = clampMag(mVelocityTracker.getXVelocity(mActivePointerId), mMinVelocity, mMaxVelocity);
        final float yvel = clampMag(mVelocityTracker.getYVelocity(mActivePointerId), mMinVelocity, mMaxVelocity);
        dispatchViewReleased(xvel, yvel);
    }

    public boolean nestedScrollingTrySwipe(int dx, int dy, boolean fly) {
        return trySwipe(fly ? POINTER_NESTED_FLY : POINTER_NESTED_SCROLL, false, 0, 0, dx, dy, false);
    }

    public boolean nestedScrollingDrag(int dx, int dy, int[] consumed, boolean fly) {
        if (mDragState == STATE_IDLE) {
            return nestedScrollingTrySwipe(dx, dy, fly);
        }
        int clampedX = 0, clampedY = 0;
        if (mClampedDistanceX != 0 || dx != 0) {
            clampedX = mSwipeConsumer.clampDistanceHorizontal(mClampedDistanceX + dx, dx);
            consumed[0] = clampedX - mClampedDistanceX;
        }
        if (mClampedDistanceY != 0 || dy != 0) {
            clampedY = mSwipeConsumer.clampDistanceVertical(mClampedDistanceY + dy, dy);
            consumed[1] = clampedY - mClampedDistanceY;
        }
        if (mClampedDistanceX == 0 && mClampedDistanceY == 0 && consumed[0] == 0 && consumed[1] == 0) {
            mActivePointerId = INVALID_POINTER;
            setDragState(STATE_IDLE);
            return false;
        } else {
            dragTo(clampedX, clampedY, consumed[0], consumed[1]);
            return true;
        }
    }

    public void nestedScrollingRelease() {
        if (mDragState == STATE_NONE_TOUCH) {
            dispatchViewReleased(0, 0);
        }
    }

    private void dragTo(int x, int y, int dx, int dy) {
        int clampedX = x;
        int clampedY = y;
        final int oldX = mClampedDistanceX;
        final int oldY = mClampedDistanceY;
        if (dx != 0) {
            clampedX = mSwipeConsumer.clampDistanceHorizontal(x, dx);
            mClampedDistanceX = clampedX;
        }
        if (dy != 0) {
            clampedY = mSwipeConsumer.clampDistanceVertical(y, dy);
            mClampedDistanceY = clampedY;
        }
        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldX;
            final int clampedDy = clampedY - oldY;
            mSwipeConsumer.onSwipeDistanceChanged(clampedX, clampedY, clampedDx, clampedDy);
        }
    }

    public SwipeConsumer getSwipeConsumer() {
        return mSwipeConsumer;
    }

    private boolean isValidPointerForActionMove(int pointerId) {
        if (!isPointerDown(pointerId)) {
            Log.e(TAG, "Ignoring pointerId=" + pointerId + " because ACTION_DOWN was not received " + "for this pointer before ACTION_MOVE. It likely happened because " + " SwipeHelper did not receive all the events in the event stream.");
            return false;
        }
        return true;
    }

    public int getMaxSettleDuration() {
        return maxSettleDuration;
    }

    public void setMaxSettleDuration(int maxSettleDuration) {
        this.maxSettleDuration = maxSettleDuration;
    }
}

19 Source : StickyNavigationLayout_medlinker.java
with Apache License 2.0
from LightSun

/**
 * <p>
 * Note: @attr ref android.R.styleable#stickyLayout_content_id the content view must be the direct child of StickyNavigationLayout.
 * or else may cause bug.
 * </p>
 *
 * @author heaven7
 * @attr ref com.heaven7.android.sticky_navigation_layout.demo.R.styleable#stickyLayout_content_id
 */
public clreplaced StickyNavigationLayout extends LinearLayout implements NestedScrollingParent, NestedScrollingChild {

    private static final String TAG = "StickyNavLayout";

    private static final boolean DEBUG = false;

    private static final long ANIMATED_SCROLL_GAP = 250;

    /**
     * The view is not currently scrolling.
     *
     * @see #getScrollState()
     */
    public static final int SCROLL_STATE_IDLE = 0;

    /**
     * The view is currently being dragged by outside input such as user touch input.
     *
     * @see #getScrollState()
     */
    public static final int SCROLL_STATE_DRAGGING = 1;

    /**
     * The view is currently animating to a final position while not under
     * outside control.
     *
     * @see #getScrollState()
     */
    public static final int SCROLL_STATE_SETTLING = 2;

    /**
     * the top view
     */
    private View mTop;

    /**
     * the navigation view
     */
    private View mIndicator;

    /**
     * the child view which will be intercept
     */
    private View mContentView;

    private int mTopViewId;

    private int mIndicatorId;

    private int mContentId;

    private int mTopViewHeight;

    private boolean mTopHide = false;

    private GroupStickyDelegate mGroupStickyDelegate;

    private OverScroller mScroller;

    private VelocityTracker mVelocityTracker;

    private int mTouchSlop;

    private int mMaximumVelocity, mMinimumVelocity;

    private int mLastTouchY, mLastTouchX;

    private int mInitialTouchY, mInitialTouchX;

    private OnScrollChangeListener mScrollListener;

    private boolean mNeedIntercept;

    private int mScrollState = SCROLL_STATE_IDLE;

    private boolean mEnableStickyTouch = true;

    /**
     * last scroll time.
     */
    private long mLastScroll;

    /**
     * auto fit the sticky scroll
     */
    private final boolean mAutoFitScroll;

    /**
     * the percent of auto fix.
     */
    private float mAutoFitPercent = 0.5f;

    private final NestedScrollingParentHelper mNestedScrollingParentHelper;

    private final NestedScrollingChildHelper mNestedScrollingChildHelper;

    private boolean mNestedScrollInProgress;

    private int[] mParentScrollConsumed = new int[2];

    private final int[] mParentOffsetInWindow = new int[2];

    private final int[] mScrollConsumed = new int[2];

    private final int[] mNestedOffsets = new int[2];

    private final int[] mScrollOffset = new int[2];

    private int mScrollPointerId;

    public StickyNavigationLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // setOrientation(LinearLayout.VERTICAL);
        mGroupStickyDelegate = new GroupStickyDelegate();
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        mScroller = new OverScroller(context);
        // 触摸阙值
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyNavigationLayout);
        // mCodeSet will set stick view from onFinishInflate.
        mTopViewId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_top_id, 0);
        mIndicatorId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_indicator_id, 0);
        mContentId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_content_id, 0);
        mAutoFitScroll = a.getBoolean(R.styleable.StickyNavigationLayout_stickyLayout_auto_fit_scroll, false);
        a.recycle();
    // getWindowVisibleDisplayFrame(mExpectTopRect);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mTop = findViewById(mTopViewId);
        mIndicator = findViewById(mIndicatorId);
        mContentView = findViewById(mContentId);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mEnableStickyTouch && mContentView != null && mIndicator != null) {
            // 设置view的高度 (将mViewPager。的高度设置为  整个 Height - 导航的高度) - 被拦截的child view
            ViewGroup.LayoutParams params = mContentView.getLayoutParams();
            int expect = getMeasuredHeight() - mIndicator.getMeasuredHeight();
            // avoid onMeasure all the time
            if (params.height != expect) {
                params.height = getMeasuredHeight() - mIndicator.getMeasuredHeight();
            }
            if (DEBUG) {
                Logger.i(TAG, "onMeasure", "height = " + params.height + ", snv height = " + getMeasuredHeight());
                Logger.i(TAG, "onMeasure", "---> snv  bottom= " + getBottom());
            }
        }
        mGroupStickyDelegate.afterOnMeasure(this, mTop, mIndicator, mContentView);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (DEBUG) {
            Logger.i(TAG, "onLayout");
        }
        if (mEnableStickyTouch && mTop != null) {
            mTopViewHeight = mTop.getMeasuredHeight();
            final ViewGroup.LayoutParams lp = mTop.getLayoutParams();
            if (lp instanceof MarginLayoutParams) {
                mTopViewHeight += ((MarginLayoutParams) lp).topMargin + ((MarginLayoutParams) lp).bottomMargin;
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (DEBUG) {
            Logger.i(TAG, "onSizeChanged");
        }
    }

    /**
     * Return the current scrolling state of the RecyclerView.
     *
     * @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
     * {@link #SCROLL_STATE_SETTLING}
     */
    public int getScrollState() {
        return mScrollState;
    }

    /**
     * set if enable the sticky touch
     *
     * @param enable true to enable, false to disable.
     */
    public void setEnableStickyTouch(boolean enable) {
        if (mEnableStickyTouch != enable) {
            this.mEnableStickyTouch = enable;
            requestLayout();
        }
    }

    /**
     * is the sticky touch enabled.
     *
     * @return true to enable.
     */
    public boolean isStickyTouchEnabled() {
        return mEnableStickyTouch;
    }

    /**
     * use {@link #addStickyDelegate(IStickyDelegate)} instead.
     * set the sticky delegate.
     *
     * @param delegate the delegate
     */
    @Deprecated
    public void setStickyDelegate(IStickyDelegate delegate) {
        mGroupStickyDelegate.addStickyDelegate(delegate);
    }

    /**
     * add a sticky delegate
     *
     * @param delegate sticky delegate.
     */
    public void addStickyDelegate(IStickyDelegate delegate) {
        mGroupStickyDelegate.addStickyDelegate(delegate);
    }

    /**
     * remove a sticky delegate
     *
     * @param delegate sticky delegate.
     */
    public void removeStickyDelegate(IStickyDelegate delegate) {
        mGroupStickyDelegate.removeStickyDelegate(delegate);
    }

    /**
     * set the OnScrollChangeListener
     *
     * @param l the listener
     */
    public void setOnScrollChangeListener(OnScrollChangeListener l) {
        this.mScrollListener = l;
    }

    /*  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
        boolean canScrollHorizontally = canScrollHorizontally();
        boolean canScrollVertically = canScrollVertically();

        final int action = MotionEventCompat.getActionMasked(ev);
        final int actionIndex = MotionEventCompat.getActionIndex(ev);
        final int y = (int) (ev.getY() + 0.5f);
        final int x = (int) (ev.getX() + 0.5f);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mScrollPointerId = ev.getPointerId(0);
                mInitialTouchX = mLastTouchX = (int) (ev.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (ev.getY() + 0.5f);

                if (mScrollState == SCROLL_STATE_SETTLING) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                }

                // Clear the nested offsets
                mNestedOffsets[0] = mNestedOffsets[1] = 0;

                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis);
                break;

            case MotionEventCompat.ACTION_POINTER_DOWN:
                mScrollPointerId = ev.getPointerId(actionIndex);
                mInitialTouchX = mLastTouchX = x;
                mInitialTouchY = mLastTouchY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                final int index = ev.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;

                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
                break;

            case MotionEventCompat.ACTION_POINTER_UP: {
                onPointerUp(ev);
            }
            break;

            case MotionEvent.ACTION_UP: {
                mVelocityTracker.clear();
                stopNestedScroll();
            }
            break;

            case MotionEvent.ACTION_CANCEL: {
                cancelTouch();
            }
        }

        return mScrollState == SCROLL_STATE_DRAGGING;
    }*/
    /* @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Logger.i(TAG, "onInterceptTouchEvent", ev.toString());
        if(ev.getAction() == MotionEvent.ACTION_UP){
            checkAutoFitScroll();
        }else if(ev.getAction() == MotionEvent.ACTION_DOWN){
            mTotalDy = 0;
        }
        return super.onInterceptTouchEvent(ev);
    }*/
    private void onPointerUp(MotionEvent e) {
        final int actionIndex = MotionEventCompat.getActionIndex(e);
        if (e.getPointerId(actionIndex) == mScrollPointerId) {
            // Pick a new pointer to pick up the slack.
            final int newIndex = actionIndex == 0 ? 1 : 0;
            mScrollPointerId = e.getPointerId(newIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
        }
    }

    private boolean canScrollHorizontally() {
        return false;
    }

    private boolean canScrollVertically() {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mEnableStickyTouch) {
            return super.onTouchEvent(event);
        }
        final boolean canScrollHorizontally = canScrollHorizontally();
        final boolean canScrollVertically = canScrollVertically();
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        boolean eventAddedToVelocityTracker = false;
        final MotionEvent vtev = MotionEvent.obtain(event);
        final int action = MotionEventCompat.getActionMasked(event);
        final int actionIndex = MotionEventCompat.getActionIndex(event);
        if (action == MotionEvent.ACTION_DOWN) {
            mNestedOffsets[0] = mNestedOffsets[1] = 0;
        }
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                {
                    if (!mScroller.isFinished())
                        mScroller.abortAnimation();
                    mScrollPointerId = event.getPointerId(0);
                    mInitialTouchX = mLastTouchX = (int) (event.getX() + 0.5f);
                    mInitialTouchY = mLastTouchY = (int) (event.getY() + 0.5f);
                    int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                    if (canScrollHorizontally) {
                        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                    }
                    if (canScrollVertically) {
                        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                    }
                    startNestedScroll(nestedScrollAxis);
                }
                break;
            case MotionEventCompat.ACTION_POINTER_DOWN:
                {
                    mScrollPointerId = event.getPointerId(actionIndex);
                    mInitialTouchX = mLastTouchX = (int) (event.getX(actionIndex) + 0.5f);
                    mInitialTouchY = mLastTouchY = (int) (event.getY(actionIndex) + 0.5f);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                {
                    // 遵循Google规范(比如recyclerView源代码)。避免处理嵌套滑动出问题。
                    final int index = event.findPointerIndex(mScrollPointerId);
                    if (index < 0) {
                        Log.e(TAG, "Error processing scroll; pointer index for id " + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                        return false;
                    }
                    final int x = (int) (event.getX(index) + 0.5f);
                    final int y = (int) (event.getY(index) + 0.5f);
                    int dx = mLastTouchX - x;
                    int dy = mLastTouchY - y;
                    if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                        dx -= mScrollConsumed[0];
                        dy -= mScrollConsumed[1];
                        vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                        // Updated the nested offsets
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                    }
                    if (mScrollState != SCROLL_STATE_DRAGGING) {
                        boolean startScroll = false;
                        if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                            if (dx > 0) {
                                dx -= mTouchSlop;
                            } else {
                                dx += mTouchSlop;
                            }
                            startScroll = true;
                        }
                        if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                            if (dy > 0) {
                                dy -= mTouchSlop;
                            } else {
                                dy += mTouchSlop;
                            }
                            startScroll = true;
                        }
                        if (startScroll) {
                            setScrollState(SCROLL_STATE_DRAGGING);
                        }
                    }
                    if (mScrollState == SCROLL_STATE_DRAGGING) {
                        mLastTouchX = x - mScrollOffset[0];
                        mLastTouchY = y - mScrollOffset[1];
                        // 手向下滑动, dy >0 否则 <0.
                        if (scrollByInternal(0, dy, vtev)) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                {
                    onPointerUp(event);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                cancelTouch();
                break;
            case MotionEvent.ACTION_UP:
                mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                final float xvel = (int) mVelocityTracker.getXVelocity();
                final float yvel = (int) mVelocityTracker.getYVelocity();
                // final float xvel = -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
                break;
        }
        if (!eventAddedToVelocityTracker) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }

    private void cancelTouch() {
        /*  if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }*/
        resetTouch();
        setScrollState(SCROLL_STATE_IDLE);
    }

    private void resetTouch() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
        }
        stopNestedScroll();
    // releaseGlows();
    }

    /**
     * Begin a standard fling with an initial velocity along each axis in pixels per second.
     * If the velocity given is below the system-defined minimum this method will return false
     * and no fling will occur.
     *
     * @param velocityX Initial horizontal velocity in pixels per second
     * @param velocityY Initial vertical velocity in pixels per second
     * @return true if the fling was started, false if the velocity was too low to fling or
     * LayoutManager does not support scrolling in the axis fling is issued.
     */
    public boolean fling(int velocityX, int velocityY) {
        // TODO
        return false;
    }

    void setScrollState(int state) {
        if (state == mScrollState) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState, new Exception());
        }
        mScrollState = state;
        if (state != SCROLL_STATE_SETTLING) {
            // stopScrollersInternal();
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
        }
        dispatchOnScrollStateChanged(state);
    }

    private void dispatchOnScrollStateChanged(int state) {
        if (mScrollListener != null) {
            mScrollListener.onScrollStateChanged(this, state);
        }
    }

    /**
     * Does not perform bounds checking. Used by internal methods that have already validated input.
     * <p/>
     * It also reports any unused scroll request to the related EdgeEffect.
     *
     * @param dx The amount of horizontal scroll request
     * @param dy The amount of vertical scroll request
     * @param ev The originating MotionEvent, or null if not from a touch event.
     * @return Whether any scroll was consumed in either direction.
     */
    boolean scrollByInternal(int dx, int dy, MotionEvent ev) {
        mScrollConsumed[0] = 0;
        mScrollConsumed[1] = 0;
        scrollInternal(dx, dy, mScrollConsumed);
        int consumedX = mScrollConsumed[0];
        int consumedY = mScrollConsumed[1];
        int unconsumedX = dx - mScrollConsumed[0];
        int unconsumedY = dy - mScrollConsumed[1];
        if (dispatchNestedScroll(mScrollConsumed[0], mScrollConsumed[1], unconsumedX, unconsumedY, mScrollOffset)) {
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return mScrollConsumed[0] != 0 || mScrollConsumed[1] != 0;
    }

    /**
     * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
     *
     * @param x the position where to scroll on the X axis
     * @param y the position where to scroll on the Y axis
     */
    public final void smoothScrollTo(int x, int y) {
        smoothScrollBy(x - getScrollX(), y - getScrollY());
    }

    /**
     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
     *
     * @param dx the number of pixels to scroll by on the X axis
     * @param dy the number of pixels to scroll by on the Y axis
     */
    public final void smoothScrollBy(int dx, int dy) {
        if (getChildCount() == 0) {
            // Nothing to do.
            return;
        }
        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
        if (duration > ANIMATED_SCROLL_GAP) {
            // design from scrollView
            mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            scrollBy(dx, dy);
        }
        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
    }

    private static String getStateString(int state) {
        switch(state) {
            case SCROLL_STATE_DRAGGING:
                return "SCROLL_STATE_DRAGGING";
            case SCROLL_STATE_SETTLING:
                return "SCROLL_STATE_SETTLING";
            case SCROLL_STATE_IDLE:
            default:
                return "SCROLL_STATE_IDLE";
        }
    }

    public void fling(int velocityY) {
        // 使得当前对象只滑动到mTopViewHeight
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    @Override
    public void computeScroll() {
        // super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            // true滑动尚未完成
            scrollTo(0, mScroller.getCurrY());
            // invalidate();
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    void dispatchOnScrolled(int hresult, int vresult) {
        // Preplaced the current scrollX/scrollY values; no actual change in these properties occurred
        // but some general-purpose code may choose to respond to changes this way.
        final int scrollX = getScrollX();
        final int scrollY = getScrollY();
        onScrollChanged(scrollX, scrollY, scrollX, scrollY);
        // Preplaced the real deltas to onScrolled, the RecyclerView-specific method.
        onScrolled(hresult, vresult);
        // Invoke listeners last. Subclreplaceded view methods always handle the event first.
        // All internal state is consistent by the time listeners are invoked.
        if (mScrollListener != null) {
            mScrollListener.onScrolled(this, hresult, vresult);
        }
    }

    protected void onScrolled(int dx, int dy) {
    // dy > 0 ? gesture up :gesture down
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final SaveState saveState = new SaveState(super.onSaveInstanceState());
        saveState.mNeedIntercept = this.mNeedIntercept;
        saveState.mTopHide = this.mTopHide;
        saveState.mLastScroll = this.mLastScroll;
        saveState.mEnableStickyTouch = this.mEnableStickyTouch;
        return saveState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if (state != null) {
            SaveState ss = (SaveState) state;
            this.mNeedIntercept = ss.mNeedIntercept;
            this.mTopHide = ss.mTopHide;
            this.mEnableStickyTouch = ss.mEnableStickyTouch;
            this.mLastScroll = ss.mLastScroll;
        }
    }

    protected static clreplaced SaveState extends BaseSavedState {

        boolean mNeedIntercept;

        boolean mTopHide;

        boolean mEnableStickyTouch;

        long mLastScroll;

        public SaveState(Parcel source) {
            super(source);
            mNeedIntercept = source.readByte() == 1;
            mTopHide = source.readByte() == 1;
            mEnableStickyTouch = source.readByte() == 1;
            mLastScroll = source.readLong();
        }

        public SaveState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeByte((byte) (mNeedIntercept ? 1 : 0));
            out.writeByte((byte) (mTopHide ? 1 : 0));
            out.writeByte((byte) (mEnableStickyTouch ? 1 : 0));
            out.writeLong(mLastScroll);
        }

        public static final Parcelable.Creator<SaveState> CREATOR = new Parcelable.Creator<SaveState>() {

            @Override
            public SaveState createFromParcel(Parcel in) {
                return new SaveState(in);
            }

            @Override
            public SaveState[] newArray(int size) {
                return new SaveState[size];
            }
        };
    }

    /**
     * the internal group Sticky Delegate.
     */
    private clreplaced GroupStickyDelegate implements IStickyDelegate {

        private final ArrayList<IStickyDelegate> mDelegates = new ArrayList<>(5);

        public void addStickyDelegate(IStickyDelegate delegate) {
            mDelegates.add(delegate);
        }

        public void removeStickyDelegate(IStickyDelegate delegate) {
            mDelegates.remove(delegate);
        }

        public void clear() {
            mDelegates.clear();
        }

        @Override
        public void afterOnMeasure(StickyNavigationLayout snv, View top, View indicator, View content) {
            for (IStickyDelegate delegate : mDelegates) {
                delegate.afterOnMeasure(snv, top, indicator, content);
            }
        }
    }

    /**
     * on scroll  change listener.
     */
    public interface OnScrollChangeListener {

        /**
         * called when the scroll state change
         *
         * @param snl            the {@link StickyNavigationLayout}
         * @param state          the scroll state . see {@link StickyNavigationLayout#SCROLL_STATE_IDLE} and etc.
         */
        void onScrollStateChanged(StickyNavigationLayout snl, int state);

        /**
         * Callback method to be invoked when the RecyclerView has been scrolled. This will be
         * called after the scroll has completed.
         * <p>
         * This callback will also be called if visible item range changes after a layout
         * calculation. In that case, dx and dy will be 0.
         *
         * @param snl the {@link StickyNavigationLayout} which scrolled.
         * @param dx  The amount of horizontal scroll.
         * @param dy  The amount of vertical scroll.
         */
        void onScrolled(StickyNavigationLayout snl, int dx, int dy);
    }

    /**
     * the sticky delegate
     */
    public interface IStickyDelegate {

        /**
         *  called after the {@link StickyNavigationLayout#onMeasure(int, int)}. this is useful used when we want to
         *  toggle two views visibility in {@link StickyNavigationLayout}(or else may cause bug). see it in demo.
         * @param snv the {@link StickyNavigationLayout}
         * @param top the top view
         * @param indicator the indicator view
         * @param contentView the content view
         */
        void afterOnMeasure(StickyNavigationLayout snv, View top, View indicator, View contentView);
    }

    /**
     * a simple implements of {@link IStickyDelegate}
     */
    public static clreplaced SimpleStickyDelegate implements IStickyDelegate {

        @Override
        public void afterOnMeasure(StickyNavigationLayout snv, View top, View indicator, View contentView) {
        }
    }

    // ========================  NestedScrollingParent begin ========================
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return isEnabled() && mEnableStickyTouch && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        // Reset the counter of how much leftover scroll needs to be consumed.
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        // Dispatch up to the nested parent
        startNestedScroll(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL);
        mNestedScrollInProgress = true;
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        scrollInternal(dx, dy, consumed);
        // Now let our nested parent consume the leftovers
        final int[] parentConsumed = mParentScrollConsumed;
        if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
            consumed[0] += parentConsumed[0];
            consumed[1] += parentConsumed[1];
        }
    }

    /**
     * do scroll internal.
     *
     * @param dx       the delta x, may be negative
     * @param dy       the delta y , may be negative
     * @param consumed optional , in not null will contains the consumed x and y by this scroll.
     * @return the consumed x and y as array by this scroll.
     */
    private int[] scrollInternal(int dx, int dy, int[] consumed) {
        // 向上滑 dy >0 , 下滑 dy < 0
        // Logger.i(TAG, "scrollInternal", "dx = " + dx + ",dy = " + dy + " ,consumed = " + Arrays.toString(consumed));
        // >0
        final int scrollY = getScrollY();
        if (consumed == null) {
            consumed = new int[2];
        }
        int by = 0;
        if (dy > 0) {
            // gesture up
            if (scrollY < mTopViewHeight) {
                int maxH = mTopViewHeight - scrollY;
                by = consumed[1] = Math.min(dy, maxH);
            } else {
            // ignore
            }
        } else {
            // gesture down
            if (scrollY > 0) {
                consumed[1] = -Math.min(Math.abs(dy), scrollY);
                by = consumed[1];
            }
        }
        scrollBy(0, by);
        return consumed;
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        /* net
       inal int myConsumed = moveBy(dyUnconsumed);
        final int myUnconsumed = dyUnconsumed - myConsumed;
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);*/
        // Dispatch up to the nested parent first
        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
        // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
        // sometimes between two nested scrolling views, we need a way to be able to know when any
        // nested scrolling parent has stopped handling events. We do that by using the
        // 'offset in window 'functionality to see if we have been moved from the event.
        // This is a decent indication of whether we should take over the event stream or not.
        final int dy = dyUnconsumed + mParentOffsetInWindow[1];
    // Logger.i(TAG, "onNestedScroll", "mTotalUnconsumed = " +   (mTotalUnconsumed + Math.abs(dy))  );
    /* if (dy < 0 && !canChildScrollUp()) {
            mTotalUnconsumed += Math.abs(dy);
          //  moveSpinner(mTotalUnconsumed);
        }*/
    }

    @Override
    public void onStopNestedScroll(View target) {
        // Logger.i(TAG, "onStopNestedScroll");
        checkAutoFitScroll();
        mNestedScrollingParentHelper.onStopNestedScroll(target);
        mNestedScrollInProgress = false;
        // Dispatch up our nested parent
        stopNestedScroll();
    }

    private void checkAutoFitScroll() {
        // check auto fit scroll
        if (mAutoFitScroll) {
            // check whole gesture.
            final float scrollY = getScrollY();
            if (scrollY >= mTopViewHeight * mAutoFitPercent) {
                smoothScrollTo(0, mTopViewHeight);
            } else {
                smoothScrollTo(0, 0);
            }
        /* if (mTotalDy > 0) {
                //finger up
                if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
                    smoothScrollTo(0, mTopViewHeight);
                } else {
                    smoothScrollTo(0, 0);
                }
            } else {
                //finger down
                //if larger the 1/2 * maxHeight go to maxHeight
                if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
                    smoothScrollTo(0, mTopViewHeight);
                } else {
                    smoothScrollTo(0, 0);
                }
            }*/
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    // ========================  NestedScrollingParent end ========================
    // ========================  NestedScrollingChild begin ========================
    public void setNestedScrollingEnabled(boolean enabled) {
        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    public boolean isNestedScrollingEnabled() {
        return mNestedScrollingChildHelper.isNestedScrollingEnabled();
    }

    public boolean startNestedScroll(int axes) {
        return mNestedScrollingChildHelper.startNestedScroll(axes);
    }

    public void stopNestedScroll() {
        mNestedScrollingChildHelper.stopNestedScroll();
    }

    public boolean hasNestedScrollingParent() {
        return mNestedScrollingChildHelper.hasNestedScrollingParent();
    }

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
    // ======================== end NestedScrollingChild =====================
}

19 Source : StickyNavigationLayout_backup.java
with Apache License 2.0
from LightSun

/**
 * sticky navigation layout:similar to google+ app.
 * <p>
 * Note: @attr ref android.R.styleable#stickyLayout_content_id the content view must be the direct child of StickyNavigationLayout.
 * or else may cause bug.
 * </p>
 *
 * @author heaven7
 * @attr ref com.heaven7.android.sticky_navigation_layout.demo.R.styleable#stickyLayout_content_id
 */
public clreplaced StickyNavigationLayout_backup extends LinearLayout implements NestedScrollingParent, NestedScrollingChild {

    private static final String TAG = "StickyNavLayout";

    private static final boolean DEBUG = true;

    private static final long ANIMATED_SCROLL_GAP = 250;

    /**
     * indicate the scroll state is just end
     */
    public static final int SCROLL_STATE_IDLE = 1;

    /**
     * indicate the scroll state is just begin.
     */
    public static final int SCROLL_STATE_START = 2;

    /**
     * indicate the scroll state is setting/scrolling.
     */
    public static final int SCROLL_STATE_SETTING = 3;

    /**
     * the view state is shown.
     */
    public static final int VIEW_STATE_SHOW = 1;

    /**
     * the view state is hide
     */
    public static final int VIEW_STATE_HIDE = 2;

    /**
     * the top view
     */
    private View mTop;

    /**
     * the navigation view
     */
    private View mIndicator;

    /**
     * the child view which will be intercept
     */
    private View mContentView;

    private int mTopViewId;

    private int mIndicatorId;

    private int mContentId;

    private int mTopViewHeight;

    private boolean mTopHide = false;

    private GroupStickyDelegate mGroupStickyDelegate;

    private OverScroller mScroller;

    private VelocityTracker mVelocityTracker;

    private int mTouchSlop;

    private int mMaximumVelocity, mMinimumVelocity;

    private int mLastY, mLastX;

    private int mDownY, mDownX;

    private boolean mDragging;

    private OnScrollChangeListener mScrollListener;

    private boolean mNeedIntercept;

    private int mScrollState = SCROLL_STATE_IDLE;

    private int mFocusDir;

    private boolean mEnableStickyTouch = true;

    private int mTotalDy;

    /**
     * last scroll time.
     */
    private long mLastScroll;

    /**
     * auto fit the sticky scroll
     */
    private final boolean mAutoFitScroll;

    /**
     * the percent of auto fix.
     */
    private float mAutoFitPercent = 0.5f;

    /**
     * code set the sticky view ,not from xml.
     */
    private boolean mCodeSet;

    private final NestedScrollingParentHelper mNestedScrollingParentHelper;

    private final NestedScrollingChildHelper mNestedScrollingChildHelper;

    private int mTotalUnconsumed;

    private int[] mParentScrollConsumed = new int[2];

    private boolean mNestedScrollInProgress;

    private int[] mParentOffsetInWindow = new int[2];

    public StickyNavigationLayout_backup(Context context, AttributeSet attrs) {
        super(context, attrs);
        // setOrientation(LinearLayout.VERTICAL);
        mGroupStickyDelegate = new GroupStickyDelegate();
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        mScroller = new OverScroller(context);
        // 触摸阙值
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StickyNavigationLayout);
        // mCodeSet will set stick view from onFinishInflate.
        if (!mCodeSet) {
            mTopViewId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_top_id, 0);
            mIndicatorId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_indicator_id, 0);
            mContentId = a.getResourceId(R.styleable.StickyNavigationLayout_stickyLayout_content_id, 0);
        }
        mAutoFitScroll = a.getBoolean(R.styleable.StickyNavigationLayout_stickyLayout_auto_fit_scroll, false);
        mAutoFitPercent = a.getFloat(R.styleable.StickyNavigationLayout_stickyLayout_threshold_percent, 0.5f);
        a.recycle();
    // getWindowVisibleDisplayFrame(mExpectTopRect);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mTop = findViewById(mTopViewId);
        mIndicator = findViewById(mIndicatorId);
        mContentView = findViewById(mContentId);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mGroupStickyDelegate.afterOnMeasure(this, mTop, mIndicator, mContentView);
        if (mEnableStickyTouch && mContentView != null && mIndicator != null) {
            // 设置view的高度 (将mViewPager。的高度设置为  整个 Height - 导航的高度) - 被拦截的child view
            ViewGroup.LayoutParams params = mContentView.getLayoutParams();
            params.height = getMeasuredHeight() - mIndicator.getMeasuredHeight();
            if (DEBUG) {
                Logger.i(TAG, "onMeasure", "height = " + params.height + ", snv height = " + getMeasuredHeight());
                Logger.i(TAG, "onMeasure", "---> snv  bottom= " + getBottom());
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (DEBUG) {
            Logger.i(TAG, "onLayout");
        }
        if (mEnableStickyTouch && mTop != null) {
            mTopViewHeight = mTop.getMeasuredHeight();
            final ViewGroup.LayoutParams lp = mTop.getLayoutParams();
            if (lp instanceof MarginLayoutParams) {
                mTopViewHeight += ((MarginLayoutParams) lp).topMargin + ((MarginLayoutParams) lp).bottomMargin;
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (DEBUG) {
            Logger.i(TAG, "onSizeChanged");
        }
    }

    /**
     * set if enable the sticky touch
     * @param enable true to enable, false to disable.
     */
    public void setEnableStickyTouch(boolean enable) {
        if (mEnableStickyTouch != enable) {
            this.mEnableStickyTouch = enable;
            requestLayout();
        }
    }

    /**
     * is the sticky touch enabled.
     * @return true to enable.
     */
    public boolean isStickyTouchEnabled() {
        return mEnableStickyTouch;
    }

    public int getTopViewState() {
        return mTopHide ? VIEW_STATE_HIDE : VIEW_STATE_SHOW;
    }

    /**
     * set the sticky views
     *
     * @param top       the top view
     * @param indicator the indicator view
     * @param content     the content view
     */
    /*public*/
    void setStickyViews(View top, View indicator, View content) {
        if (top == null || indicator == null || content == null) {
            throw new NullPointerException();
        }
        mTopViewId = top.getId();
        mIndicatorId = indicator.getId();
        mContentId = content.getId();
        mTop = top;
        mIndicator = indicator;
        mContentView = content;
        mCodeSet = true;
        requestLayout();
    }

    /**
     * use {@link #addStickyDelegate(IStickyDelegate)} instead.
     * set the sticky delegate.
     * @param delegate the delegate
     */
    @Deprecated
    public void setStickyDelegate(IStickyDelegate delegate) {
        mGroupStickyDelegate.addStickyDelegate(delegate);
    }

    /**
     * add a sticky delegate
     * @param delegate sticky delegate.
     */
    public void addStickyDelegate(IStickyDelegate delegate) {
        mGroupStickyDelegate.addStickyDelegate(delegate);
    }

    /**
     * remove a sticky delegate
     * @param delegate sticky delegate.
     */
    public void removeStickyDelegate(IStickyDelegate delegate) {
        mGroupStickyDelegate.removeStickyDelegate(delegate);
    }

    /**
     * set the OnScrollChangeListener
     *
     * @param l the listener
     */
    public void setOnScrollChangeListener(OnScrollChangeListener l) {
        this.mScrollListener = l;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /* if (!mEnableStickyTouch) {
            return super.onInterceptTouchEvent(ev);
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);*/
        // 1, 上拉的时候,停靠后分发给child滑动. max = mTopViewHeight
        // 2, 下拉时,先拉上面的,拉完后分发给child.
        /*  int action = ev.getAction();
        int y = (int) (ev.getY() + 0.5f);
        int x = (int) (ev.getY() + 0.5f);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownY = mLastY = y;
                mDownX = mLastX = x;
                break;

            case MotionEvent.ACTION_MOVE:
                int dy = y - mLastY;

                if (Math.abs(dy) > mTouchSlop) {
                    if (mNeedIntercept) {
                        return true;
                    }
                    if (dy > 0) {
                        return getScrollY() == mTopViewHeight;
                    }
                    if (mGroupStickyDelegate.shouldIntercept(this, dy,
                            mTopHide ? VIEW_STATE_HIDE : VIEW_STATE_SHOW)) {
                        mDragging = true;
                        return true;
                    }
                }
                break;
        }*/
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mEnableStickyTouch) {
            return super.onInterceptTouchEvent(event);
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        int action = event.getAction();
        int y = (int) (event.getY() + 0.5f);
        int x = (int) (event.getY() + 0.5f);
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished())
                    mScroller.abortAnimation();
                // mVelocityTracker.addMovement(event);
                mDownY = mLastY = y;
                mDownX = mLastX = x;
                return true;
            case MotionEvent.ACTION_MOVE:
                int dy = y - mLastY;
                int dx = x - mLastX;
                if (!mDragging && Math.abs(dy) > mTouchSlop) {
                    mDragging = true;
                }
                if (mDragging) {
                    // 手向下滑动, dy >0 否则 <0.
                    final int scrollY = getScrollY();
                    final int totalDy = mTotalDy = y - mDownY;
                    // Logger.i(TAG, "onTouchEvent", "ScrollY = " + scrollY + " ,dy = " + dy + " , mTopViewHeight = " + mTopViewHeight);
                    mLastY = y;
                    mLastX = x;
                    mFocusDir = dy < 0 ? View.FOCUS_DOWN : View.FOCUS_UP;
                    setScrollState(SCROLL_STATE_START);
                    onPreScrollDistanceChange(0, dy, 0, totalDy);
                    if (dy < 0) {
                        // 手势向上滑动 ,view down
                        /**
                         *  called [ onTouchEvent() ]: ScrollY = 666 ,dy = -7.4692383 , mTopViewHeight = 788
                         *  called [ onTouchEvent() ]: ScrollY = 673 ,dy = -3.748291 , mTopViewHeight = 788
                         */
                        if (scrollY == mTopViewHeight) {
                            // 分发给child
                            mGroupStickyDelegate.dispatchTouchEventToChild(this, dx, dy, MotionEvent.obtain(event));
                        } else if (scrollY - dy > mTopViewHeight) {
                            // top height is the max scroll height
                            scrollTo(getScrollX(), mTopViewHeight);
                        } else {
                            scrollBy(0, -dy);
                        }
                    } else {
                        // 手势向下
                        if (scrollY == 0) {
                            // 分发事件给child
                            // mGroupStickyDelegate.scrollBy(this, dy);
                            mGroupStickyDelegate.dispatchTouchEventToChild(this, dx, dy, MotionEvent.obtain(event));
                        } else {
                            if (scrollY - dy < 0) {
                                dy = scrollY;
                            }
                            scrollBy(0, -dy);
                        }
                    }
                    onAfterScrollDistanceChange(0, dy, 0, totalDy);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                mDragging = false;
                if (!mScroller.isFinished()) {
                    // 如果手离开了,就终止滑动
                    mScroller.abortAnimation();
                }
                setScrollState(SCROLL_STATE_IDLE);
                mFocusDir = 0;
                break;
            case MotionEvent.ACTION_UP:
                if (Math.abs(y - mLastY) > mTouchSlop) {
                    // 1000表示像素/秒
                    mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int velocityY = (int) mVelocityTracker.getYVelocity();
                    if (Math.abs(velocityY) > mMinimumVelocity) {
                        if (DEBUG) {
                            Logger.i(TAG, "onTouchEvent", "begin fling: velocityY = " + velocityY);
                        }
                        // fling(-velocityY);
                        smoothScrollTo(0, mTotalDy < 0 ? mTopViewHeight : 0);
                    }
                } else {
                    // check auto fit scroll
                    if (mAutoFitScroll) {
                        // check whole gesture.
                        if (mTotalDy < 0) {
                            // finger up
                            // if larger the 1/2 * maxHeight go to maxHeight
                            if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
                                smoothScrollTo(0, mTopViewHeight);
                            } else {
                                smoothScrollTo(0, 0);
                            }
                        } else {
                            // finger down
                            if (Math.abs(mTotalDy) >= mTopViewHeight * mAutoFitPercent) {
                                smoothScrollTo(0, 0);
                            } else {
                                smoothScrollTo(0, mTopViewHeight);
                            }
                        }
                    }
                }
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                mDragging = false;
                setScrollState(SCROLL_STATE_IDLE);
                mFocusDir = 0;
                mGroupStickyDelegate.onTouchEventUp(this, event);
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
     *
     * @param x the position where to scroll on the X axis
     * @param y the position where to scroll on the Y axis
     */
    public final void smoothScrollTo(int x, int y) {
        smoothScrollBy(x - getScrollX(), y - getScrollY());
    }

    /**
     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
     *
     * @param dx the number of pixels to scroll by on the X axis
     * @param dy the number of pixels to scroll by on the Y axis
     */
    public final void smoothScrollBy(int dx, int dy) {
        if (getChildCount() == 0) {
            // Nothing to do.
            return;
        }
        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
        if (duration > ANIMATED_SCROLL_GAP) {
            // design from scrollView
            mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            scrollBy(dx, dy);
        }
        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
    }

    /**
     * get the focus direction . 0 or  {@link View#FOCUS_DOWN} or {@link View#FOCUS_UP}
     *
     * @return the focus direction
     */
    public int getFocusDirection() {
        return mFocusDir;
    }

    /**
     * called pre ths scroll distance change. may be negative .
     *
     * @param dx      the delta x between this touch and last touch
     * @param dy      the delta y between this touch and last touch
     * @param totalDx the delta x between this touch and first touch
     * @param totalDy the delta y between this touch and first touch
     */
    private void onPreScrollDistanceChange(int dx, int dy, int totalDx, int totalDy) {
        if (mScrollListener != null) {
            mScrollListener.onPreScrollDistanceChange(this, dx, dy, totalDx, totalDy);
        } else {
            if (getContext() instanceof OnScrollChangeListener) {
                ((OnScrollChangeListener) getContext()).onPreScrollDistanceChange(this, dx, dy, totalDx, totalDy);
            }
        }
    }

    /**
     * called after ths scroll distance change. may be negative .
     *
     * @param dx      the delta x between this touch and last touch
     * @param dy      the delta y between this touch and last touch
     * @param totalDx the delta x between this touch and first touch
     * @param totalDy the delta y between this touch and first touch
     */
    private void onAfterScrollDistanceChange(int dx, int dy, int totalDx, int totalDy) {
        if (mScrollListener != null) {
            mScrollListener.onAfterScrollDistanceChange(this, dx, dy, totalDx, totalDy);
        } else {
            if (getContext() instanceof OnScrollChangeListener) {
                ((OnScrollChangeListener) getContext()).onAfterScrollDistanceChange(this, dx, dy, totalDx, totalDy);
            }
        }
    }

    /**
     * called when the scroll state change
     *
     * @param expectScrollState the expect state.
     */
    private void setScrollState(int expectScrollState) {
        expectScrollState = adjustState(expectScrollState);
        if (mScrollState == expectScrollState) {
            // ignore
            return;
        }
        if (DEBUG) {
            Logger.i(TAG, "setScrollState", "new state = " + getStateString(expectScrollState));
        }
        mScrollState = expectScrollState;
        if (mScrollListener != null) {
            mScrollListener.onScrollStateChange(this, expectScrollState, mFocusDir);
        } else {
            if (getContext() instanceof OnScrollChangeListener) {
                ((OnScrollChangeListener) getContext()).onScrollStateChange(this, expectScrollState, mFocusDir);
            }
        }
    }

    private static String getStateString(int state) {
        switch(state) {
            case SCROLL_STATE_START:
                return "SCROLL_STATE_START";
            case SCROLL_STATE_SETTING:
                return "SCROLL_STATE_SETTING";
            case SCROLL_STATE_IDLE:
            default:
                return "SCROLL_STATE_IDLE";
        }
    }

    private int adjustState(int expectScrollState) {
        switch(expectScrollState) {
            case SCROLL_STATE_START:
                {
                    if (mScrollState == SCROLL_STATE_START) {
                        return SCROLL_STATE_SETTING;
                    } else if (mScrollState == SCROLL_STATE_IDLE) {
                        return expectScrollState;
                    } else {
                        return SCROLL_STATE_SETTING;
                    }
                }
            case SCROLL_STATE_SETTING:
            case SCROLL_STATE_IDLE:
            default:
                return expectScrollState;
        }
    }

    public void fling(int velocityY) {
        // 使得当前对象只滑动到mTopViewHeight
        mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    // 限定滑动的y范围
    @Override
    public void scrollTo(int x, int y) {
        // Logger.i(TAG, "scrollTo", "x = " + x + ", y = " + y);
        if (y < 0) {
            y = 0;
        }
        // maxY =  mTopViewHeight
        if (y > mTopViewHeight) {
            y = mTopViewHeight;
        }
        if (y == 0 || y == mTopViewHeight) {
            mNeedIntercept = false;
        } else {
            mNeedIntercept = true;
        }
        super.scrollTo(x, y);
        mTopHide = getScrollY() == mTopViewHeight;
    }

    @Override
    public void computeScroll() {
        // super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            // true滑动尚未完成
            scrollTo(0, mScroller.getCurrY());
            // invalidate();
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final SaveState saveState = new SaveState(super.onSaveInstanceState());
        saveState.mNeedIntercept = this.mNeedIntercept;
        saveState.mTopHide = this.mTopHide;
        saveState.mLastScroll = this.mLastScroll;
        saveState.mEnableStickyTouch = this.mEnableStickyTouch;
        saveState.mCodeSet = this.mCodeSet;
        saveState.mTopViewId = this.mTopViewId;
        saveState.mIndicatorId = this.mIndicatorId;
        saveState.mContentId = this.mContentId;
        return saveState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if (state != null) {
            SaveState ss = (SaveState) state;
            this.mNeedIntercept = ss.mNeedIntercept;
            this.mTopHide = ss.mTopHide;
            this.mEnableStickyTouch = ss.mEnableStickyTouch;
            this.mLastScroll = ss.mLastScroll;
            this.mCodeSet = ss.mCodeSet;
            this.mTopViewId = ss.mTopViewId;
            this.mIndicatorId = ss.mIndicatorId;
            this.mContentId = ss.mContentId;
        }
    }

    protected static clreplaced SaveState extends BaseSavedState {

        boolean mNeedIntercept;

        boolean mTopHide;

        boolean mEnableStickyTouch;

        long mLastScroll;

        boolean mCodeSet;

        int mTopViewId;

        int mIndicatorId;

        int mContentId;

        public SaveState(Parcel source) {
            super(source);
            mNeedIntercept = source.readByte() == 1;
            mTopHide = source.readByte() == 1;
            mEnableStickyTouch = source.readByte() == 1;
            mLastScroll = source.readLong();
            mCodeSet = source.readByte() == 1;
            mTopViewId = source.readInt();
            mIndicatorId = source.readInt();
            mContentId = source.readInt();
        }

        public SaveState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeByte((byte) (mNeedIntercept ? 1 : 0));
            out.writeByte((byte) (mTopHide ? 1 : 0));
            out.writeByte((byte) (mEnableStickyTouch ? 1 : 0));
            out.writeLong(mLastScroll);
            out.writeByte((byte) (mCodeSet ? 1 : 0));
            out.writeInt(mTopViewId);
            out.writeInt(mIndicatorId);
            out.writeInt(mContentId);
        }

        public static final Creator<SaveState> CREATOR = new Creator<SaveState>() {

            @Override
            public SaveState createFromParcel(Parcel in) {
                return new SaveState(in);
            }

            @Override
            public SaveState[] newArray(int size) {
                return new SaveState[size];
            }
        };
    }

    /**
     * the internal group Sticky Delegate.
     */
    private clreplaced GroupStickyDelegate implements IStickyDelegate {

        private final ArrayList<IStickyDelegate> mDelegates = new ArrayList<>(5);

        public void addStickyDelegate(IStickyDelegate delegate) {
            mDelegates.add(delegate);
        }

        public void removeStickyDelegate(IStickyDelegate delegate) {
            mDelegates.remove(delegate);
        }

        public void clear() {
            mDelegates.clear();
        }

        @Override
        public boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState) {
            for (IStickyDelegate delegate : mDelegates) {
                if (delegate.shouldIntercept(snv, dy, topViewState)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView) {
            for (IStickyDelegate delegate : mDelegates) {
                delegate.afterOnMeasure(snv, top, indicator, contentView);
            }
        }

        @Override
        public void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event) {
            for (IStickyDelegate delegate : mDelegates) {
                delegate.dispatchTouchEventToChild(snv, dx, dy, event);
            }
        }

        @Override
        public void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event) {
            for (IStickyDelegate delegate : mDelegates) {
                delegate.onTouchEventUp(snv, event);
            }
        }
    }

    /**
     * on scroll  change listener.
     */
    public interface OnScrollChangeListener {

        /**
         * called when ths scroll distance change. may be negative .
         *
         * @param snl     the {@link StickyNavigationLayout_backup}
         * @param dx      the delta x between this touch and last touch
         * @param dy      the delta y between this touch and last touch
         * @param totalDx the delta x between this touch and first touch
         * @param totalDy the delta y between this touch and first touch
         */
        void onPreScrollDistanceChange(StickyNavigationLayout_backup snl, int dx, int dy, int totalDx, int totalDy);

        /**
         * called when ths scroll distance change. may be negative .
         *
         * @param snl     the {@link StickyNavigationLayout_backup}
         * @param dx      the delta x between this touch and last touch
         * @param dy      the delta y between this touch and last touch
         * @param totalDx the delta x between this touch and first touch
         * @param totalDy the delta y between this touch and first touch
         */
        void onAfterScrollDistanceChange(StickyNavigationLayout_backup snl, int dx, int dy, int totalDx, int totalDy);

        /**
         * called when the scroll state change
         *
         * @param snl            the {@link StickyNavigationLayout_backup}
         * @param state          the scroll state . see {@link StickyNavigationLayout_backup#SCROLL_STATE_START} and etc.
         * @param focusDirection {@link View#FOCUS_UP} means finger down or {@link View#FOCUS_DOWN} means finger up.
         */
        void onScrollStateChange(StickyNavigationLayout_backup snl, int state, int focusDirection);
    }

    /**
     * the sticky delegate
     */
    public interface IStickyDelegate {

        /**
         * called when you should intercept child's touch event.
         *
         * @param snv          the {@link StickyNavigationLayout_backup}
         * @param dy           the delta y distance
         * @param topViewState the view state of top view. {@link #VIEW_STATE_SHOW} or {@link #VIEW_STATE_HIDE}
         * @return true to intercept
         */
        boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState);

        /**
         *  called after the {@link StickyNavigationLayout_backup#onMeasure(int, int)}. this is useful used when we want to
         *  toggle two views visibility in {@link StickyNavigationLayout_backup}(or else may cause bug). see it in demo.
         * @param snv the {@link StickyNavigationLayout_backup}
         * @param top the top view
         * @param indicator the indicator view
         * @param contentView the content view
         */
        void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView);

        /**
         * dispatch the touch event
         * @param snv the {@link StickyNavigationLayout_backup}
         * @param dx the delta x
         * @param dy the delta y
         * @param event the event.
         */
        void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event);

        void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event);
    }

    /**
     * a simple implements of {@link IStickyDelegate}
     */
    public static clreplaced SimpleStickyDelegate implements IStickyDelegate {

        @Override
        public boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState) {
            return false;
        }

        @Override
        public void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView) {
        }

        @Override
        public void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event) {
        }

        @Override
        public void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event) {
        }
    }

    public static clreplaced RecyclerViewStickyDelegate implements IStickyDelegate {

        private final WeakReference<RecyclerView> mWeakRecyclerView;

        private boolean mParentReceived;

        public RecyclerViewStickyDelegate(RecyclerView mRv) {
            this.mWeakRecyclerView = new WeakReference<>(mRv);
        }

        @Override
        public boolean shouldIntercept(StickyNavigationLayout_backup snv, int dy, int topViewState) {
            final RecyclerView view = mWeakRecyclerView.get();
            if (view == null)
                return false;
            final int position = findFirstVisibleItemPosition(view);
            if (position == -1)
                return false;
            final View child = view.getChildAt(position);
            boolean isTopHidden = topViewState == StickyNavigationLayout_backup.VIEW_STATE_HIDE;
            if (!isTopHidden || (child != null && child.getTop() == 0 && dy > 0)) {
                // 滑动到顶部,并且要继续向下滑动时,拦截触摸
                return true;
            }
            return false;
        }

        @Override
        public void afterOnMeasure(StickyNavigationLayout_backup snv, View top, View indicator, View contentView) {
        }

        @Override
        public void dispatchTouchEventToChild(StickyNavigationLayout_backup snv, int dx, int dy, MotionEvent event) {
            final RecyclerView view = mWeakRecyclerView.get();
            if (view != null) {
                /* final int position = findFirstVisibleItemPosition(view);
                if (position == -1){
                    return;
                }
                final View child = view.getChildAt(position);
                if(child != null && child.getTop() == 0  && dy > 0){
                    if(snv.getTopViewState() == VIEW_STATE_SHOW){
                        ViewGroup vg = (ViewGroup) view.getParent();
                        vg.dispatchTouchEvent(event);
                        mParentReceived = true;
                        return;
                    }
                }*/
                view.scrollBy(0, -dy);
                if (DEBUG) {
                    Logger.i(TAG, "dispatchTouchEventToChild", "dy = " + dy + " ,can scroll: " + view.getLayoutManager().canScrollVertically());
                }
            }
        }

        @Override
        public void onTouchEventUp(StickyNavigationLayout_backup snv, MotionEvent event) {
        /*if( mParentReceived ) {
                mParentReceived = false;
                final RecyclerView view = mWeakRecyclerView.get();
                if (view != null) {
                    ViewGroup vg = (ViewGroup) view.getParent();
                    vg.dispatchTouchEvent(event);
                }
            }*/
        }

        public static int findFirstVisibleItemPosition(RecyclerView rv) {
            RecyclerView.LayoutManager lm = rv.getLayoutManager();
            int firstPos = RecyclerView.NO_POSITION;
            if (lm instanceof GridLayoutManager) {
                firstPos = ((GridLayoutManager) lm).findFirstVisibleItemPosition();
            } else if (lm instanceof LinearLayoutManager) {
                firstPos = ((LinearLayoutManager) lm).findFirstVisibleItemPosition();
            } else if (lm instanceof StaggeredGridLayoutManager) {
                int[] positions = ((StaggeredGridLayoutManager) lm).findFirstVisibleItemPositions(null);
                for (int pos : positions) {
                    if (pos < firstPos) {
                        firstPos = pos;
                    }
                }
            }
            return firstPos;
        }
    }

    /**
     * @return Whether it is possible for the child view of this layout to
     *         scroll up. Override this if the child view is a custom view.
     */
    /* public boolean canChildScrollUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            if (mTarget instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) mTarget;
                return absListView.getChildCount() > 0
                        && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                        .getTop() < absListView.getPaddingTop());
            } else {
                return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(mTarget, -1);
        }
    }*/
    // ========================  NestedScrollingParent begin ========================
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return isEnabled() && mEnableStickyTouch && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        // Reset the counter of how much leftover scroll needs to be consumed.
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        // Dispatch up to the nested parent
        startNestedScroll(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL);
        mTotalUnconsumed = 0;
        mNestedScrollInProgress = true;
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        /**
         * 滑动的最大高度:
         */
        final int scrollY = getScrollY();
        int by = 0;
        if (dy > 0) {
            // 手势向下,view向上
            if (scrollY != 0) {
                consumed[1] = Math.min(dy, scrollY);
                by = -consumed[1];
            }
        } else {
            // 手势向上,view向下
            if (scrollY < mTopViewHeight) {
                int maxH = mTopViewHeight - scrollY;
                consumed[1] = -Math.min(Math.abs(dy), maxH);
                by = -consumed[1];
            } else {
            // ignore
            }
        }
        scrollBy(0, by);
        // If we are in the middle of consuming, a scroll, then we want to move the spinner back up
        // before allowing the list to scroll
        /*  if (dy > 0 && mTotalUnconsumed > 0) {
            if ( dy > mTotalUnconsumed ) {
                consumed[1] = dy - (int) mTotalUnconsumed;
                mTotalUnconsumed = 0;
            } else {
                mTotalUnconsumed -= dy;
                consumed[1] = dy;
            }
            Logger.i(TAG, "onNestedPreScroll", "mTotalUnconsumed = " + mTotalUnconsumed);
           // moveSpinner(mTotalUnconsumed);
        }*/
        // If a client layout is using a custom start position for the circle
        // view, they mean to hide it again before scrolling the child view
        // If we get back to mTotalUnconsumed == 0 and there is more to go, hide
        // the circle so it isn't exposed if its blocking content is moved
        /*  if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0
                && Math.abs(dy - consumed[1]) > 0) {
            mCircleView.setVisibility(View.GONE);
        }*/
        // Now let our nested parent consume the leftovers
        final int[] parentConsumed = mParentScrollConsumed;
        if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
            consumed[0] += parentConsumed[0];
            consumed[1] += parentConsumed[1];
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        /* net
       inal int myConsumed = moveBy(dyUnconsumed);
        final int myUnconsumed = dyUnconsumed - myConsumed;
        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);*/
        // Dispatch up to the nested parent first
        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, mParentOffsetInWindow);
        // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
        // sometimes between two nested scrolling views, we need a way to be able to know when any
        // nested scrolling parent has stopped handling events. We do that by using the
        // 'offset in window 'functionality to see if we have been moved from the event.
        // This is a decent indication of whether we should take over the event stream or not.
        final int dy = dyUnconsumed + mParentOffsetInWindow[1];
        Logger.i(TAG, "onNestedScroll", "mTotalUnconsumed = " + (mTotalUnconsumed + Math.abs(dy)));
    /* if (dy < 0 && !canChildScrollUp()) {
            mTotalUnconsumed += Math.abs(dy);
          //  moveSpinner(mTotalUnconsumed);
        }*/
    }

    @Override
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
        mNestedScrollInProgress = false;
        // Finish the spinner for nested scrolling if we ever consumed any
        // unconsumed nested scroll
        if (mTotalUnconsumed > 0) {
            // finishSpinner(mTotalUnconsumed);
            mTotalUnconsumed = 0;
        }
        // Dispatch up our nested parent
        stopNestedScroll();
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    // ========================  NestedScrollingParent end ========================
    // ========================  NestedScrollingChild begin ========================
    public void setNestedScrollingEnabled(boolean enabled) {
        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    public boolean isNestedScrollingEnabled() {
        return mNestedScrollingChildHelper.isNestedScrollingEnabled();
    }

    public boolean startNestedScroll(int axes) {
        return mNestedScrollingChildHelper.startNestedScroll(axes);
    }

    public void stopNestedScroll() {
        mNestedScrollingChildHelper.stopNestedScroll();
    }

    public boolean hasNestedScrollingParent() {
        return mNestedScrollingChildHelper.hasNestedScrollingParent();
    }

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
    // ======================== end NestedScrollingChild =====================
}

19 Source : ScrollHelper.java
with Apache License 2.0
from LightSun

/**
 * <p>
 * this clreplaced is a simple implement of {@link IScrollHelper}. it can do most work of scroller.
 * such as {@link IScrollHelper#smoothScrollTo(int, int)}, {@link IScrollHelper#smoothScrollBy(int, int)} and etc.
 * </p>
 * Created by heaven7 on 2016/11/14.
 */
public clreplaced ScrollHelper implements IScrollHelper {

    /*protected*/
    static final boolean DEBUG = Util.sDEBUG;

    private static final long ANIMATED_SCROLL_GAP = 250;

    private CopyOnWriteArrayList<OnScrollChangeListener> mScrollListeners;

    private final OverScroller mScroller;

    protected final ScrollCallback mCallback;

    protected final String mTag;

    private final View mTarget;

    private final int mTouchSlop;

    private final float mMinFlingVelocity;

    private final float mMaxFlingVelocity;

    private long mLastScroll;

    private int mScrollState = SCROLL_STATE_IDLE;

    /**
     * create a ScrollHelper.
     *
     * @param target   the target view
     * @param scroller the over Scroller
     * @param callback the callback
     */
    public ScrollHelper(View target, OverScroller scroller, ScrollCallback callback) {
        this(target, 1, scroller, callback);
    }

    /**
     * create a ScrollHelper.
     *
     * @param target      the target view
     * @param sensitivity Multiplier for how sensitive the helper should be about detecting
     *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
     * @param scroller    the over Scroller
     * @param callback    the callback
     */
    public ScrollHelper(View target, float sensitivity, OverScroller scroller, ScrollCallback callback) {
        Util.check(target, "target view can't be null.");
        Util.check(scroller, null);
        Util.check(callback, "ScrollCallback can't be null");
        final ViewConfiguration vc = ViewConfiguration.get(target.getContext());
        this.mTag = target.getClreplaced().getSimpleName();
        this.mTarget = target;
        this.mCallback = callback;
        this.mScroller = scroller;
        this.mTouchSlop = (int) (vc.getScaledTouchSlop() * (1 / sensitivity));
        this.mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
        this.mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
    }

    public OverScroller getScroller() {
        return mScroller;
    }

    public int getTouchSlop() {
        return mTouchSlop;
    }

    public float getMinFlingVelocity() {
        return mMinFlingVelocity;
    }

    public float getMaxFlingVelocity() {
        return mMaxFlingVelocity;
    }

    public View getTarget() {
        return mTarget;
    }

    @Override
    public void dispatchOnScrolled(int dx, int dy) {
        // Preplaced the current scrollX/scrollY values; no actual change in these properties occurred
        // but some general-purpose code may choose to respond to changes this way.
        /* final int scrollX = mTarget.getScrollX();
        final int scrollY = mTarget.getScrollY();
        mTarget.onScrollChanged(scrollX, scrollY, scrollX, scrollY);*/
        // Invoke listeners last. Subclreplaceded view methods always handle the event first.
        // All internal state is consistent by the time listeners are invoked.
        if (mScrollListeners != null && mScrollListeners.size() > 0) {
            for (OnScrollChangeListener l : mScrollListeners) {
                if (l != null) {
                    l.onScrolled(mTarget, dx, dy);
                }
            }
        }
        // Preplaced the real deltas to onScrolled, the RecyclerView-specific method.
        onScrolled(dx, dy);
    }

    /**
     * Called when the scroll position of this view changes. Subclreplacedes should use
     * this method to respond to scrolling within the adapter's data set instead of an explicit
     * listener. this is called in {@link #dispatchOnScrolled(int, int)}.
     * <p/>
     * <p>This method will always be invoked before listeners. If a subclreplaced needs to perform
     * any additional upkeep or bookkeeping after scrolling but before listeners run,
     * this is a good place to do so.</p>
     *
     * @param dx horizontal distance scrolled in pixels
     * @param dy vertical distance scrolled in pixels
     */
    protected void onScrolled(int dx, int dy) {
        mCallback.onScrolled(dx, dy);
    }

    @Override
    public int getScrollState() {
        return mScrollState;
    }

    @Override
    public void setScrollState(int state) {
        if (state == mScrollState) {
            return;
        }
        if (DEBUG) {
            Log.d(mTag, "setting scroll state to " + state + " from " + mScrollState, new Exception());
        }
        mScrollState = state;
        if (state != SCROLL_STATE_SETTLING) {
            stopScrollerInternal();
        }
        dispatchOnScrollStateChanged(state);
    }

    protected void stopScrollerInternal() {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
    }

    /**
     * dispatch the scroll state change, this is called in {@link #setScrollState(int)}.
     *
     * @param state the target scroll state.
     */
    protected void dispatchOnScrollStateChanged(int state) {
        if (mScrollListeners != null && mScrollListeners.size() > 0) {
            for (OnScrollChangeListener l : mScrollListeners) {
                if (l != null) {
                    l.onScrollStateChanged(mTarget, state);
                }
            }
        }
    }

    @Override
    public void scrollBy(int dx, int dy) {
        scrollTo(mTarget.getScrollX() + dx, mTarget.getScrollY() + dy);
    }

    /**
     * {@inheritDoc}.  Note: this is  similar to {@link View#scrollTo(int, int)}, but limit the range of scroll,
     * which is indicate by {@link ScrollCallback#getMaximumXScrollDistance(View)} with {@link ScrollCallback#getMaximumYScrollDistance(View)}.
     *
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    @Override
    public void scrollTo(int x, int y) {
        mTarget.scrollTo(Math.min(x, mCallback.getMaximumXScrollDistance(mTarget)), Math.min(y, mCallback.getMaximumYScrollDistance(mTarget)));
    }

    @Override
    public void smoothScrollBy(int dx, int dy) {
        if (mTarget instanceof ViewGroup && ((ViewGroup) mTarget).getChildCount() == 0) {
            // Nothing to do.
            return;
        }
        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
        if (duration > ANIMATED_SCROLL_GAP) {
            // design from scrollView
            final int scrollX = mTarget.getScrollX();
            final int scrollY = mTarget.getScrollY();
            final int maxX = mCallback.getMaximumXScrollDistance(mTarget);
            final int maxY = mCallback.getMaximumYScrollDistance(mTarget);
            if ((scrollX + dx) > maxX) {
                dx -= scrollX + dx - maxX;
            }
            if ((scrollY + dy) > maxY) {
                dy -= scrollY + dy - maxY;
            }
            setScrollState(SCROLL_STATE_SETTLING);
            mScroller.startScroll(scrollX, scrollY, dx, dy);
            ViewCompat.postInvalidateOnAnimation(mTarget);
        } else {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            mTarget.scrollBy(dx, dy);
        }
        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
    }

    @Override
    public final void smoothScrollTo(int x, int y) {
        smoothScrollBy(x - mTarget.getScrollX(), y - mTarget.getScrollY());
    }

    @Override
    public void stopScroll() {
        setScrollState(SCROLL_STATE_IDLE);
        stopScrollerInternal();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            // true if not finish
            if (DEBUG) {
                Log.i(mTag, "computeScroll: scroll not finished: currX = " + mScroller.getCurrX() + " ,currY = " + mScroller.getCurrY());
            }
            mTarget.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            ViewCompat.postInvalidateOnAnimation(mTarget);
        }
    }

    @Override
    public boolean isScrollFinish() {
        return mScroller.isFinished();
    }

    @Override
    public boolean fling(float velocityX, float velocityY) {
        final boolean canScrollHorizontal = mCallback.canScrollHorizontally(mTarget);
        final boolean canScrollVertical = mCallback.canScrollVertically(mTarget);
        if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;
        }
        if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;
        }
        if (velocityX == 0 && velocityY == 0) {
            // If we don't have any velocity, return false
            return false;
        }
        return onFling(canScrollHorizontal, canScrollVertical, velocityX, velocityY);
    }

    @Override
    public void addOnScrollChangeListener(OnScrollChangeListener l) {
        if (mScrollListeners == null) {
            mScrollListeners = new CopyOnWriteArrayList<>();
        }
        mScrollListeners.add(l);
    }

    @Override
    public void removeOnScrollChangeListener(OnScrollChangeListener l) {
        if (mScrollListeners != null) {
            mScrollListeners.remove(l);
        }
    }

    @Override
    public boolean hasOnScrollChangeListener(OnScrollChangeListener l) {
        return mScrollListeners != null && mScrollListeners.contains(l);
    }

    /**
     * do fling , this method is called in {@link #fling(float, float)}
     *
     * @param canScrollHorizontal if can scroll in Horizontal
     * @param canScrollVertical   if can scroll in Vertical
     * @param velocityX           the velocity of X
     * @param velocityY           the velocity of y
     * @return true if the fling was started.
     */
    protected boolean onFling(boolean canScrollHorizontal, boolean canScrollVertical, float velocityX, float velocityY) {
        if (canScrollHorizontal || canScrollVertical) {
            setScrollState(SCROLL_STATE_SETTLING);
            velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
            velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
            mScroller.fling(mTarget.getScrollX(), mTarget.getScrollY(), (int) velocityX, (int) velocityY, 0, canScrollHorizontal ? mCallback.getMaximumXScrollDistance(mTarget) : 0, 0, canScrollVertical ? mCallback.getMaximumYScrollDistance(mTarget) : 0);
            // TODO why recyclerView use mScroller.fling(0, 0, (int)velocityX, (int)velocityY,Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            ViewCompat.postInvalidateOnAnimation(mTarget);
            return true;
        }
        return false;
    }

    /**
     * get the scroll state as string log.
     * @param state the scroll state.
     * @return the state as string
     */
    public static String getScrollStateString(int state) {
        switch(state) {
            case SCROLL_STATE_DRAGGING:
                return "SCROLL_STATE_DRAGGING";
            case SCROLL_STATE_SETTLING:
                return "SCROLL_STATE_SETTLING";
            case SCROLL_STATE_IDLE:
                return "SCROLL_STATE_IDLE";
            default:
                return "unknown state";
        }
    }

    /**
     * the scroll callback of {@link ScrollHelper}.
     */
    public static abstract clreplaced ScrollCallback {

        /**
         * if can scroll in Horizontal
         *
         * @param target the target view.
         * @return true if can scroll in Horizontal
         */
        public abstract boolean canScrollHorizontally(View target);

        /**
         * if can scroll in Vertical
         *
         * @param target the target view.
         * @return true if can scroll in Vertical
         */
        public abstract boolean canScrollVertically(View target);

        /**
         * get the maximum x scroll distance of the target view.
         *
         * @param target the target view.
         * @return the maximum x scroll distance
         */
        public int getMaximumXScrollDistance(View target) {
            return target.getWidth();
        }

        /**
         * get the maximum y scroll distance of the target view.
         *
         * @param target the target view.
         * @return the maximum y scroll distance
         */
        public int getMaximumYScrollDistance(View target) {
            return target.getHeight();
        }

        /**
         * called in {@link ScrollHelper#dispatchOnScrolled(int, int)}.
         *
         * @param dx the delta x
         * @param dy the delta y
         */
        public void onScrolled(int dx, int dy) {
        }
    }
}

19 Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun

/**
 * create a ScrollHelper.
 * @param target      the target view
 * @param sensitivity Multiplier for how sensitive the helper should be about detecting
 *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
 * @param scroller    the over Scroller
 * @param callback    the callback
 */
public static ScrollHelper create(View target, float sensitivity, OverScroller scroller, ScrollHelper.ScrollCallback callback) {
    return new ScrollHelper(target, sensitivity, scroller, callback);
}

19 Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun

/**
 * create a ScrollHelper.
 * @param target      the target view
 * @param scroller    the over Scroller
 * @param callback    the callback
 */
public static ScrollHelper create(View target, OverScroller scroller, ScrollHelper.ScrollCallback callback) {
    return new ScrollHelper(target, 1, scroller, callback);
}

19 Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun

/**
 * create the nested scroll helper, but the target view must implements {@link NestedScrollingChild}.
 * @param target  the target view
 * @param scroller  the scroller
 * @param callback the callback
 */
public static NestedScrollHelper create(View target, OverScroller scroller, NestedScrollHelper.NestedScrollCallback callback) {
    return new NestedScrollHelper(target, 1, scroller, (NestedScrollingChild) target, callback);
}

19 Source : NestedScrollFactory.java
with Apache License 2.0
from LightSun

/**
 * create the nested scroll helper.
 * @param target  the target view
 * @param sensitivity Multiplier for how sensitive the helper should be about detecting
 *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
 * @param scroller  the scroller
 * @param child the NestedScrollingChild.
 * @param callback the callback
 */
public static NestedScrollHelper create(View target, float sensitivity, OverScroller scroller, NestedScrollingChild child, NestedScrollHelper.NestedScrollCallback callback) {
    return new NestedScrollHelper(target, sensitivity, scroller, child, callback);
}

19 Source : SmartDragLayout.java
with Apache License 2.0
from li-xiaojun

/**
 * Description: 智能的拖拽布局,优先滚动整体,整体滚到头,则滚动内部能滚动的View
 * Create by dance, at 2018/12/23
 */
public clreplaced SmartDragLayout extends LinearLayout implements NestedScrollingParent {

    private View child;

    OverScroller scroller;

    VelocityTracker tracker;

    // 是否启用手势拖拽
    boolean enableDrag = true;

    boolean dismissOnTouchOutside = true;

    boolean isUserClose = false;

    // 是否开启三段拖拽
    boolean isThreeDrag = false;

    LayoutStatus status = LayoutStatus.Close;

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

    public SmartDragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SmartDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (enableDrag) {
            scroller = new OverScroller(context);
        }
    }

    int maxY;

    int minY;

    @Override
    public void onViewAdded(View c) {
        super.onViewAdded(c);
        child = c;
    }

    int lastHeight;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        maxY = child.getMeasuredHeight();
        minY = 0;
        int l = getMeasuredWidth() / 2 - child.getMeasuredWidth() / 2;
        child.layout(l, getMeasuredHeight(), l + child.getMeasuredWidth(), getMeasuredHeight() + maxY);
        if (status == LayoutStatus.Open) {
            if (isThreeDrag) {
                // 通过scroll上移
                scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
            } else {
                // 通过scroll上移
                scrollTo(getScrollX(), getScrollY() - (lastHeight - maxY));
            }
        }
        lastHeight = maxY;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        isUserClose = true;
        return super.dispatchTouchEvent(ev);
    }

    float touchX, touchY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (enableDrag && scroller.computeScrollOffset()) {
            touchX = 0;
            touchY = 0;
            return true;
        }
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (enableDrag) {
                    if (tracker != null)
                        tracker.clear();
                    tracker = VelocityTracker.obtain();
                }
                touchX = event.getX();
                touchY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (enableDrag && tracker != null) {
                    tracker.addMovement(event);
                    tracker.computeCurrentVelocity(1000);
                    int dy = (int) (event.getY() - touchY);
                    scrollTo(getScrollX(), getScrollY() - dy);
                    touchY = event.getY();
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // click in child rect
                Rect rect = new Rect();
                child.getGlobalVisibleRect(rect);
                if (!XPopupUtils.isInRect(event.getRawX(), event.getRawY(), rect) && dismissOnTouchOutside) {
                    float distance = (float) Math.sqrt(Math.pow(event.getX() - touchX, 2) + Math.pow(event.getY() - touchY, 2));
                    if (distance < ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
                        performClick();
                    }
                } else {
                }
                if (enableDrag && tracker != null) {
                    float yVelocity = tracker.getYVelocity();
                    if (yVelocity > 1500 && !isThreeDrag) {
                        close();
                    } else {
                        finishScroll();
                    }
                    // tracker.recycle();
                    tracker = null;
                }
                break;
        }
        return true;
    }

    private void finishScroll() {
        if (enableDrag) {
            int threshold = isScrollUp ? (maxY - minY) / 3 : (maxY - minY) * 2 / 3;
            int dy = (getScrollY() > threshold ? maxY : minY) - getScrollY();
            if (isThreeDrag) {
                int per = maxY / 3;
                if (getScrollY() > per * 2.5f) {
                    dy = maxY - getScrollY();
                } else if (getScrollY() <= per * 2.5f && getScrollY() > per * 1.5f) {
                    dy = per * 2 - getScrollY();
                } else if (getScrollY() > per) {
                    dy = per - getScrollY();
                } else {
                    dy = minY - getScrollY();
                }
            }
            scroller.startScroll(getScrollX(), getScrollY(), 0, dy, XPopup.getAnimationDuration());
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    boolean isScrollUp;

    @Override
    public void scrollTo(int x, int y) {
        if (y > maxY)
            y = maxY;
        if (y < minY)
            y = minY;
        float fraction = (y - minY) * 1f / (maxY - minY);
        isScrollUp = y > getScrollY();
        if (listener != null) {
            if (isUserClose && fraction == 0f && status != LayoutStatus.Close) {
                status = LayoutStatus.Close;
                listener.onClose();
            } else if (fraction == 1f && status != LayoutStatus.Open) {
                status = LayoutStatus.Open;
                listener.onOpen();
            }
            listener.onDrag(y, fraction, isScrollUp);
        }
        super.scrollTo(x, y);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isScrollUp = false;
        isUserClose = false;
        setTranslationY(0);
    }

    public void open() {
        post(new Runnable() {

            @Override
            public void run() {
                int dy = maxY - getScrollY();
                smoothScroll(enableDrag && isThreeDrag ? dy / 3 : dy, true);
                status = LayoutStatus.Opening;
            }
        });
    }

    public void close() {
        isUserClose = true;
        post(new Runnable() {

            @Override
            public void run() {
                scroller.abortAnimation();
                smoothScroll(minY - getScrollY(), false);
                status = LayoutStatus.Closing;
            }
        });
    }

    public void smoothScroll(final int dy, final boolean isOpen) {
        post(new Runnable() {

            @Override
            public void run() {
                scroller.startScroll(getScrollX(), getScrollY(), 0, dy, (int) (isOpen ? XPopup.getAnimationDuration() : XPopup.getAnimationDuration() * 0.8f));
                ViewCompat.postInvalidateOnAnimation(SmartDragLayout.this);
            }
        });
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL && enableDrag;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        // 必须要取消,否则会导致滑动初次延迟
        scroller.abortAnimation();
    }

    @Override
    public void onStopNestedScroll(View target) {
        finishScroll();
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        scrollTo(getScrollX(), getScrollY() + dyUnconsumed);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0) {
            // scroll up
            int newY = getScrollY() + dy;
            if (newY < maxY) {
                // dy不一定能消费完
                consumed[1] = dy;
            }
            scrollTo(getScrollX(), newY);
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        boolean isDragging = getScrollY() > minY && getScrollY() < maxY;
        if (isDragging && velocityY < -1500 && !isThreeDrag) {
            close();
        }
        return false;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    public void isThreeDrag(boolean isThreeDrag) {
        this.isThreeDrag = isThreeDrag;
    }

    public void enableDrag(boolean enableDrag) {
        this.enableDrag = enableDrag;
    }

    public void dismissOnTouchOutside(boolean dismissOnTouchOutside) {
        this.dismissOnTouchOutside = dismissOnTouchOutside;
    }

    private OnCloseListener listener;

    public void setOnCloseListener(OnCloseListener listener) {
        this.listener = listener;
    }

    public interface OnCloseListener {

        void onClose();

        void onDrag(int y, float percent, boolean isScrollUp);

        void onOpen();
    }
}

19 Source : HorizontalPageScrollView.java
with Apache License 2.0
from Launcher3-dev

/**
 * Created by yuchuan
 * DATE 16/4/5
 * TIME 09:37
 */
public clreplaced HorizontalPageScrollView<T extends MenuItem> extends LinearLayout implements DragSource, View.OnClickListener, View.OnLongClickListener {

    private Context mContext;

    private Launcher mLauncher;

    private OverScroller mScroller;

    public static boolean startTouch = true;

    private static final String TAG = "HorizontalScrollView";

    /*
     * 速度追踪器,主要是为了通过当前滑动速度判断当前滑动是否为fling
     */
    private VelocityTracker mVelocityTracker;

    /*
     * 记录当前屏幕下标,取值范围是:0 到 getMenuItemCount()-1
     */
    private int mCurScreen = 0;

    /*
     * Touch状态值 0:静止 1:滑动
     */
    private static final int TOUCH_STATE_REST = 0;

    private static final int TOUCH_STATE_SCROLLING = 1;

    /*
     * 记录当前touch事件状态--滑动(TOUCH_STATE_SCROLLING)、静止(TOUCH_STATE_REST 默认)
     */
    private int mTouchState = TOUCH_STATE_REST;

    private static final int SNAP_VELOCITY = 300;

    /*
     * 记录滑动时上次手指所处的位置
     */
    private float mLastMotionX;

    private float mLastMotionY;

    private int mPageCount;

    private OnScrollChangedListener mScrollChangedListener = null;

    private int mCellViewWidth;

    private int mCellViewHeight;

    private int mCountX;

    /**
     * 是否以页滑动
     */
    private boolean mPageScroll;

    // 是自由滑动还是分页滑动
    private boolean mFreeScroll;

    // 如果自由滑动失效,如果不是自由滑动,不满一屏的是否居中显示
    private boolean mCenterLayout;

    public HorizontalPageScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.mContext = context;
        mScroller = new OverScroller(mContext);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MenuDeviceProfile, defStyle, 0);
        mFreeScroll = ta.getBoolean(R.styleable.MenuDeviceProfile_free_scroll, false);
        mCenterLayout = ta.getBoolean(R.styleable.MenuDeviceProfile_center_layout, true);
        mCountX = ta.getInteger(R.styleable.MenuDeviceProfile_menu_numColumns, 4);
        ta.recycle();
        mLauncher = Launcher.getLauncher(context);
    }

    public HorizontalPageScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(widthSize, heightSize);
        int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight());
        int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
        mCellViewWidth = DeviceProfile.calculateCellWidth(childWidthSize, mCountX);
        mCellViewHeight = DeviceProfile.calculateCellHeight(childHeightSize, 1);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            measureChild(child);
        }
    }

    private void measureChild(View child) {
        final DeviceProfile profile = Launcher.getLauncher(mContext).getDeviceProfile();
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        lp.width = mCellViewWidth;
        lp.height = mCellViewHeight;
        int cHeight = getCellContentHeight(profile);
        int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
        int cellPaddingX = (int) (profile.edgeMarginPx / 2f);
        child.setPadding(cellPaddingX, cellPaddingY, cellPaddingX, 0);
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    int getCellContentHeight(DeviceProfile profile) {
        return Math.min(getMeasuredHeight(), profile.getCellHeight(CellLayout.HOTSEAT));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int top = getPaddingTop();
        mPageCount = getPageCount();
        if (!mFreeScroll) {
            if (mPageCount > 1) {
                int left = 0;
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    if (child.getVisibility() == View.GONE) {
                        continue;
                    }
                    if (i % mCountX == 0) {
                        left += getPaddingLeft();
                    }
                    LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    child.layout(left, top, left + lp.width, top + lp.height);
                    left += mCellViewWidth;
                    if (i % mCountX == (mCountX - 1)) {
                        left += getPaddingRight();
                    }
                }
            } else {
                int left = getPaddingLeft();
                if (mCenterLayout) {
                    left = getWidth() / 2 - mCellViewWidth * childCount / 2;
                }
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    if (child.getVisibility() == View.GONE) {
                        continue;
                    }
                    LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    child.layout(left, top, left + lp.width, top + lp.height);
                    left += lp.width;
                }
            }
        } else {
            int left = getPaddingLeft();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                child.layout(left, top, left + lp.width, top + lp.width);
                left += lp.width;
            }
        }
    }

    public void setAdapter(IMenuAdapter adapter) {
        removeAllViews();
        resetPage();
        adapter.setContainer(this);
        int N = adapter.getMenuItemCount();
        for (int i = 0; i < N; i++) {
            View view = adapter.getChildView(i, null, this);
            addView(view, i);
            startLayoutAnimation();
        }
    }

    private int getPageColumn() {
        int maxWidth = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            maxWidth = Math.max(mCellViewWidth, maxWidth);
        }
        return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / (maxWidth);
    }

    /**
     * 是否满屏
     */
    private boolean childIsFull() {
        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() < getAllChildLength();
    }

    private int getAllChildLength() {
        int column = mCountX;
        int childVisibleCount = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            childVisibleCount++;
        }
        return (int) Math.ceil(childVisibleCount * (1.0f * getWidth() / column));
    }

    // 计算最左边的child的左边位置,如果没有满一屏则所有居中
    private int computeLayoutLeftPoint() {
        boolean isFull = childIsFull();
        if (isFull) {
            return getPaddingLeft();
        } else {
            int width = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                width += child.getPaddingLeft() + child.getPaddingRight() + child.getMeasuredWidth();
            }
            if (getChildCount() > 0) {
                width += (getChildCount() - 1);
            }
            return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - width) / 2;
        }
    }

    // 获取页数
    public int getPageCount() {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            if (params.width == LayoutParams.MATCH_PARENT) {
                return childCount;
            }
        }
        return (int) Math.ceil(1.0 * getAllChildLength() / getWidth());
    }

    public int getAllPageCount() {
        return mPageCount;
    }

    public void setSupportPageScroll(boolean pageScroll) {
        this.mPageScroll = pageScroll;
    }

    public boolean getSupportPageScroll() {
        return mPageScroll;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        final int action = event.getAction();
        final float x = event.getX();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                mLastMotionX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                // 横向滑动距离
                int deltaX = (int) (mLastMotionX - x);
                mLastMotionX = x;
                scrollBy(deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000);
                int velocityX = (int) velocityTracker.getXVelocity();
                if (!mFreeScroll) {
                    if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {
                        // Fling enough to move left
                        int page = mCurScreen - 1;
                        page = page > 0 ? page : 0;
                        snapToScreen(page);
                    } else if (velocityX < -SNAP_VELOCITY && mCurScreen < mPageCount) {
                        // Fling enough to move right
                        int page = mCurScreen + 1;
                        page = page < mPageCount ? page : mPageCount - 1;
                        snapToScreen(page);
                    } else {
                        snapToDestination();
                    }
                } else {
                    View view = getChildAt(getChildCount() - 1);
                    if (view == null)
                        return true;
                    int end = view.getRight() - getWidth();
                    end = Math.max(0, end);
                    float minVelocity = ViewConfiguration.getMinimumFlingVelocity();
                    float maxVelocity = ViewConfiguration.getMaximumFlingVelocity();
                    final float step = 0.2f;
                    if (velocityX > SNAP_VELOCITY) {
                        // F;ling enough to move left
                        int delta;
                        if (getScrollX() > -getWidth() && getScrollX() <= 0) {
                            delta = -getScrollX();
                        } else {
                            float velocity = (velocityX - minVelocity);
                            float moveLength = velocity * step;
                            if (moveLength > getScrollX()) {
                                delta = -getScrollX();
                            } else {
                                delta = -(int) moveLength;
                            }
                        }
                        mScroller.startScroll(getScrollX(), 0, delta, 0, 300);
                        // Redraw the layout
                        invalidate();
                    } else if (velocityX < -SNAP_VELOCITY) {
                        int delta;
                        if (end - getScrollX() < getWidth()) {
                            delta = end - getScrollX();
                        } else {
                            float velocity = (minVelocity - velocityX);
                            float moveLength = velocity * step;
                            if (moveLength > (end - getScrollX())) {
                                delta = end - getScrollX();
                            } else {
                                delta = (int) moveLength;
                            }
                        }
                        mScroller.startScroll(getScrollX(), 0, delta, 0, 300);
                        // Redraw the layout
                        invalidate();
                    } else {
                        int length = 0;
                        if (getScrollX() > end) {
                            length = end - getScrollX();
                        } else if (getScrollX() < 0) {
                            length = -getScrollX();
                        }
                        mScroller.startScroll(getScrollX(), 0, length, 0, 300);
                        // Redraw the layout
                        invalidate();
                    }
                }
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                mTouchState = TOUCH_STATE_REST;
                break;
            case MotionEvent.ACTION_CANCEL:
                mTouchState = TOUCH_STATE_REST;
                break;
        }
        return true;
    }

    /**
     * 方法名称:snapToDestination 方法描述:根据当前位置滑动到相应界面
     */
    public void snapToDestination() {
        final int screenWidth = getWidth();
        final int destScreen = (int) Math.ceil(1.0f * getScrollX() / screenWidth);
        snapToScreen(destScreen < mPageCount ? destScreen : mPageCount - 1);
    }

    /**
     * 方法名称:snapToScreen 方法描述:滑动到到第whichScreen(从0开始)个界面,有过渡效果
     *
     * @param whichScreen
     */
    public void snapToScreen(int whichScreen) {
        // get the valid layout effect_page
        whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
        if (getScrollX() != (whichScreen * getWidth())) {
            final int delta = whichScreen * getWidth() - getScrollX();
            mScroller.startScroll(getScrollX(), 0, delta, 0, 300);
            notifyScrollChanged(whichScreen, mCurScreen);
            mCurScreen = whichScreen;
            // Redraw the layout
            invalidate();
        }
    }

    /**
     * 当滑动切换界面时执行相应操作
     *
     * @param newPage 目标页面
     * @param oldPage 要离开的页面
     */
    private void notifyScrollChanged(int newPage, int oldPage) {
        if (mScrollChangedListener != null) {
            mScrollChangedListener.onScrollChanged(newPage, oldPage);
            int allPageCount = getPageCount();
            mScrollChangedListener.onPagePositionChanged(newPage, allPageCount);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
            return true;
        }
        final float x = ev.getX();
        final float y = ev.getY();
        switch(action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionX = x;
                mLastMotionY = y;
                mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
                break;
            case MotionEvent.ACTION_MOVE:
                final int xDiff = (int) Math.abs(mLastMotionX - x);
                if (xDiff > ViewConfiguration.getTouchSlop()) {
                    if (Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1) {
                        mTouchState = TOUCH_STATE_SCROLLING;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mTouchState = TOUCH_STATE_REST;
                break;
        }
        return mTouchState != TOUCH_STATE_REST;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            int x = mScroller.getCurrX();
            scrollTo(x, 0);
            postInvalidate();
        }
    }

    /**
     * 是否滑动到了结尾
     *
     * @param distanceX 滑动距离
     *
     * @return
     */
    private boolean isEnd(float distanceX) {
        int fastLeft = getPaddingLeft();
        View view = getChildAt(getChildCount() - 1);
        int lastRight = view.getRight() - getWidth();
        int mScrollX = 0;
        return (distanceX > 0 && lastRight < mScrollX) || (distanceX < 0 && fastLeft > mScrollX);
    }

    public int getCurrentPage() {
        return mCurScreen;
    }

    public void setOnScrollChangeListener(OnScrollChangedListener onScrollChangeListener) {
        this.mScrollChangedListener = onScrollChangeListener;
    }

    @Override
    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target, LauncherLogProto.Target targetParent) {
    }

    @Override
    public void onClick(View v) {
    }

    @Override
    public boolean onLongClick(View v) {
        return false;
    }

    @Override
    public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
        // if (!success || (target != mLauncher.getWorkspace() &&
        // !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
        // // Exit spring loaded mode if we have not successfully dropped or have not handled the
        // // drop in Workspace
        // mLauncher.exitSpringLoadedDragModeDelayed(true,
        // Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
        // }
        // mLauncher.unlockScreenOrientation(false);
        if (!success) {
            d.deferDragViewCleanupPostAnimation = false;
        }
    }

    public interface OnScrollChangedListener {

        void onScrollChanged(int newPage, int oldPage);

        void onPagePositionChanged(int currentPage, int allPage);
    }

    public void resetPage() {
        scrollTo(0, 0);
        mCurScreen = 0;
    }

    /**
     * 设置等距布局
     *
     * @param equidistant 是否等距
     */
    public void setEquidistantLayout(boolean equidistant) {
        mFreeScroll = equidistant;
        requestLayout();
    }

    /**
     * 是否等距布局
     *
     * @return
     */
    public boolean hasEquidistantLayout() {
        return mFreeScroll;
    }
}

19 Source : NestedScrollWebView.java
with GNU General Public License v3.0
from KnIfER

public clreplaced NestedScrollWebView extends WebViewmy implements NestedScrollingChild3 {

    private static final String TAG = NestedScrollWebView.clreplaced.getSimpleName();

    private final int[] mScrollConsumed = new int[2];

    private final int[] mScrollOffset = new int[2];

    private int mLastMotionY;

    private VelocityTracker mVelocityTracker;

    private int mMinimumVelocity;

    private int mMaximumVelocity;

    private OverScroller mScroller;

    private int mLastScrollerY;

    private final NestedScrollingChildHelper mChildHelper;

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

    public NestedScrollWebView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NestedScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
        mScroller = new OverScroller(getContext());
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    public void fling(int velocityY) {
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
        // start
        mScroller.fling(// start
        getScrollX(), // start
        getScrollY(), // velocities
        0, // velocities
        velocityY, // x
        0, // x
        0, // y
        Integer.MIN_VALUE, // y
        Integer.MAX_VALUE, 0, // overscroll
        0);
        mLastScrollerY = getScrollY();
        ViewCompat.postInvalidateOnAnimation(this);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            final int x = mScroller.getCurrX();
            final int y = mScroller.getCurrY();
            Log.d(TAG, "computeScroll: y : " + y);
            int dy = y - mLastScrollerY;
            if (dy != 0) {
                int scrollY = getScrollY();
                int dyUnConsumed = 0;
                int consumedY = dy;
                if (scrollY == 0) {
                    dyUnConsumed = dy;
                    consumedY = 0;
                } else if (scrollY + dy < 0) {
                    dyUnConsumed = dy + scrollY;
                    consumedY = -scrollY;
                }
                dispatchNestedScroll(0, consumedY, 0, dyUnConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed);
            }
            // Finally update the scroll positions and post an invalidation
            mLastScrollerY = y;
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            // We can't scroll any more, so stop any indirect scrolling
            if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
                stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
            }
            // and reset the scroller y
            mLastScrollerY = 0;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        initVelocityTrackerIfNotExists();
        MotionEvent vtev = MotionEvent.obtain(event);
        final int actionMasked = event.getAction();
        switch(actionMasked) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = (int) event.getRawY();
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                mVelocityTracker.addMovement(vtev);
                mScroller.computeScrollOffset();
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity();
                if (Math.abs(initialVelocity) > mMinimumVelocity) {
                    fling(-initialVelocity);
                }
            case MotionEvent.ACTION_CANCEL:
                stopNestedScroll();
                recycleVelocityTracker();
                break;
            case MotionEvent.ACTION_MOVE:
                final int y = (int) event.getRawY();
                int deltaY = mLastMotionY - y;
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    Log.d(TAG, "onTouchEvent: deltaY : " + deltaY + " , mScrollConsumedY : " + mScrollConsumed[1] + " , mScrollOffset : " + mScrollOffset[1]);
                    vtev.offsetLocation(0, mScrollConsumed[1]);
                }
                mLastMotionY = y;
                int scrollY = getScrollY();
                int dyUnconsumed = 0;
                if (scrollY == 0) {
                    dyUnconsumed = deltaY;
                } else if (scrollY + deltaY < 0) {
                    dyUnconsumed = deltaY + scrollY;
                    vtev.offsetLocation(0, -dyUnconsumed);
                }
                mVelocityTracker.addMovement(vtev);
                dispatchNestedScroll(0, deltaY - dyUnconsumed, 0, dyUnconsumed, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed);
            default:
                break;
        }
        super.onTouchEvent(vtev);
        return true;
    }

    // NestedScrollingChild
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    @Override
    public boolean startNestedScroll(int axes, int type) {
        return mChildHelper.startNestedScroll(axes, type);
    }

    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    @Override
    public void stopNestedScroll(int type) {
        mChildHelper.stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return mChildHelper.hasNestedScrollingParent(type);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
        mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
    }
}

See More Examples