Java tutorial
/* * 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; } }