content: package com.alexvasilkov.gestures.internal;
  
  import android.graphics.Point;
  import android.graphics.RectF;
  import android.view.View;
  
  import androidx.annotation.NonNull;
  
  import com.alexvasilkov.gestures.GestureController;
  import com.alexvasilkov.gestures.GestureControllerForPager;
  import com.alexvasilkov.gestures.Settings.ExitType;
  import com.alexvasilkov.gestures.State;
  import com.alexvasilkov.gestures.animation.ViewPositionAnimator;
  import com.alexvasilkov.gestures.utils.GravityUtils;
  import com.alexvasilkov.gestures.utils.MathUtils;
  import com.alexvasilkov.gestures.views.interfaces.AnimatorView;
  
  public class ExitController {
  
      private static final float SCROLL_FACTOR = 0.5f;
      private static final float SCROLL_THRESHOLD = 30f;
  
      private static final float ZOOM_FACTOR = 0.75f;
      private static final float ZOOM_THRESHOLD = 0.75f;
  
      private static final float EXIT_THRESHOLD = 0.75f;
  
      private static final float MIN_EXIT_STATE = 0.01f; // Ensure we'll not hit 0 accidentally
  
      // Temporary objects
      private static final RectF tmpArea = new RectF();
      private static final Point tmpPivot = new Point();
  
      private final float scrollThresholdScaled;
  
      private final GestureController controller;
      private final AnimatorView animatorView;
  
      private float exitState = 1f; // 1f - fully opened, 0f - fully closed
  
      private boolean isZoomInAction;
      private boolean isRotationInAction;
  
      private boolean skipScrollDetection;
      private boolean skipZoomDetection;
  
      private boolean isScrollDetected;
      private boolean isZoomDetected;
  
      private float totalScrollX;
      private float totalScrollY;
      private float zoomAccumulator = 1f;
  
      private float scrollDirection;
      private float initialY;
      private float initialZoom;
  
  
      public ExitController(@NonNull View view, @NonNull GestureController gestureController) {
          controller = gestureController;
          animatorView = view instanceof AnimatorView ? ((AnimatorView) view) : null;
  
          scrollThresholdScaled = UnitsUtils.toPixels(view.getContext(), SCROLL_THRESHOLD);
      }
  
      public boolean isExitDetected() {
          return isScrollDetected || isZoomDetected;
      }
  
      public void stopDetection() {
          if (isExitDetected()) {
              exitState = 1f;
              updateState();
              finishDetection();
          }
      }
  
      public void onUpOrCancel() {
          finishDetection();
      }
  
      /**
       * @param dx The distance along the X axis that has been scrolled since the last call
       * @param dy The distance along the Y axis that has been scrolled since the last call
       * @return true if scroll was consumed, false otherwise.
       */
      public boolean onScroll(float dx, float dy) {
          // Exit by scroll should not be detected if zoom or rotation is currently in place.
          // Also, we can detect scroll only if image is zoomed out and it reached movement bounds.
  
          if (!skipScrollDetection && !isExitDetected() && canDetectExit()
                  && canDetectScroll() && !canScroll(dy)) {
  
              totalScrollX += dx;
              totalScrollY += dy;
  
              // Waiting until we scrolled enough to trigger exit detection or to skip it
              if (Math.abs(totalScrollY) > scrollThresholdScaled) {
                  isScrollDetected = true;
                  initialY = controller.getState().getY();
                  startDetection();
              } else if (Math.abs(totalScrollX) > scrollThresholdScaled) {
                  skipScrollDetection = true;
              }
          }
  
          if (isScrollDetected) {
              // Initializing scroll direction with current direction, if not initialized yet
              if (scrollDirection == 0f) {
                  scrollDirection = Math.signum(dy);
              }
  
              // Gradually decreasing scrolled distance when scrolling beyond exit point
              if (exitState < EXIT_THRESHOLD && Math.signum(dy) == scrollDirection) {
                  dy *= exitState / EXIT_THRESHOLD;
              }
  
              // Updating exit state depending on the amount scrolled in relation to total space
              final float total = scrollDirection * SCROLL_FACTOR * Math.max(
                      controller.getSettings().getMovementAreaW(),
                      controller.getSettings().getMovementAreaH());
  
              exitState = 1f - (controller.getState().getY() + dy - initialY) / total;
              exitState = MathUtils.restrict(exitState, MIN_EXIT_STATE, 1f);
  
              if (exitState == 1f) {
                  // Scrolling to initial position
                  controller.getState().translateTo(controller.getState().getX(), initialY);
              } else {
                  // Applying scrolled distance
                  controller.getState().translateBy(0f, dy);
              }
  
              updateState();
  
              if (exitState == 1f) {
                  finishDetection();
              }
              return true;
          }
  
          return isExitDetected();
      }
  
      /**
       * @return true if fling was consumed, false otherwise.
       */
      public boolean onFling() {
          return isExitDetected();
      }
  
      public void onScaleBegin() {
          isZoomInAction = true;
      }
  
      public void onScaleEnd() {
          isZoomInAction = false;
          skipZoomDetection = false;
          if (isZoomDetected) {
              finishDetection();
          }
      }
  
      /**
       * @param scaleFactor Current scaling factor
       * @return true if scale was consumed, false otherwise.
       */
      public boolean onScale(float scaleFactor) {
          // Exit by zoom should not be detected if rotation is currently in place.
          // Also, we can detect zoom only if image is zoomed out and we are zooming out.
  
          if (!canDetectZoom()) {
              skipZoomDetection = true;
          }
  
          if (!skipZoomDetection && !isExitDetected() && canDetectExit() && scaleFactor < 1f) {
  
              // Waiting until we zoomed enough to trigger exit detection
              zoomAccumulator *= scaleFactor;
              if (zoomAccumulator < ZOOM_THRESHOLD) {
                  isZoomDetected = true;
                  initialZoom = controller.getState().getZoom();
                  startDetection();
              }
          }
  
          if (isZoomDetected) {
              // Updating exit state by applying zoom factor
              exitState = controller.getState().getZoom() * scaleFactor / initialZoom;
              exitState = MathUtils.restrict(exitState, MIN_EXIT_STATE, 1f);
  
              GravityUtils.getDefaultPivot(controller.getSettings(), tmpPivot);
  
              if (exitState == 1f) {
                  // Zooming to initial level using default pivot point
                  controller.getState().zoomTo(initialZoom, tmpPivot.x, tmpPivot.y);
              } else {
                  // Applying zoom factor using default pivot point
                  final float scaleFactorFixed = 1f + (scaleFactor - 1f) * ZOOM_FACTOR;
                  controller.getState().zoomBy(scaleFactorFixed, tmpPivot.x, tmpPivot.y);
              }
  
              updateState();
  
              if (exitState == 1f) {
                  finishDetection();
                  return true;
              }
          }
  
          return isExitDetected();
      }
  
      public void applyZoomPatch() {
          // Applying zoom patch (needed in case if image size is changed)
          initialZoom = controller.getStateController().applyZoomPatch(initialZoom);
      }
  
      public void onRotationBegin() {
          isRotationInAction = true;
      }
  
      public void onRotationEnd() {
          isRotationInAction = false;
      }
  
      /**
       * @return true if rotation was consumed, false otherwise.
       */
      public boolean onRotate() {
          return isExitDetected();
      }
  
  
      private boolean canDetectExit() {
          return controller.getSettings().isExitEnabled() && animatorView != null
                  && !animatorView.getPositionAnimator().isLeaving();
      }
  
      private boolean canDetectScroll() {
          ExitType exitType = controller.getSettings().getExitType();
          return (exitType == ExitType.ALL || exitType == ExitType.SCROLL)
                  && !isZoomInAction && !isRotationInAction && isZoomedOut();
      }
  
      private boolean canDetectZoom() {
          ExitType exitType = controller.getSettings().getExitType();
          return (exitType == ExitType.ALL || exitType == ExitType.ZOOM)
                  && !isRotationInAction && isZoomedOut();
      }
  
  
      private boolean canScroll(float dy) {
          if (!controller.getSettings().isRestrictBounds()) {
              return true;
          }
          final State state = controller.getState();
          controller.getStateController().getMovementArea(state, tmpArea);
          return (dy > 0f && State.compare(state.getY(), tmpArea.bottom) < 0f)
                  || (dy < 0f && State.compare(state.getY(), tmpArea.top) > 0f);
      }
  
      private boolean isZoomedOut() {
          final State state = controller.getState();
          final float minZoom = controller.getStateController().getMinZoom(state);
          return State.compare(state.getZoom(), minZoom) <= 0;
      }
  
  
      private void startDetection() {
          controller.getSettings().disableBounds();
  
          if (controller instanceof GestureControllerForPager) {
              ((GestureControllerForPager) controller).disableViewPager(true);
          }
      }
  
      private void finishDetection() {
          if (isExitDetected()) {
              if (controller instanceof GestureControllerForPager) {
                  ((GestureControllerForPager) controller).disableViewPager(false);
              }
  
              controller.getSettings().enableBounds();
  
              final ViewPositionAnimator animator = animatorView.getPositionAnimator();
  
              if (!animator.isAnimating() && canDetectExit()) {
                  // Exiting or returning to initial state if the view is not yet animating
  
                  final float position = animator.getPosition();
                  final boolean isLeaving = position < EXIT_THRESHOLD;
  
                  if (isLeaving) {
                      animator.exit(true);
                  } else {
                      final float y = controller.getState().getY();
                      final float zoom = controller.getState().getZoom();
                      final boolean isScrolledBack = isScrollDetected && State.equals(y, initialY);
                      final boolean isZoomedBack = isZoomDetected && State.equals(zoom, initialZoom);
  
                      if (position < 1f) {
                          animator.setState(position, false, true);
  
                          // Animating bounds if user didn't scroll or zoom to initial position
                          // manually. We will also need to temporary re-enable bounds restrictions
                          // disabled by position animator (a bit hacky though).
                          if (!isScrolledBack && !isZoomedBack) {
                              controller.getSettings().enableBounds();
                              controller.animateKeepInBounds();
                              controller.getSettings().disableBounds();
                          }
                      }
                  }
              }
          }
  
          isScrollDetected = false;
          isZoomDetected = false;
          skipScrollDetection = false;
          exitState = 1f;
          scrollDirection = 0f;
          totalScrollX = 0f;
          totalScrollY = 0f;
          zoomAccumulator = 1f;
      }
  
      private void updateState() {
          if (canDetectExit()) {
              animatorView.getPositionAnimator().setToState(controller.getState(), exitState);
              animatorView.getPositionAnimator().setState(exitState, false, false);
          }
      }
  
  }
  