content: package com.alexvasilkov.gestures;
  
  import android.annotation.SuppressLint;
  import android.graphics.Matrix;
  import android.graphics.RectF;
  import android.view.MotionEvent;
  import android.view.ScaleGestureDetector;
  import android.view.View;
  import android.view.ViewConfiguration;
  
  import androidx.annotation.NonNull;
  import androidx.viewpager.widget.ViewPager;
  
  import com.alexvasilkov.gestures.internal.detectors.RotationGestureDetector;
  
  /**
   * Allows cross movement between view controlled by this {@link GestureController} and it's parent
   * {@link ViewPager} by splitting scroll movements between them.
   */
  public class GestureControllerForPager extends GestureController {
  
      private static final float SCROLL_THRESHOLD = 15f;
      private static final float OVERSCROLL_THRESHOLD_FACTOR = 4f;
  
      // Temporary objects
      private static final Matrix tmpMatrix = new Matrix();
      private static final RectF tmpRectF = new RectF();
  
  
      /**
       * Because ViewPager will immediately return true from onInterceptTouchEvent() method during
       * settling animation, we will have no chance to prevent it from doing this.
       * But this listener will be called if ViewPager intercepted touch event,
       * so we can try fix this behavior here.
       */
      private static final View.OnTouchListener PAGER_TOUCH_LISTENER = new View.OnTouchListener() {
          private boolean isTouchInProgress;
  
          @SuppressLint("ClickableViewAccessibility") // Not needed for ViewPager
          @Override
          public boolean onTouch(View view, @NonNull MotionEvent event) {
              // ViewPager will steal touch events during settling regardless of
              // requestDisallowInterceptTouchEvent. We will prevent it here.
              if (!isTouchInProgress && event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                  isTouchInProgress = true;
                  // Now ViewPager is in drag mode, so it should not intercept DOWN event
                  view.dispatchTouchEvent(event);
                  isTouchInProgress = false;
                  return true;
              }
  
              // User can touch outside of child view, so we will not have a chance to settle
              // ViewPager. If so, this listener should be called and we will be able to settle
              // ViewPager manually.
              settleViewPagerIfFinished((ViewPager) view, event);
  
              return true; // We should skip view pager touches to prevent some subtle bugs
          }
      };
  
      private final int touchSlop;
  
      private ViewPager viewPager;
      private boolean isViewPagerDisabled;
  
      private boolean isScrollGestureDetected;
      private boolean isSkipViewPager;
  
      private int viewPagerX;
      private float viewPagerSkippedX;
      private boolean isViewPagerInterceptedScroll;
      private float lastViewPagerEventX;
  
      public GestureControllerForPager(@NonNull View view) {
          super(view);
          touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
      }
  
      /**
       * Enables scroll inside {@link ViewPager}
       * (by enabling cross movement between ViewPager and it's child view).
       *
       * @param pager Target ViewPager
       */
      @SuppressLint("ClickableViewAccessibility")
      public void enableScrollInViewPager(ViewPager pager) {
          viewPager = pager;
          pager.setOnTouchListener(PAGER_TOUCH_LISTENER);
  
          // Disabling motion event splitting
          pager.setMotionEventSplittingEnabled(false);
      }
  
      /**
       * Disables ViewPager scroll. Default is false.
       *
       * @param disable Whether to disable ViewPager scroll or not
       */
      public void disableViewPager(boolean disable) {
          isViewPagerDisabled = disable;
      }
  
      @SuppressLint("ClickableViewAccessibility") // performClick() will be called in super class
      @Override
      public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
          // We need to always receive touch events to pass them to ViewPager (if provided)
          boolean result = super.onTouch(view, event);
          return viewPager != null || result;
      }
  
      @Override
      protected boolean onTouchInternal(@NonNull View view, @NonNull MotionEvent event) {
          if (viewPager == null) {
              return super.onTouchInternal(view, event);
          } else {
              // Getting motion event in pager coordinates
              MotionEvent pagerEvent = MotionEvent.obtain(event);
              transformToPagerEvent(pagerEvent, view, viewPager);
  
              handleTouch(pagerEvent);
  
              boolean result = super.onTouchInternal(view, pagerEvent);
              pagerEvent.recycle();
              return result;
          }
      }
  
      @Override
      protected boolean shouldDisallowInterceptTouch(MotionEvent event) {
          // If ViewPager is set then we'll always disallow touch interception
          return viewPager != null || super.shouldDisallowInterceptTouch(event);
      }
  
      private void handleTouch(MotionEvent event) {
          if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
              if (event.getPointerCount() == 2) { // on first non-primary pointer
                  // Skipping ViewPager fake dragging if we're not started dragging yet
                  // to allow scale/rotation gestures
                  isSkipViewPager = !hasViewPagerX();
              }
          }
      }
  
      @Override
      protected boolean onDown(@NonNull MotionEvent event) {
          if (viewPager == null) {
              return super.onDown(event);
          }
  
          isSkipViewPager = false;
          isViewPagerInterceptedScroll = false;
          isScrollGestureDetected = false;
  
          viewPagerX = computeInitialViewPagerScroll(event);
          lastViewPagerEventX = event.getX();
          viewPagerSkippedX = 0f;
  
          passEventToViewPager(event);
          return super.onDown(event);
      }
  
      @Override
      protected void onUpOrCancel(@NonNull MotionEvent event) {
          passEventToViewPager(event);
          super.onUpOrCancel(event);
      }
  
      @Override
      protected boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
              float dx, float dy) {
  
          if (viewPager == null) {
              return super.onScroll(e1, e2, dx, dy);
          } else {
              if (!isScrollGestureDetected) {
                  isScrollGestureDetected = true;
                  // First scroll event can stutter a bit, so we will ignore it for smoother scrolling
                  return true;
              }
  
              // Splitting movement between pager and view
              float fixedDistanceX = -scrollBy(e2, -dx);
              // Skipping vertical movement if ViewPager is dragged
              float fixedDistanceY = hasViewPagerX() ? 0f : dy;
  
              return super.onScroll(e1, e2, fixedDistanceX, fixedDistanceY);
          }
      }
  
      @Override
      protected boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
              float vx, float vy) {
  
          return !hasViewPagerX() && super.onFling(e1, e2, vx, vy);
      }
  
      @Override
      protected boolean onScaleBegin(@NonNull ScaleGestureDetector detector) {
          return !hasViewPagerX() && super.onScaleBegin(detector);
      }
  
      @Override
      protected boolean onRotationBegin(@NonNull RotationGestureDetector detector) {
          return !hasViewPagerX() && super.onRotationBegin(detector);
      }
  
      @Override
      protected boolean onDoubleTapEvent(@NonNull MotionEvent event) {
          return !hasViewPagerX() && super.onDoubleTapEvent(event);
      }
  
      /*
       * Scrolls ViewPager if view reached bounds. Returns distance at which view can be actually
       * scrolled. Here we will split given distance (dX) into movement of ViewPager and movement
       * of view itself.
       */
      private float scrollBy(@NonNull MotionEvent event, float dx) {
          if (isSkipViewPager || isViewPagerDisabled) {
              return dx;
          }
  
          final State state = getState();
          getStateController().getMovementArea(state, tmpRectF);
  
          float pagerDx = splitPagerScroll(dx, state, tmpRectF);
          pagerDx = skipPagerMovement(pagerDx, state, tmpRectF);
          float viewDx = dx - pagerDx;
  
          // Applying pager scroll
          boolean shouldFixViewX = isViewPagerInterceptedScroll && viewPagerX == 0;
          int actualX = performViewPagerScroll(event, pagerDx);
          viewPagerX += actualX;
          if (shouldFixViewX) { // Adding back scroll not handled by ViewPager
              viewDx += Math.round(pagerDx) - actualX;
          }
  
          // Returning altered scroll left for image
          return viewDx;
      }
  
      /*
       * Splits x scroll between viewpager and view.
       */
      @SuppressWarnings("SameParameterValue")
      private float splitPagerScroll(float dx, State state, RectF movBounds) {
          if (getSettings().isPanEnabled()) {
              final float dir = Math.signum(dx);
              final float movementX = Math.abs(dx); // always >= 0, no direction info
  
              final float viewX = state.getX();
              // available movement distances (always >= 0, no direction info)
              float availableViewX = dir < 0 ? viewX - movBounds.left : movBounds.right - viewX;
              float availablePagerX = dir * viewPagerX < 0 ? Math.abs(viewPagerX) : 0;
  
              // Not available if already overscrolled in same direction
              if (availableViewX < 0) {
                  availableViewX = 0;
              }
  
              float pagerMovementX;
              if (availablePagerX >= movementX) {
                  // Only ViewPager is moved
                  pagerMovementX = movementX;
              } else if (availableViewX + availablePagerX >= movementX) {
                  // Moving pager for full available distance and moving view for remaining distance
                  pagerMovementX = availablePagerX;
              } else {
                  // Moving view for full available distance and moving pager for remaining distance
                  pagerMovementX = movementX - availableViewX;
              }
  
              return pagerMovementX * dir; // Applying direction
          } else {
              return dx;
          }
      }
  
      /*
       * Skips part of pager movement to make it harder scrolling pager when image is zoomed
       * or when image is over-scrolled in y direction.
       */
      @SuppressWarnings("SameParameterValue")
      private float skipPagerMovement(float pagerDx, State state, RectF movBounds) {
          float overscrollDist = getSettings().getOverscrollDistanceY() * OVERSCROLL_THRESHOLD_FACTOR;
  
          float overscrollThreshold = 0f;
          if (state.getY() < movBounds.top) {
              overscrollThreshold = (movBounds.top - state.getY()) / overscrollDist;
          } else if (state.getY() > movBounds.bottom) {
              overscrollThreshold = (state.getY() - movBounds.bottom) / overscrollDist;
          }
  
          float minZoom = getStateController().getFitZoom(state);
          float zoomThreshold = minZoom == 0f ? 0f : state.getZoom() / minZoom - 1f;
  
          float pagerThreshold = Math.max(overscrollThreshold, zoomThreshold);
          pagerThreshold = (float) Math.sqrt(Math.max(0f, Math.min(pagerThreshold, 1f)));
          pagerThreshold *= SCROLL_THRESHOLD * touchSlop;
  
          // Resetting skipped amount when starting scrolling in different direction
          if (viewPagerSkippedX * pagerDx < 0f && viewPagerX == 0) {
              viewPagerSkippedX = 0f;
          }
  
          // Ensuring we have full skipped amount if pager is scrolled
          if (hasViewPagerX()) {
              viewPagerSkippedX = pagerThreshold * Math.signum(viewPagerX);
          }
  
          // Skipping pager movement and accumulating skipped amount, if not passed threshold
          if (Math.abs(viewPagerSkippedX) < pagerThreshold && pagerDx * viewPagerSkippedX >= 0) {
              viewPagerSkippedX += pagerDx;
              // Reverting over-skipped amount
              float over = Math.abs(viewPagerSkippedX) - pagerThreshold;
              over = Math.max(0f, over) * Math.signum(pagerDx);
  
              viewPagerSkippedX -= over;
              return over;
          } else {
              return pagerDx;
          }
      }
  
      private int computeInitialViewPagerScroll(MotionEvent downEvent) {
          // ViewPager can be in intermediate position, we should compute correct initial scroll
          int scroll = viewPager.getScrollX();
          int pageWidth = viewPager.getWidth() + viewPager.getPageMargin();
  
          // After state restore ViewPager can return negative scroll, let's fix it
          while (scroll < 0) {
              scroll += pageWidth;
          }
  
          int touchedItem = (int) ((scroll + downEvent.getX()) / pageWidth);
          return pageWidth * touchedItem - scroll;
      }
  
      private boolean hasViewPagerX() {
          // Looks like ViewPager has a rounding issue (it may be off by 1 in settled state)
          return viewPagerX < -1 || viewPagerX > 1;
      }
  
      /*
       * Manually scrolls ViewPager and returns actual distance at which pager was scrolled.
       */
      private int performViewPagerScroll(@NonNull MotionEvent event, float pagerDx) {
          int scrollBegin = viewPager.getScrollX();
          lastViewPagerEventX += pagerDx;
          passEventToViewPager(event);
          return scrollBegin - viewPager.getScrollX();
      }
  
      private void passEventToViewPager(@NonNull MotionEvent event) {
          if (viewPager == null) {
              return;
          }
  
          MotionEvent fixedEvent = obtainOnePointerEvent(event);
          fixedEvent.setLocation(lastViewPagerEventX, 0f);
  
          if (isViewPagerInterceptedScroll) {
              viewPager.onTouchEvent(fixedEvent);
          } else {
              isViewPagerInterceptedScroll = viewPager.onInterceptTouchEvent(fixedEvent);
          }
  
          // If ViewPager intercepted touch it will settle itself automatically,
          // but if touch was not intercepted we should settle it manually
          if (!isViewPagerInterceptedScroll && hasViewPagerX()) {
              settleViewPagerIfFinished(viewPager, event);
          }
  
          // Hack: ViewPager has bug when endFakeDrag() does not work properly. But we need to ensure
          // ViewPager is not in fake drag mode after settleViewPagerIfFinished()
          try {
              if (viewPager != null && viewPager.isFakeDragging()) {
                  viewPager.endFakeDrag();
              }
          } catch (Exception ignored) {
          }
  
          fixedEvent.recycle();
      }
  
      private static MotionEvent obtainOnePointerEvent(@NonNull MotionEvent event) {
          return MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(),
                  event.getX(), event.getY(), event.getMetaState());
      }
  
      private static void settleViewPagerIfFinished(ViewPager pager, @NonNull MotionEvent event) {
          if (event.getActionMasked() == MotionEvent.ACTION_UP
                  || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
              // Hack: if ViewPager is not settled we should force it to do so, fake drag will help
              try {
                  // Pager may throw an annoying exception if there are no internal page state items
                  pager.beginFakeDrag();
                  if (pager.isFakeDragging()) {
                      pager.endFakeDrag();
                  }
              } catch (Exception ignored) {
              }
          }
      }
  
      private static void transformToPagerEvent(MotionEvent event, View view, ViewPager pager) {
          tmpMatrix.reset();
          transformMatrixToPager(tmpMatrix, view, pager);
          event.transform(tmpMatrix);
      }
  
      /*
       * Inspired by hidden method View#transformMatrixToGlobal().
       */
      private static void transformMatrixToPager(Matrix matrix, View view, ViewPager pager) {
          if (view.getParent() instanceof View) {
              View parent = (View) view.getParent();
              if (parent != pager) {
                  transformMatrixToPager(matrix, parent, pager);
              }
              matrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
          }
  
          matrix.preTranslate(view.getLeft(), view.getTop());
          matrix.preConcat(view.getMatrix());
      }
  
  }
  