dg.shenm233.drag2expandview.Drag2ExpandView.java Source code

Java tutorial

Introduction

Here is the source code for dg.shenm233.drag2expandview.Drag2ExpandView.java

Source

/*
 * Copyright 2016 Shen Zhang
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dg.shenm233.drag2expandview;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
 * Thanks for Flavien Laurent 's tutorial about ViewDragHelper
 * flavienlaurent.com/blog/2013/08/28/each-navigation-drawer-hides-a-viewdraghelper/
 * and also reference SlidingUpPanel (https://github.com/umano/AndroidSlidingUpPanel)
 */

public class Drag2ExpandView extends ViewGroup {
    //    public final static int STATE_IDLE = 0;
    public final static int STATE_DRAGGING = 1;
    public final static int STATE_EXPAND = 2;
    public final static int STATE_COLLAPSE = 3;
    public final static int STATE_ANCHORED = 4;

    public final static int DEFAULT_STATE = STATE_COLLAPSE;

    private static int[] OTHER_ATTRS = new int[] { android.R.attr.gravity };

    private final ViewDragHelper mViewDragHelper;
    private ViewDragCallback mViewDragCallback;

    private int mHeaderHeight = 0;

    /**
     * the main view,is sliding
     */
    private View mMainView;

    private int mScrollableViewResId;
    /**
     *
     */
    private View mScrollableView;

    private int mDragViewResId;
    /**
     * the view that to be dragged
     */
    private View mDragView;

    /**
     * anchor offset is that sliding to special position
     */
    private float mAnchorOffset = 1.0f;

    private int mDragRange = 0; // always > 0
    private float mDragOffset;

    private boolean mIsUpSliding = false;

    private float mPreviousDownX = 0;
    private float mPreviousDownY = 0;

    private int mViewState = DEFAULT_STATE;

    private boolean isFirstLayout = true;

    private ScrollableViewHelper mScrollableViewHelper;
    private boolean mLetScrollableViewHandle;

    private OnDragListener mOnDragListener;

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

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

    public Drag2ExpandView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public Drag2ExpandView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        if (attrs != null) {
            TypedArray o = context.obtainStyledAttributes(attrs, OTHER_ATTRS);
            if (o != null) {
                int gravity = o.getInt(0, Gravity.NO_GRAVITY);
                setGravity(gravity);
                o.recycle();
            }

            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Drag2ExpandView);
            if (a != null) {
                mHeaderHeight = a.getDimensionPixelSize(R.styleable.Drag2ExpandView_sHeaderHeight, -1);
                mScrollableViewResId = a.getResourceId(R.styleable.Drag2ExpandView_sScrollableView, -1);
                mDragViewResId = a.getResourceId(R.styleable.Drag2ExpandView_sViewToDrag, -1);
                mAnchorOffset = a.getFloat(R.styleable.Drag2ExpandView_sAnchorOffset, 1.0f);
                a.recycle();
            }
        }

        mViewDragHelper = ViewDragHelper.create(this, 1.0f, mViewDragCallback = new ViewDragCallback());
        mScrollableViewHelper = new ScrollableViewHelper();

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (mScrollableViewResId != -1) {
            setScrollableView(findViewById(mScrollableViewResId));
        }
        if (mDragViewResId != -1) {
            setViewToDrag(findViewById(mDragViewResId));
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        //        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        final int childCount = getChildCount();
        if (childCount != 1) {
            throw new IllegalStateException("only support one child view");
        }

        if (widthMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("width must be exactly value or MATCH_PARENT");
        }

        final View mainView = getChildAt(0);

        int paddingWidth = getPaddingLeft() + getPaddingRight();
        int paddingHeight = getPaddingTop() + getPaddingBottom();

        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize - paddingWidth, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize - paddingHeight, MeasureSpec.EXACTLY);

        mainView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        mMainView = mainView;

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int paddingLeft = getPaddingLeft();

        int top;

        if (isFirstLayout) {
            mDragRange = getHeight() - mHeaderHeight;
            mDragOffset = 0.0f;
            isFirstLayout = false;
        }

        top = computePanelTopPosition(mDragOffset);

        mMainView.layout(paddingLeft, top, paddingLeft + mMainView.getMeasuredWidth(),
                top + mMainView.getMeasuredHeight());
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        final float x = ev.getX();
        final float y = ev.getY();

        int maskAction = action & MotionEvent.ACTION_MASK;
        if (maskAction == MotionEvent.ACTION_DOWN) {
            mLetScrollableViewHandle = false;
            mPreviousDownX = x;
            mPreviousDownY = y;
        } else if (maskAction == MotionEvent.ACTION_MOVE) {
            if (!isViewUnder(mScrollableView, (int) x, (int) y)) {
                return super.dispatchTouchEvent(ev);
            }

            if (mScrollableViewHelper.getScrollableViewScrollPosition(mScrollableView, mIsUpSliding) > 0) {
                mLetScrollableViewHandle = true;
                return super.dispatchTouchEvent(ev);
            }
        }

        // default
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mLetScrollableViewHandle) {
            mViewDragHelper.cancel();
            return false;
        }

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!mLetScrollableViewHandle) {
            mViewDragHelper.processTouchEvent(ev);
            if (mViewDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING) {
                return true;
            }
        }

        final float x = ev.getX();
        final float y = ev.getY();

        boolean isDragViewUnder = isViewUnder(mDragView, (int) x, (int) y);

        final int action = ev.getAction();
        int maskAction = action & MotionEvent.ACTION_MASK;
        if (maskAction == MotionEvent.ACTION_DOWN) {
            mPreviousDownX = x;
            mPreviousDownY = y;
        } else if (maskAction == MotionEvent.ACTION_UP) {
            float dx = mPreviousDownX - x;
            float dy = mPreviousDownY - y;
            int slop = mViewDragHelper.getTouchSlop();
            if (dx * dx + dy * dy < slop * slop && isDragViewUnder) {
                if (mViewState == STATE_COLLAPSE) {
                    smoothSlideViewTo(mAnchorOffset); //expand
                } else {
                    smoothSlideViewTo(0.0f); // collapse
                }
            }
        }

        return isDragViewUnder;
    }

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // if size changed,just re-layout
        if (h != oldh) {
            isFirstLayout = true;
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        isFirstLayout = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isFirstLayout = true;
    }

    public void setGravity(int gravity) {
        if (gravity == Gravity.BOTTOM) {
            mIsUpSliding = true;
        } else if (gravity == Gravity.TOP) {
            mIsUpSliding = false;
        } else {
            throw new IllegalStateException("gravity must be BOTTOM or TOP");
        }
        mViewState = DEFAULT_STATE;
        mDragOffset = 0; // collapse
        if (!isFirstLayout) {
            requestLayout();
        }
    }

    public void setHeaderHeight(int height) {
        mHeaderHeight = height;
        mDragRange = getHeight() - mHeaderHeight;
    }

    public int getHeaderHeight() {
        return mHeaderHeight;
    }

    public void expandView() {
        smoothSlideViewTo(1.0f);
    }

    public void collapseView() {
        smoothSlideViewTo(0.0f);
    }

    public int getViewState() {
        return mViewState;
    }

    public void setScrollableView(View scrollableView) {
        mScrollableView = scrollableView;
    }

    public void setViewToDrag(View dragView) {
        mDragView = dragView;
    }

    public void setOnDragListener(OnDragListener l) {
        mOnDragListener = l;
    }

    private boolean smoothSlideViewTo(float offset) {
        int finalY = computePanelTopPosition(offset);
        final View slidingView = mMainView;

        if (mViewDragHelper.smoothSlideViewTo(slidingView, slidingView.getLeft(), finalY)) {
            ViewCompat.postInvalidateOnAnimation(this);
            return true;
        }
        return false;
    }

    private void onDragView(int top) {
        mDragOffset = computeSlideOffset(top);
        requestLayout();
        dispatchOnDrag();
    }

    private void dispatchOnDrag() {
        if (mOnDragListener != null) {
            mOnDragListener.onDrag(this, mDragOffset);
        }
    }

    private void dispatchOnDragStateChanged(int oldState, int newState) {
        if (mOnDragListener != null) {
            mOnDragListener.onStateChanged(this, oldState, newState);
        }
    }

    private boolean isViewUnder(View view, int x, int y) {
        if (view == null)
            return false;
        int[] viewLocation = new int[2];
        view.getLocationOnScreen(viewLocation);
        int[] parentLocation = new int[2];
        this.getLocationOnScreen(parentLocation);
        int screenX = parentLocation[0] + x;
        int screenY = parentLocation[1] + y;
        return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth()
                && screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
    }

    /*
     * Computes the top position of the panel based on the slide offset.
     */
    private int computePanelTopPosition(float slideOffset) {
        int slidingViewHeight = mMainView != null ? mMainView.getMeasuredHeight() : 0;
        int slidePixelOffset = (int) (slideOffset * mDragRange);
        // Compute the top of the panel if its collapsed
        return mIsUpSliding ? getMeasuredHeight() - getPaddingBottom() - mHeaderHeight - slidePixelOffset
                : getPaddingTop() - slidingViewHeight + mHeaderHeight + slidePixelOffset;
    }

    /*
     * Computes the slide offset based on the top position of the panel
     */
    private float computeSlideOffset(int topPosition) {
        // Compute the panel top position if the panel is collapsed (offset 0)
        final int topBoundCollapsed = computePanelTopPosition(0);

        // Determine the new slide offset based on the collapsed top position and the new required
        // top position
        return mIsUpSliding ? (float) (topBoundCollapsed - topPosition) / mDragRange
                : (float) (topPosition - topBoundCollapsed) / mDragRange;
    }

    private class ViewDragCallback extends ViewDragHelper.Callback {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == mMainView;
        }

        @Override
        public void onViewDragStateChanged(int state) {
            int oldState = mViewState;

            if (state == ViewDragHelper.STATE_DRAGGING) {
                mViewState = STATE_DRAGGING;
                dispatchOnDragStateChanged(oldState, STATE_DRAGGING);
            } else if (state == ViewDragHelper.STATE_IDLE) {
                if (mDragOffset == 0.0f) {
                    mViewState = STATE_COLLAPSE;
                    dispatchOnDragStateChanged(oldState, STATE_COLLAPSE);
                } else if (mDragOffset == mAnchorOffset && mDragOffset != 1.0f) {
                    mViewState = STATE_ANCHORED;
                    dispatchOnDragStateChanged(oldState, STATE_ANCHORED);
                } else {
                    mViewState = STATE_EXPAND;
                    dispatchOnDragStateChanged(oldState, STATE_EXPAND);
                }
            }
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            onDragView(top);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            // up:yvel < 0,down:yvel > 0
            float direction = mIsUpSliding ? -yvel : yvel;
            float offset = mDragOffset;
            if (direction > 0 && mDragOffset < mAnchorOffset) {
                offset = mAnchorOffset;
            } else if (direction > 0 && mDragOffset >= mAnchorOffset) {
                offset = 1.0f; // expand
            } else if (direction < 0 && mDragOffset > mAnchorOffset) {
                offset = mAnchorOffset;
            } else if (direction < 0 && mDragOffset <= mAnchorOffset) {
                offset = 0.0f; // collapse
            }
            smoothSlideViewTo(offset);
        }

        @Override
        public int getViewVerticalDragRange(View child) {
            return mDragRange;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            final int collapsedTop = computePanelTopPosition(0.0f);
            final int expandedTop = computePanelTopPosition(1.0f);
            // Restrict top
            if (mIsUpSliding) {
                return Math.min(Math.max(top, expandedTop), collapsedTop);
            } else {
                return Math.min(Math.max(top, collapsedTop), expandedTop);
            }
        }
    }

    public interface OnDragListener {
        void onDrag(Drag2ExpandView v, float dragOffset);

        void onStateChanged(Drag2ExpandView v, int oldState, int newState);
    }
}