com.jakewharton.behavior.drawer.BehaviorDelegate.java Source code

Java tutorial

Introduction

Here is the source code for com.jakewharton.behavior.drawer.BehaviorDelegate.java

Source

/*
 * Copyright 2013 The Android Open Source Project
 * Copyright 2016 Jake Wharton
 *
 * 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.jakewharton.behavior.drawer;

import android.os.Build;
import android.os.SystemClock;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;

import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;

final class BehaviorDelegate extends ViewDragHelper.Callback {
    private static final int PEEK_DELAY = 160; // ms
    private static final int MIN_FLING_VELOCITY = 400; // dips per second
    private static final int FLAG_IS_OPENED = 0x1;
    private static final int FLAG_IS_OPENING = 0x2;
    private static final int FLAG_IS_CLOSING = 0x4;
    private static final int DEFAULT_SCRIM_COLOR = 0x99000000;

    private final CoordinatorLayout parent;
    private final View child;
    private final boolean isLeft;
    private final ContentScrimDrawer scrimDrawer;
    private final ViewDragHelper dragger;

    private float initialMotionX;
    private float initialMotionY;
    private boolean childrenCanceledTouch;
    private int openState;
    private boolean isPeeking;
    private float onScreen;
    private int drawerState;

    private int scrimColor = DEFAULT_SCRIM_COLOR;

    private final Runnable peekRunnable = new Runnable() {
        @Override
        public void run() {
            peekDrawer();
        }
    };
    private final Runnable draggerSettle = new Runnable() {
        @Override
        public void run() {
            if (dragger.continueSettling(true)) {
                ViewCompat.postOnAnimation(parent, this);
            }
        }
    };

    BehaviorDelegate(CoordinatorLayout parent, View child, int gravity) {
        this.parent = parent;
        this.child = child;

        int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(parent));
        this.isLeft = absGravity == Gravity.LEFT;

        float density = parent.getResources().getDisplayMetrics().density;
        float minVel = MIN_FLING_VELOCITY * density;

        dragger = ViewDragHelper.create(parent, this);
        dragger.setEdgeTrackingEnabled(isLeft ? ViewDragHelper.EDGE_LEFT : ViewDragHelper.EDGE_RIGHT);
        dragger.setMinVelocity(minVel);

        scrimDrawer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
                ? new ContentScrimDrawer.JellyBeanMr2(parent)
                : new ContentScrimDrawer.Base(parent, child);
    }

    private boolean isContentView(View child) {
        return child != this.child;
    }

    private boolean isDrawerView(View child) {
        return child == this.child;
    }

    private void removeCallbacks() {
        parent.removeCallbacks(peekRunnable);
    }

    private void peekDrawer() {
        int peekDistance = dragger.getEdgeSize();
        int childLeft;
        if (isLeft) {
            childLeft = -child.getWidth() + peekDistance;
        } else {
            childLeft = parent.getWidth() - peekDistance;
        }
        // Only peek if it would mean making the drawer more visible and the drawer isn't locked
        if ((isLeft && child.getLeft() < childLeft) //
                || (!isLeft && child.getLeft() > childLeft)) {
            dragger.smoothSlideViewTo(child, childLeft, child.getTop());
            ViewCompat.postOnAnimation(parent, draggerSettle);
            isPeeking = true;

            cancelChildViewTouch();
        }
    }

    private void cancelChildViewTouch() {
        // Cancel child touches
        if (!childrenCanceledTouch) {
            final long now = SystemClock.uptimeMillis();
            final MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            final int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                parent.getChildAt(i).dispatchTouchEvent(cancelEvent);
            }
            cancelEvent.recycle();
            childrenCanceledTouch = true;
        }
    }

    boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean interceptForDrag = dragger.shouldInterceptTouchEvent(ev);
        boolean interceptForTap = false;
        switch (MotionEventCompat.getActionMasked(ev)) {
        case MotionEvent.ACTION_DOWN: {
            float x = ev.getX();
            float y = ev.getY();
            initialMotionX = x;
            initialMotionY = y;
            if (onScreen > 0) {
                View child = dragger.findTopChildUnder((int) x, (int) y);
                if (child != null && isContentView(child)) {
                    interceptForTap = true;
                }
            }
            childrenCanceledTouch = false;
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            // If we cross the touch slop, don't perform the delayed peek for an edge touch.
            if (dragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) {
                removeCallbacks();
            }
            break;
        }

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            closeDrawers(true);
            childrenCanceledTouch = false;
            break;
        }
        }

        return interceptForDrag || interceptForTap || isPeeking || childrenCanceledTouch;
    }

    boolean onTouchEvent(MotionEvent ev) {
        dragger.processTouchEvent(ev);

        switch (MotionEventCompat.getActionMasked(ev)) {
        case MotionEvent.ACTION_DOWN: {
            initialMotionX = ev.getX();
            initialMotionY = ev.getY();
            childrenCanceledTouch = false;
            break;
        }

        case MotionEvent.ACTION_UP: {
            float x = ev.getX();
            float y = ev.getY();
            boolean peekingOnly = true;
            View touchedView = dragger.findTopChildUnder((int) x, (int) y);
            if (touchedView != null && isContentView(child)) {
                final float dx = x - initialMotionX;
                final float dy = y - initialMotionY;
                final int slop = dragger.getTouchSlop();
                if (dx * dx + dy * dy < slop * slop) {
                    // Taps close a dimmed open drawer but only if it isn't locked open.
                    if ((openState & FLAG_IS_OPENED) == FLAG_IS_OPENED) { // TODO isDrawerOpen method?
                        peekingOnly = false;
                    }
                }
            }
            closeDrawers(peekingOnly);
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            closeDrawers(true);
            childrenCanceledTouch = false;
            break;
        }
        }

        return true;
    }

    private void closeDrawers(boolean peekingOnly) {
        if (peekingOnly && !isPeeking) {
            return;
        }

        boolean needsSettle;
        if (isLeft) {
            needsSettle = dragger.smoothSlideViewTo(child, -child.getWidth(), child.getTop());
        } else {
            needsSettle = dragger.smoothSlideViewTo(child, parent.getWidth(), child.getTop());
        }
        isPeeking = false;

        removeCallbacks();

        if (needsSettle) {
            ViewCompat.postOnAnimation(parent, draggerSettle);
        }
    }

    @Override
    public void onViewCaptured(View capturedChild, int activePointerId) {
        isPeeking = false;
    }

    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        // Offset is how open the drawer is, therefore left/right values
        // are reversed from one another.
        float offset = onScreen;
        int childWidth = releasedChild.getWidth();

        int left;
        if (isLeft) {
            left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth;
        } else {
            int width = parent.getWidth();
            left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width;
        }

        dragger.settleCapturedViewAt(left, releasedChild.getTop());
        ViewCompat.postOnAnimation(parent, draggerSettle);
    }

    @Override
    public void onViewDragStateChanged(int state) {
        updateDrawerState(state, dragger.getCapturedView());
    }

    private void updateDrawerState(int activeState, View activeDrawer) {
        final int state = dragger.getViewDragState();

        if (activeDrawer != null && activeState == ViewDragHelper.STATE_IDLE) {
            if (onScreen == 0) {
                dispatchOnDrawerClosed(activeDrawer);
            } else if (onScreen == 1) {
                dispatchOnDrawerOpened(activeDrawer);
            }
        }

        if (state != drawerState) {
            drawerState = state;
        }
    }

    private void dispatchOnDrawerClosed(View drawerView) {
        if ((openState & FLAG_IS_OPENED) == FLAG_IS_OPENED) {
            openState = 0;

            updateChildrenImportantForAccessibility(drawerView, false);

            // Only send WINDOW_STATE_CHANGE if the host has window focus. This
            // may change if support for multiple foreground windows (e.g. IME)
            // improves.
            if (parent.hasWindowFocus()) {
                final View rootView = parent.getRootView();
                if (rootView != null) {
                    rootView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
                }
            }
        }
    }

    private void dispatchOnDrawerOpened(View drawerView) {
        if ((openState & FLAG_IS_OPENED) == 0) {
            openState = FLAG_IS_OPENED;

            updateChildrenImportantForAccessibility(drawerView, true);

            // Only send WINDOW_STATE_CHANGE if the host has window focus.
            if (parent.hasWindowFocus()) {
                parent.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
            }

            drawerView.requestFocus();
        }
    }

    private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) {
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            if (!isDrawerOpen && child != this.child || isDrawerOpen && child == drawerView) {
                // Drawer is closed and this is a content view or this is an
                // open drawer view, so it should be visible.
                ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
            } else {
                ViewCompat.setImportantForAccessibility(child,
                        ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
            }
        }
    }

    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        int childWidth = changedView.getWidth();

        // This reverses the positioning shown in onLayout.
        float offset;
        if (isLeft) {
            int edge = childWidth + left;
            offset = (float) edge / childWidth;
            scrimDrawer.setBounds(edge, 0, parent.getWidth(), parent.getHeight());
        } else {
            int edge = parent.getWidth() - left;
            offset = (float) edge / childWidth;
            scrimDrawer.setBounds(0, 0, left, parent.getHeight());
        }

        int baseAlpha = (scrimColor & 0xff000000) >>> 24;
        int imag = (int) (baseAlpha * offset);
        int color = imag << 24 | (scrimColor & 0xffffff);
        scrimDrawer.setColor(color);

        setDrawerViewOffset(offset);
        boolean gone = offset == 0;
        changedView.setVisibility(gone ? INVISIBLE : VISIBLE);
        scrimDrawer.setVisible(!gone);
        parent.invalidate();
    }

    private void setDrawerViewOffset(float slideOffset) {
        if (slideOffset == onScreen) {
            return;
        }

        onScreen = slideOffset;
    }

    @Override
    public void onEdgeTouched(int edgeFlags, int pointerId) {
        parent.postDelayed(peekRunnable, PEEK_DELAY);
    }

    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return isDrawerView(child);
    }

    @Override
    public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        if (((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT && isLeft)
                || ((edgeFlags & ViewDragHelper.EDGE_RIGHT) == ViewDragHelper.EDGE_RIGHT && !isLeft)) {
            dragger.captureChildView(child, pointerId);
        }
    }

    @Override
    public int getViewHorizontalDragRange(View child) {
        return isDrawerView(child) ? child.getWidth() : 0;
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        if (isLeft) {
            return Math.max(-child.getWidth(), Math.min(left, 0));
        } else {
            int width = parent.getWidth();
            return Math.max(width - child.getWidth(), Math.min(left, width));
        }
    }

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

    boolean onLayoutChild() {
        int width = parent.getMeasuredWidth();
        int height = parent.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();

        int childLeft;
        float newOffset;
        if (isLeft) {
            childLeft = -childWidth + (int) (childWidth * onScreen);
            newOffset = (float) (childWidth + childLeft) / childWidth;
        } else {
            childLeft = width - (int) (childWidth * onScreen);
            newOffset = (float) (width - childLeft) / childWidth;
        }

        boolean changeOffset = newOffset != onScreen;

        CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
        int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;

        switch (vgrav) {
        default:
        case Gravity.TOP: {
            child.layout(childLeft, lp.topMargin, childLeft + childWidth, lp.topMargin + childHeight);
            break;
        }

        case Gravity.BOTTOM: {
            child.layout(childLeft, height - lp.bottomMargin - child.getMeasuredHeight(), childLeft + childWidth,
                    height - lp.bottomMargin);
            break;
        }

        case Gravity.CENTER_VERTICAL: {
            int childTop = (height - childHeight) / 2;

            // Offset for margins. If things don't fit right because of
            // bad measurement before, oh well.
            if (childTop < lp.topMargin) {
                childTop = lp.topMargin;
            } else if (childTop + childHeight > height - lp.bottomMargin) {
                childTop = height - lp.bottomMargin - childHeight;
            }
            child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
            break;
        }
        }

        if (changeOffset) {
            setDrawerViewOffset(newOffset);
        }

        int newVisibility = onScreen > 0 ? VISIBLE : INVISIBLE;
        if (child.getVisibility() != newVisibility) {
            child.setVisibility(newVisibility);
        }
        return true;
    }
}