com.lwz.dragpanelayout.view.DragPaneLayout.java Source code

Java tutorial

Introduction

Here is the source code for com.lwz.dragpanelayout.view.DragPaneLayout.java

Source

/* Copyright 2015 Liu Wenzhu
 *
 * 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 com.lwz.dragpanelayout.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewGroupCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

/**
 * ?
 * @author Liu Wenzhu<lwz0316@gmail.com>
 * 2015-3-5 ?1:58:42
 */
public class DragPaneLayout extends FrameLayout {

    /**
     * ???
     */
    public static enum Mode {
        /**  */
        LEFT,
        /** ? */
        RIGHT,
        /** ??*/
        BOTH
    }

    public static interface OnPaneStateChangedListener {
        /**
         * 
         */
        public void onPaneClosed();

        /**
         * ?
         * @param mode {@link Mode}
         */
        public void onPaneOpened(Mode mode, float offset);

        /**
         * @param mode
         * @param offset
         *    <li> mode = {@link Mode#RIGHT} offset  [-1.0f, 0]
         *    <li> mode = {@link Mode#LEFT}  offset   [0, 1.0f]
         *    <li> mode = {@link Mode#BOTH}  offset   [-1.0f, 1]
         */
        public void onPaneDragged(Mode mode, float offset);

    }

    public class OnSimplePanelStateChangedListener implements OnPaneStateChangedListener {

        @Override
        public void onPaneClosed() {
        }

        @Override
        public void onPaneOpened(Mode mode, float offset) {
        }

        @Override
        public void onPaneDragged(Mode mode, float offset) {
        }

    }

    private static final float TOUCH_SLOP_SENSITIVITY = 0.5f;

    /**
     * ??Y???????
     *  {@link #requestDisallowInterceptTouchEvent(boolean)} ?
     * ?
     */
    private static final float DISALLOW_INTECEPT_TOUCH_EVNET_MAX_Y_OFFSET = 20;
    /**
     * Minimum velocity that will be detected as a fling
     */
    private static final int MIN_FLING_VELOCITY = 400; // dips per second

    private ViewDragHelper mDragHelper;
    private ViewDragCallback mViewDragCallback;
    private View mDragPane;
    private Mode mMode = Mode.RIGHT;
    private int mDragRange;
    private float mDragOffset;
    final float mDensity;
    /** ??? */
    private boolean mDragOpenable = true;
    Rect mDragViewVisibleBounds = new Rect();

    /**
      * Stores whether or not the pane was open the last time it was slideable.
      * If open/close operations are invoked this state is modified. Used by
      * instance state save/restore.
      */
    private boolean mPreservedOpenState;
    private int mBothModeSildeOffsetState;
    private boolean mFirstLayout = true;
    private int mLeftOffset;
    private int mTopOffset;

    private GestureDetectorCompat mGestureDetector;

    private OnPaneStateChangedListener mPaneStateChangedListener;
    private OnPaneStateChangedListener mPaneStateChangedProxy = new OnPaneStateChangedListener() {

        @Override
        public void onPaneClosed() {
            if (mPaneStateChangedListener != null) {
                mPaneStateChangedListener.onPaneClosed();
            }
        }

        @Override
        public void onPaneOpened(Mode mode, float offset) {
            if (mPaneStateChangedListener != null) {
                mPaneStateChangedListener.onPaneOpened(mode, offset);
            }
        }

        @Override
        public void onPaneDragged(Mode mode, float offset) {
            if (mPaneStateChangedListener != null) {
                mPaneStateChangedListener.onPaneDragged(mode, offset);
            }
        }
    };

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

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

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

        mDensity = context.getResources().getDisplayMetrics().density;

        mViewDragCallback = new ViewDragCallback();
        mDragHelper = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mViewDragCallback);
        mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * mDensity);

        ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);

        // So that we can catch the back button ?
        setFocusableInTouchMode(true);
        setClickable(true);
        ViewGroupCompat.setMotionEventSplittingEnabled(this, false);

        mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
    }

    private GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            boolean disallowIntercept = (Math
                    .abs(distanceY) < (DISALLOW_INTECEPT_TOUCH_EVNET_MAX_Y_OFFSET * mDensity)) || !isClosed();
            requestDisallowInterceptTouchEvent(disallowIntercept);
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

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

    };

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

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mFirstLayout) {
            if (mPreservedOpenState) {
                openPane();
            } else {
                closePane();
            }
        } else {
            // ????????ViewrequestLayout()bug 
            mDragPane.offsetLeftAndRight(mLeftOffset);
            mDragPane.offsetTopAndBottom(mTopOffset);
        }
        mFirstLayout = false;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w != oldw) {
            mFirstLayout = true;
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!mDragOpenable && isClosed()) {
            return super.onInterceptTouchEvent(ev);
        }
        int action = MotionEventCompat.getActionMasked(ev);
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            mDragHelper.cancel();
            return false;
        }
        return mDragHelper.shouldInterceptTouchEvent(ev) || shouldInterceptDragPaneTouchEvent(ev);
    }

    /**
     *  PaneView Touch 
     * ??? PaneView , Touch 
     * @param ev 
     * @return true  false ?
     */
    private boolean shouldInterceptDragPaneTouchEvent(MotionEvent ev) {
        // ????DragView ?
        if (!isClosed()) {
            mDragViewVisibleBounds.set(mDragPane.getLeft(), mDragPane.getTop(), mDragPane.getRight(),
                    mDragPane.getBottom());
            return mDragViewVisibleBounds.contains((int) ev.getX(), (int) ev.getY());
        }
        return false;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mDragOpenable && isClosed()) {
            return super.onTouchEvent(event);
        }
        //      requestDisallowInterceptTouchEvent(true);
        mGestureDetector.onTouchEvent(event);
        mDragHelper.processTouchEvent(event);
        return true;
    }

    /**
     *  PaneView
     * @param viewId pane view  id
     * @see DragPaneLayout#setDragPane(View)
     */
    public void setDragPane(int viewId) {
        setDragPane(findViewById(viewId));
    }

    /**
     *  PaneView
     * @param view pane view
     */
    public void setDragPane(View view) {
        mDragHelper.abort();
        if (mDragPane != view) {
            mDragPane = view;
        }
    }

    /**
     *  PaneView 
     * @param dragRange
     */
    public void setDragRange(int dragRange) {
        if (mDragRange != dragRange) {
            closePane();
            mDragRange = dragRange;
        }
    }

    /**
     * ?? PaneView 
     * @return dragRange
     */
    public int getDragRange() {
        return mDragRange;
    }

    @Override
    public void computeScroll() {
        if (mDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            // ????? onLayout() ?????
            mLeftOffset = mDragPane.getLeft();
            mTopOffset = mDragPane.getTop();
        }
    }

    /**
      * Smoothly animate mDraggingPane to the target X position within its range.
      *
      * @param slideOffset position to animate to
      * @param velocity initial velocity in case of fling, or 0.
      */
    boolean smoothSlideTo(float slideOffset, int velocity) {
        int startBound = 0;
        int x = (int) (startBound + slideOffset * mDragRange);
        if (mDragHelper.smoothSlideViewTo(mDragPane, x, mDragPane.getTop())) {
            ViewCompat.postInvalidateOnAnimation(this);
            return true;
        }
        return false;
    }

    private void onPaneDragged(int newLeft) {
        mDragOffset = (float) (newLeft) / mDragRange;
        mPaneStateChangedProxy.onPaneDragged(mMode, mDragOffset);
    }

    /**
     *  PaneView
     */
    public void closePane() {
        if (mDragPane == null || isClosed()) {
            return;
        }
        smoothSlideTo(0f, 0);
    }

    /**
     *  PaneView
     */
    public void openPane() {
        if (mDragPane == null) {
            return;
        }
        if (Mode.LEFT == mMode) {
            smoothSlideTo(1.0f, 0);
        } else if (mFirstLayout && Mode.BOTH == mMode) {
            smoothSlideTo(mBothModeSildeOffsetState, 0);
        } else { // Mode.RIGHT || Mode.BOTH
            smoothSlideTo(-1.0f, 0);
        }
    }

    /**
     * PaneView ???
     * @return true ???, false 
     */
    public boolean isOpened() {
        return Float.valueOf(Math.abs(mDragOffset)).intValue() == 1;
    }

    /**
     * PaneView ??
     * @return true ?, false 
     */
    public boolean isClosed() {
        return Math.abs(mDragOffset) < 0.0009f;
    }

    /**
     *  PaneView ?? {@link Mode}
     * @param mode {@link Mode}
     */
    public void setMode(Mode mode) {
        if (mMode != mode) {
            if (mode != Mode.BOTH) {
                closePane();
            }
            mMode = mode;
        }
    }

    /**
     *  PaneView ??
     * @param openable true ?; false ??? {@link #openPane()}  
     */
    public void setDragOpenable(boolean openable) {
        mDragOpenable = openable;
    }

    /**
     * ?? PaneView
     * @return true ?, false ??
     * @see #setDragOpenable(boolean)
     */
    public boolean isDragOpenable() {
        return mDragOpenable;
    }

    /**
     *  PaneView ???
     * @param l
     */
    public void setOnPaneStateChangedListener(OnPaneStateChangedListener l) {
        mPaneStateChangedListener = l;
    }

    class ViewDragCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(View view, int arg1) {
            return mDragPane == view;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return child.getTop();
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            int newLeft = left;
            int startBound = 0;
            int endBound = startBound + getViewHorizontalDragRange(child);
            if (Mode.RIGHT == mMode) {
                newLeft = Math.max(Math.min(newLeft, -startBound), -endBound);
            } else if (Mode.LEFT == mMode) {
                newLeft = Math.min(Math.max(newLeft, startBound), endBound);
            } else {
                newLeft = Math.min(Math.max(newLeft, -endBound), endBound);
            }
            return newLeft;
        }

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

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

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            int finalLeft = 0;
            if (Mode.RIGHT == mMode) {
                // ?
                if (xvel < 0 || (xvel == 0 && mDragOffset < -0.5f)) {
                    finalLeft -= mDragRange;
                }
            } else if (Mode.LEFT == mMode) {
                // ??
                if (xvel > 0 || (xvel == 0 && mDragOffset > 0.5f)) {
                    finalLeft += mDragRange;
                }
            } else {
                // Mode.BOTH == mMode
                // ?
                final int viewLeft = releasedChild.getLeft();
                if (xvel < 0 || (xvel == 0 && mDragOffset < -0.5f)) {
                    if (viewLeft < 0) {
                        finalLeft -= mDragRange;
                    }
                } else if (xvel > 0 || (xvel == 0 && mDragOffset > 0.5f)) {
                    if (viewLeft > 0) {
                        finalLeft += mDragRange;
                    }
                }
            }
            mDragHelper.settleCapturedViewAt(finalLeft, releasedChild.getTop());
            // don't forget this
            // ???
            invalidate();
        }

        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
            if (ViewDragHelper.STATE_IDLE == state) {
                if (mDragOffset == 0) {
                    mPaneStateChangedProxy.onPaneClosed();
                    mPreservedOpenState = false;
                } else {
                    mPaneStateChangedProxy.onPaneOpened(mMode, mDragOffset);
                    mPreservedOpenState = true;
                }
            }
        }

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            mDragHelper.captureChildView(mDragPane, pointerId);
        }

    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        ss.isOpen = isOpened();
        ss.isDragOpenable = isDragOpenable();
        ss.mode = mMode;
        ss.bothModeDragOffsetState = (int) mDragOffset;
        ss.dragRange = mDragRange;
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        setMode(ss.mode);
        setDragOpenable(ss.isDragOpenable);
        mPreservedOpenState = ss.isOpen;
        mBothModeSildeOffsetState = ss.bothModeDragOffsetState;
        mDragRange = ss.dragRange;
        if (ss.isOpen) {
            openPane();
        } else {
            closePane();
        }
    }

    static class SavedState extends BaseSavedState {
        boolean isOpen;
        boolean isDragOpenable;
        Mode mode;
        int bothModeDragOffsetState; // for Mode.BOTH only. left open OR right open OR close
        int dragRange;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            isOpen = in.readInt() != 0;
            isDragOpenable = in.readInt() != 0;
            mode = Mode.valueOf(in.readString());
            bothModeDragOffsetState = in.readInt();
            dragRange = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(isOpen ? 1 : 0);
            out.writeInt(isDragOpenable ? 1 : 0);
            out.writeString(mode.toString());
            out.writeInt(bothModeDragOffsetState);
            out.writeInt(dragRange);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}