CustomDrawerLayout from four screen sides issue with Fling gesture and detection
Asked Answered
C

1

24

I am trying to create and improve existing SlidingDrawers projects that can work for all four sides of the screen {LEFT, RIGHT, TOP, BOTTOM}. There are a few libraries, however, they all have limitations, complications and bugs. One of the more common ones is umano's AndroidSlidingUpPanel, however, I do not like this library because you have to only include two child layouts, and also need to be mindful of a specific arrangement for main content to drawer. Other libraries are similar, or more complicated, or have bugs.

I am close to completing my version of SlidingDrawers, I am focusing on BOTTOM gravity. I need some help with the fling gesture. Clicking on the drawer will open and close it. You can also slide the drawer with your finger. But if you fling the drawer, the entire view will shift higher than it should or lower than it should.

How can I resolve this? As far as I can tell, my math is correct. The translation value I am passing to my animator should be right. Below is the work I have completed. Please checkout this project https://github.com/drxeno02/CustomDrawerLayout.git. Thank you in advance.

For those of you who want to see a snippet of how the problematic code looks, here is how I am currently doing my gestures.

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mActivePointerId = MotionEventCompat.getPointerId(event, 0);
            switch (mStickTo) {
                case GRAVITY_BOTTOM:
                case GRAVITY_TOP:
                    mInitialCoordinate = event.getY();
                    break;
                case GRAVITY_LEFT:
                case GRAVITY_RIGHT:
                    mInitialCoordinate = event.getX();
                    break;
            }
            break;

        case MotionEvent.ACTION_MOVE:

            float coordinate = 0;
            switch (mStickTo) {
                case GRAVITY_BOTTOM:
                case GRAVITY_TOP:
                    coordinate = event.getY();

                    break;
                case GRAVITY_LEFT:
                case GRAVITY_RIGHT:
                    coordinate = event.getX();
                    break;
            }

            final int diff = (int) Math.abs(coordinate - mInitialCoordinate);

            // confirm that difference is enough to indicate drag action
            if (diff > mTouchSlop) {
                // start capturing events
                Logger.d(TAG, "drag is being captured");
                return true;
            }
            break;

        case MotionEvent.ACTION_UP:
            if (!FrameworkUtils.checkIfNull(mVelocityTracker)) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            break;
    }
    // add velocity movements
    if (FrameworkUtils.checkIfNull(mVelocityTracker)) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);

    return super.onInterceptTouchEvent(event);
}

@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
        return false;
    }

    // add velocity movements
    if (FrameworkUtils.checkIfNull(mVelocityTracker)) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);

    final View parent = (View) getParent();
    final int coordinate;
    final int distance = getDistance();
    final int tapCoordinate;

    switch (mStickTo) {
        case GRAVITY_BOTTOM:
            coordinate = (int) event.getRawY();
            tapCoordinate = (int) event.getRawY();
            break;
        case GRAVITY_LEFT:
            coordinate = parent.getWidth() - (int) event.getRawX();
            tapCoordinate = (int) event.getRawX();
            break;
        case GRAVITY_RIGHT:
            coordinate = (int) event.getRawX();
            tapCoordinate = (int) event.getRawX();
            break;
        case GRAVITY_TOP:
            coordinate = getRawDisplayHeight(getContext()) - (int) event.getRawY();
            tapCoordinate = (int) event.getRawY();
            break;
        // if view position is not initialized throw an error
        default:
            throw new IllegalStateException("Failed to initialize coordinates");
    }

    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:

            /*
             * Return the pointer identifier associated with a particular pointer data index is
             * this event. The identifier tells you the actual pointer number associated with
             * the data, accounting for individual pointers going up and down since the start
             * of the current gesture.
             */
            mActivePointerId = event.getPointerId(0);

            switch (mStickTo) {
                case GRAVITY_BOTTOM:
                    mDelta = coordinate - ((RelativeLayout.LayoutParams) getLayoutParams()).topMargin;
                    break;
                case GRAVITY_LEFT:
                    mDelta = coordinate - ((RelativeLayout.LayoutParams) getLayoutParams()).rightMargin;
                    break;
                case GRAVITY_RIGHT:
                    mDelta = coordinate - ((RelativeLayout.LayoutParams) getLayoutParams()).leftMargin;
                    break;
                case GRAVITY_TOP:
                    mDelta = coordinate - ((RelativeLayout.LayoutParams) getLayoutParams()).bottomMargin;
                    break;
            }

            mLastCoordinate = coordinate;
            mPressStartTime = System.currentTimeMillis();
            break;

        case MotionEvent.ACTION_MOVE:

            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
            final int farMargin = coordinate - mDelta;
            final int closeMargin = distance - farMargin;

            switch (mStickTo) {
                case GRAVITY_BOTTOM:
                    if (farMargin > distance && closeMargin > mOffsetHeight - getHeight()) {
                        layoutParams.bottomMargin = closeMargin;
                        layoutParams.topMargin = farMargin;
                    }
                    break;
                case GRAVITY_LEFT:
                    if (farMargin > distance && closeMargin > mOffsetHeight - getWidth()) {
                        layoutParams.leftMargin = closeMargin;
                        layoutParams.rightMargin = farMargin;
                    }
                    break;
                case GRAVITY_RIGHT:
                    if (farMargin > distance && closeMargin > mOffsetHeight - getWidth()) {
                        layoutParams.rightMargin = closeMargin;
                        layoutParams.leftMargin = farMargin;
                    }
                    break;
                case GRAVITY_TOP:
                    if (farMargin > distance && closeMargin > mOffsetHeight - getHeight()) {
                        layoutParams.topMargin = closeMargin;
                        layoutParams.bottomMargin = farMargin;
                    }
                    break;
            }
            setLayoutParams(layoutParams);
            break;

        case MotionEvent.ACTION_UP:

            final int diff = coordinate - mLastCoordinate;
            final long pressDuration = System.currentTimeMillis() - mPressStartTime;

            switch (mStickTo) {
                case GRAVITY_BOTTOM:

                    // determine if fling
                    int relativeVelocity;
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    final int initialVelocityY = (int) VelocityTrackerCompat.getYVelocity(
                            velocityTracker, mActivePointerId);
                    relativeVelocity = initialVelocityY * -1;
                    // take absolute value to have positive values
                    final int absoluteVelocity = Math.abs(relativeVelocity);

                    if (Math.abs(diff) > mFlingDistance && absoluteVelocity > mMinimumVelocity) {
                        if (tapCoordinate > parent.getHeight() - mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_CLOSED) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, parent.getHeight() - mOffsetHeight, true);
                        } else if (Math.abs(getRawDisplayHeight(getContext()) -
                                tapCoordinate - getHeight()) < mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_OPEN) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_CLOSED, parent.getHeight() - mOffsetHeight, true);
                        }
                    } else {
                        if (isClicked(getContext(), diff, pressDuration)) {
                            if (tapCoordinate > parent.getHeight() - mOffsetHeight &&
                                    mLockMode == LockMode.LOCK_MODE_CLOSED) {
                                notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, parent.getHeight() - mOffsetHeight, true);
                            } else if (Math.abs(getRawDisplayHeight(getContext()) -
                                    tapCoordinate - getHeight()) < mOffsetHeight &&
                                    mLockMode == LockMode.LOCK_MODE_OPEN) {
                                notifyActionAndAnimateForState(LockMode.LOCK_MODE_CLOSED, parent.getHeight() - mOffsetHeight, true);
                            }
                        } else {
                            smoothScrollToAndNotify(diff);
                        }
                    }
                    break;
                case GRAVITY_TOP:
                    if (isClicked(getContext(), diff, pressDuration)) {
                        final int y = getLocationInYAxis(this);
                        if (tapCoordinate - Math.abs(y) <= mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_CLOSED) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, parent.getHeight() - mOffsetHeight, true);
                        } else if (getHeight() - (tapCoordinate - Math.abs(y)) < mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_OPEN) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_CLOSED, parent.getHeight() - mOffsetHeight, true);
                        }
                    } else {
                        smoothScrollToAndNotify(diff);
                    }
                    break;
                case GRAVITY_LEFT:
                    if (isClicked(getContext(), diff, pressDuration)) {
                        if (tapCoordinate <= mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_CLOSED) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, getWidth() - mOffsetHeight, true);
                        } else if (tapCoordinate > getWidth() - mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_OPEN) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_CLOSED, getWidth() - mOffsetHeight, true);
                        }
                    } else {
                        smoothScrollToAndNotify(diff);
                    }
                    break;
                case GRAVITY_RIGHT:
                    if (isClicked(getContext(), diff, pressDuration)) {
                        if (parent.getWidth() - tapCoordinate <= mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_CLOSED) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, getWidth() - mOffsetHeight, true);
                        } else if (parent.getWidth() - tapCoordinate > getWidth() - mOffsetHeight &&
                                mLockMode == LockMode.LOCK_MODE_OPEN) {
                            notifyActionAndAnimateForState(LockMode.LOCK_MODE_CLOSED, getWidth() - mOffsetHeight, true);
                        }
                    } else {
                        smoothScrollToAndNotify(diff);
                    }
                    break;
            }
            break;
    }
    return true;
}

/**
 * Method is used to animate the view to the given position
 *
 * @param diff
 */
private void smoothScrollToAndNotify(int diff) {
    int length = getLength();
    LockMode stateToApply;
    if (diff > 0) {
        if (diff > length / 2.5) {
            stateToApply = LockMode.LOCK_MODE_CLOSED;
            notifyActionAndAnimateForState(stateToApply, getTranslationFor(stateToApply), true);
        } else if (mLockMode == LockMode.LOCK_MODE_OPEN) {
            stateToApply = LockMode.LOCK_MODE_OPEN;
            notifyActionAndAnimateForState(stateToApply, getTranslationFor(stateToApply), false);
        }
    } else {
        if (Math.abs(diff) > length / 2.5) {
            stateToApply = LockMode.LOCK_MODE_OPEN;
            notifyActionAndAnimateForState(stateToApply, getTranslationFor(stateToApply), true);
        } else if (mLockMode == LockMode.LOCK_MODE_CLOSED) {
            stateToApply = LockMode.LOCK_MODE_CLOSED;
            notifyActionAndAnimateForState(stateToApply, getTranslationFor(stateToApply), false);
        }
    }
}

/**
 * Method is used to retrieve dimensions meant for translation
 *
 * @param stateToApply
 * @return
 */
private int getTranslationFor(LockMode stateToApply) {

    switch (mStickTo) {
        case GRAVITY_BOTTOM:
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    return getHeight() - (getRawDisplayHeight(getContext()) - getLocationInYAxis(this));

                case LOCK_MODE_CLOSED:
                    return getRawDisplayHeight(getContext()) - getLocationInYAxis(this) - mOffsetHeight;
            }
            break;
        case GRAVITY_TOP:
            final int actionBarDiff = getRawDisplayHeight(getContext()) - ((View) getParent()).getHeight();
            final int y = getLocationInYAxis(this) + getHeight();

            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    return getHeight() - y + actionBarDiff;

                case LOCK_MODE_CLOSED:
                    return y - mOffsetHeight - actionBarDiff;
            }
            break;
        case GRAVITY_LEFT:
            final int x = getLocationInXAxis(this) + getWidth();
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    return getWidth() - x;

                case LOCK_MODE_CLOSED:
                    return x - mOffsetHeight;
            }
            break;
        case GRAVITY_RIGHT:
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    return getWidth() - (getRawDisplayWidth(getContext()) - getLocationInXAxis(this));

                case LOCK_MODE_CLOSED:
                    return getRawDisplayWidth(getContext()) - getLocationInXAxis(this) - mOffsetHeight;
            }
            break;
    }
    throw new IllegalStateException("Failed to return translation for drawer");
}

/**
 * Method is used to perform the animations
 *
 * @param stateToApply
 * @param translation
 * @param notify
 */
private void notifyActionAndAnimateForState(final LockMode stateToApply,
                                            final int translation, final boolean notify) {

    switch (mStickTo) {
        case GRAVITY_BOTTOM:
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    animate().translationY(-translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationY(0);
                                }
                            });
                    break;
                case LOCK_MODE_CLOSED:
                    animate().translationY(translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationY(0);
                                }
                            });
                    break;
            }
            break;
        case GRAVITY_TOP:
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    animate().translationY(translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationY(0);
                                }
                            });
                    break;
                case LOCK_MODE_CLOSED:
                    animate().translationY(-translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationY(0);
                                }
                            });
                    break;
            }
            break;
        case GRAVITY_LEFT:
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    animate().translationX(translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationX(0);
                                }
                            });
                    break;
                case LOCK_MODE_CLOSED:
                    animate().translationX(-translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationX(0);
                                }
                            });
                    break;
            }
            break;
        case GRAVITY_RIGHT:
            switch (stateToApply) {
                case LOCK_MODE_OPEN:
                    animate().translationX(-translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationX(0);
                                }
                            });
                    break;
                case LOCK_MODE_CLOSED:
                    animate().translationX(translation)
                            .setDuration(TRANSLATION_ANIM_DURATION)
                            .setInterpolator(new DecelerateInterpolator())
                            .setListener(new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    notifyActionForState(stateToApply, notify);
                                    setTranslationX(0);
                                }
                            });
                    break;
            }
            break;
    }
}

Additional notes: I have further insight into this issue. I commented out the ACTION_MOVE to eliminate the position of the drawer having been moved before the fling action. The animation works perfectly. I believe my idea is correct. To get translation for "open" I do

getHeight() - (getRawDisplayHeight(getContext()) - getLocationInYAxis(this))
  • getHeight() is the height of the drawer
  • getRawDisplayHeight(getContext()) is the height of the device screen
  • getLocationInYAxis(this), when ACTION_MOVE is out of the picture, is effectively (getRawDisplayHeight(getContext()) - mOffsetHeight). In
    this scenario they should be interchangeable.

So, what is left is the amount of distance necessary to translate. Once the drawer has been dragged x-distance, however, I am expecting that getLocationInYAxis(this) would return me the dragged position. But the calculation is off.

Crissman answered 6/4, 2017 at 20:31 Comment(0)
F
2

Seems that you should use the getTranslationFor function to calculate the new translation for the state.

You currently taking into account only the Height and the offset, but from the getTranslationFor code it seems that you should consider also getLocationInYAxis.

So, instead of this line:

notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, parent.getHeight() - mOffsetHeight, true);

try this line:

notifyActionAndAnimateForState(LockMode.LOCK_MODE_OPEN, getTranslationFor(LockMode.LOCK_MODE_OPEN), true);
Fraud answered 23/5, 2017 at 9:21 Comment(11)
Actually, the line you mentioned is for click/tap case, but the author's problem is one with the fling case. So, not sure that this is the correct answerюSoto
Well @rom4ek, if you take a look on the source file from which he started his version, you will see that there they use the line I provided for the fling as well - it simply calculate the relative translate needed from the current position - fling or no fling. Take a look at github.com/drxeno02/CustomDrawerLayout/blob/master/app/src/main/…Fraud
Seems they changed the code last night, and I was looking through the new lines. Sorry then.Soto
@EyalBiran the tap action is working perfectly as expected by taking the parent height, which is the height of the drawer and subtracting the offsetHeight. That logic works. The issue is with fling, because getLocationInYAxis(this) is not what I need it to be. I am really looking for the Y-axis of the drawer from the moment it was flung open.. For whatever reason, getLocationInYAxis(this) will fluctuate depending upon how hard I fling the drawer. Try to fling the drawer hard to see what I mean. That is the issue, because the math is wrong, even though I believe it should be correct.Crissman
Another clue I have is if you comment out the ACTION_MOVE, and then perform a fling action you will see the ideal animated behavior. The problem really is finding the distance between the top of the drawer and the height it has to translate after ACTION_MOVE has changed the layout parameters.Crissman
@Crissman Where can I find your full source code?Fraud
checkout this project github.com/drxeno02/CustomDrawerLayout.gitCrissman
@Crissman I don't see the code you added to the question in that project.. I want to play with your final code to see what the issue is...Fraud
@EyalBiran if you checkout that project, you will see everything. That is the entire project. Since the post the left, right, top code has been stripped down so that I can focus on bottom. If that is what you mean.Crissman
@EyanBiran any luck so far? I believe the calculation is correct, right? Just to confirm, when opening that value should be the same as if I getTop() for the drawer, or if I take the height of the screen and subtract getBottom() of the drawer. I have confirmed that all three of these calculations are the same. Yet my view still flings higher or lower than it should.Crissman
@Crissman I had a look, it seems to me that there are several things that are a bit off there that makes it all kinda hard to pinpoint the issue. I will suggest you take a look on this lib of a side menu (only one side) and try and grab the code you need from there github.com/lemonade-hq/SlideSideMenuFraud

© 2022 - 2024 — McMap. All rights reserved.