diff --git a/.all-contributorsrc b/.all-contributorsrc index cd865d08..7a9958b9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -79,6 +79,25 @@ "code" ] }, + { + "login": "stamatiap", + "name": "Stamatia Papageorgiou", + "avatar_url": "https://avatars.githubusercontent.com/u/57223967?v=4", + "profile": "https://github.com/stamatiap", + "contributions": [ + "code", + "translation" + ] + }, + { + "login": "The-EDev", + "name": "Farook Al-Sammarraie", + "avatar_url": "https://avatars.githubusercontent.com/u/60552923?v=4", + "profile": "https://github.com/The-EDev", + "contributions": [ + "code" + ] + }, { "login": "Zopieux", "name": "Alexandre Macabies", diff --git a/.github/workflows/github_nightly_release.yml b/.github/workflows/github_nightly_release.yml index 8edb6a63..bddf63b5 100644 --- a/.github/workflows/github_nightly_release.yml +++ b/.github/workflows/github_nightly_release.yml @@ -15,10 +15,11 @@ jobs: uses: actions/checkout@v2 - name: set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: - java-version: 1.8 - + distribution: 'zulu' + java-version: '8' + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/github_pre_release.yml b/.github/workflows/github_pre_release.yml index 9700fdfc..42d8e481 100644 --- a/.github/workflows/github_pre_release.yml +++ b/.github/workflows/github_pre_release.yml @@ -16,9 +16,10 @@ jobs: uses: actions/checkout@v2 - name: set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'zulu' + java-version: '8' - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/README.md b/README.md index d1301419..15999d16 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) [![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE) [![GitHub stars](https://img.shields.io/github/stars/austinhuang0131/instagrabber.svg?style=social&label=Star)](https://GitHub.com/austinhuang0131/barinsta/stargazers/) -[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg)](#contributors) +[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg)](#contributors) Instagram client; previously known as InstaGrabber. @@ -63,49 +63,53 @@ Prominent contributors are listed here in the [all-contributors](https://allcont
+ * This is primarily used when animating the [WindowInsetsCompat.Type.ime], so that the
+ * appropriate view is focused for accepting input from the IME.
+ */
+public class ControlFocusInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback {
+
+ private final View view;
+
+ public ControlFocusInsetsAnimationCallback(@NonNull final View view) {
+ this(view, DISPATCH_MODE_STOP);
+ }
+
+ /**
+ * @param view the view to request/clear focus
+ * @param dispatchMode The dispatch mode for this callback.
+ * @see WindowInsetsAnimationCompat.Callback.DispatchMode
+ */
+ public ControlFocusInsetsAnimationCallback(@NonNull final View view, final int dispatchMode) {
+ super(dispatchMode);
+ this.view = view;
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List
+ * This class enables the root view is selectively defer handling any insets which match
+ * [deferredInsetTypes], to enable better looking [WindowInsetsAnimationCompat]s.
+ *
+ * An example is the following: when a [WindowInsetsAnimationCompat] is started, the system will dispatch
+ * a [WindowInsetsCompat] instance which contains the end state of the animation. For the scenario of
+ * the IME being animated in, that means that the insets contains the IME height. If the view's
+ * [View.OnApplyWindowInsetsListener] simply always applied the combination of
+ * [WindowInsetsCompat.Type.ime] and [WindowInsetsCompat.Type.systemBars] using padding, the viewport of any
+ * child views would then be smaller. This results in us animating a smaller (padded-in) view into
+ * a larger viewport. Visually, this results in the views looking clipped.
+ *
+ * This class allows us to implement a different strategy for the above scenario, by selectively
+ * deferring the [WindowInsetsCompat.Type.ime] insets until the [WindowInsetsAnimationCompat] is ended.
+ * For the above example, you would create a [RootViewDeferringInsetsCallback] like so:
+ *
+ * ```
+ * val callback = RootViewDeferringInsetsCallback(
+ * persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
+ * deferredInsetTypes = WindowInsetsCompat.Type.ime()
+ * )
+ * ```
+ *
+ * This class is not limited to just IME animations, and can work with any [WindowInsetsCompat.Type]s.
+ */
+public class RootViewDeferringInsetsCallback extends WindowInsetsAnimationCompat.Callback implements OnApplyWindowInsetsListener {
+
+ private final int persistentInsetTypes;
+ private final int deferredInsetTypes;
+ @Nullable
+ private View view = null;
+ @Nullable
+ private WindowInsetsCompat lastWindowInsets = null;
+ private boolean deferredInsets = false;
+
+ /**
+ * @param persistentInsetTypes the bitmask of any inset types which should always be handled
+ * through padding the attached view
+ * @param deferredInsetTypes the bitmask of insets types which should be deferred until after
+ * any related [WindowInsetsAnimationCompat]s have ended
+ */
+ public RootViewDeferringInsetsCallback(final int persistentInsetTypes, final int deferredInsetTypes) {
+ super(DISPATCH_MODE_CONTINUE_ON_SUBTREE);
+ if ((persistentInsetTypes & deferredInsetTypes) != 0) {
+ throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " +
+ "any of same WindowInsetsCompat.Type values");
+ }
+ this.persistentInsetTypes = persistentInsetTypes;
+ this.deferredInsetTypes = deferredInsetTypes;
+ }
+
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(@NonNull final View v, @NonNull final WindowInsetsCompat windowInsets) {
+ // Store the view and insets for us in onEnd() below
+ view = v;
+ lastWindowInsets = windowInsets;
+
+ final int types = deferredInsets
+ // When the deferred flag is enabled, we only use the systemBars() insets
+ ? persistentInsetTypes
+ // Otherwise we handle the combination of the the systemBars() and ime() insets
+ : persistentInsetTypes | deferredInsetTypes;
+
+ // Finally we apply the resolved insets by setting them as padding
+ final Insets typeInsets = windowInsets.getInsets(types);
+ v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom);
+
+ // We return the new WindowInsetsCompat.CONSUMED to stop the insets being dispatched any
+ // further into the view hierarchy. This replaces the deprecated
+ // WindowInsetsCompat.consumeSystemWindowInsets() and related functions.
+ return WindowInsetsCompat.CONSUMED;
+ }
+
+ @Override
+ public void onPrepare(WindowInsetsAnimationCompat animation) {
+ if ((animation.getTypeMask() & deferredInsetTypes) != 0) {
+ // We defer the WindowInsetsCompat.Type.ime() insets if the IME is currently not visible.
+ // This results in only the WindowInsetsCompat.Type.systemBars() being applied, allowing
+ // the scrolling view to remain at it's larger size.
+ deferredInsets = true;
+ }
+ }
+
+ @NonNull
+ @Override
+ public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets,
+ @NonNull final List
+ * See [InsetsAnimationLinearLayout] and [InsetsAnimationTouchListener] for examples of how
+ * to use this class.
+ */
+public class SimpleImeAnimationController {
+ private static final String TAG = SimpleImeAnimationController.class.getSimpleName();
+ /**
+ * Scroll threshold for determining whether to animating to the end state, or to the start state.
+ * Currently 15% of the total swipe distance distance
+ */
+ private static final float SCROLL_THRESHOLD = 0.15f;
+
+ @Nullable
+ private WindowInsetsAnimationControllerCompat insetsAnimationController = null;
+ @Nullable
+ private CancellationSignal pendingRequestCancellationSignal = null;
+ @Nullable
+ private OnRequestReadyListener pendingRequestOnReadyListener;
+ /**
+ * True if the IME was shown at the start of the current animation.
+ */
+ private boolean isImeShownAtStart = false;
+ @Nullable
+ private SpringAnimation currentSpringAnimation = null;
+ private WindowInsetsAnimationControlListenerCompat fwdListener;
+
+ /**
+ * A LinearInterpolator instance we can re-use across listeners.
+ */
+ private final LinearInterpolator linearInterpolator = new LinearInterpolator();
+ /* To take control of the an WindowInsetsAnimation, we need to pass in a listener to
+ controlWindowInsetsAnimation() in startControlRequest(). The listener created here
+ keeps track of the current WindowInsetsAnimationController and resets our state. */
+ private final WindowInsetsAnimationControlListenerCompat animationControlListener = new WindowInsetsAnimationControlListenerCompat() {
+ /**
+ * Once the request is ready, call our [onRequestReady] function
+ */
+ @Override
+ public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {
+ onRequestReady(controller);
+ if (fwdListener != null) {
+ fwdListener.onReady(controller, types);
+ }
+ }
+
+ /**
+ * If the request is finished, we should reset our internal state
+ */
+ @Override
+ public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) {
+ reset();
+ if (fwdListener != null) {
+ fwdListener.onFinished(controller);
+ }
+ }
+
+ /**
+ * If the request is cancelled, we should reset our internal state
+ */
+ @Override
+ public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) {
+ reset();
+ if (fwdListener != null) {
+ fwdListener.onCancelled(controller);
+ }
+ }
+ };
+
+ /**
+ * Start a control request to the [view]s [android.view.WindowInsetsController]. This should
+ * be called once the view is in a position to take control over the position of the IME.
+ *
+ * @param view The view which is triggering this request
+ * @param onRequestReadyListener optional listener which will be called when the request is ready and
+ * the animation can proceed
+ */
+ public void startControlRequest(@NonNull final View view,
+ @Nullable final OnRequestReadyListener onRequestReadyListener) {
+ if (isInsetAnimationInProgress()) {
+ Log.w(TAG, "startControlRequest: Animation in progress. Can not start a new request to controlWindowInsetsAnimation()");
+ return;
+ }
+
+ // Keep track of the IME insets, and the IME visibility, at the start of the request
+ final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view);
+ if (rootWindowInsets != null) {
+ isImeShownAtStart = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
+ }
+
+ // Create a cancellation signal, which we pass to controlWindowInsetsAnimation() below
+ pendingRequestCancellationSignal = new CancellationSignal();
+ // Keep reference to the onReady callback
+ pendingRequestOnReadyListener = onRequestReadyListener;
+
+ // Finally we make a controlWindowInsetsAnimation() request:
+ final WindowInsetsControllerCompat windowInsetsController = ViewCompat.getWindowInsetsController(view);
+ if (windowInsetsController != null) {
+ windowInsetsController.controlWindowInsetsAnimation(
+ // We're only catering for IME animations in this listener
+ WindowInsetsCompat.Type.ime(),
+ // Animation duration. This is not used by the system, and is only passed to any
+ // WindowInsetsAnimation.Callback set on views. We pass in -1 to indicate that we're
+ // not starting a finite animation, and that this is completely controlled by
+ // the user's touch.
+ -1,
+ // The time interpolator used in calculating the animation progress. The fraction value
+ // we passed into setInsetsAndAlpha() which be passed into this interpolator before
+ // being used by the system to inset the IME. LinearInterpolator is a good type
+ // to use for scrolling gestures.
+ linearInterpolator,
+ // A cancellation signal, which allows us to cancel the request to control
+ pendingRequestCancellationSignal,
+ // The WindowInsetsAnimationControlListener
+ animationControlListener
+ );
+ }
+ }
+
+ /**
+ * Start a control request to the [view]s [android.view.WindowInsetsController], similar to
+ * [startControlRequest], but immediately fling to a finish using [velocityY] once ready.
+ *
+ * This function is useful for fire-and-forget operations to animate the IME.
+ *
+ * @param view The view which is triggering this request
+ * @param velocityY the velocity of the touch gesture which caused this call
+ */
+ public void startAndFling(@NonNull final View view, final float velocityY) {
+ startControlRequest(view, null);
+ animateToFinish(velocityY);
+ }
+
+ /**
+ * Update the inset position of the IME by the given [dy] value. This value will be coerced
+ * into the hidden and shown inset values.
+ *
+ * This function should only be called if [isInsetAnimationInProgress] returns true.
+ *
+ * @return the amount of [dy] consumed by the inset animation, in pixels
+ */
+ public int insetBy(final int dy) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Current WindowInsetsAnimationController is null." +
+ "This should only be called if isAnimationInProgress() returns true");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ // Call updateInsetTo() with the new inset value
+ return insetTo(controller.getCurrentInsets().bottom - dy);
+ }
+
+ /**
+ * Update the inset position of the IME to be the given [inset] value. This value will be
+ * coerced into the hidden and shown inset values.
+ *
+ * This function should only be called if [isInsetAnimationInProgress] returns true.
+ *
+ * @return the distance moved by the inset animation, in pixels
+ */
+ public int insetTo(final int inset) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Current WindowInsetsAnimationController is null." +
+ "This should only be called if isAnimationInProgress() returns true");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ final int hiddenBottom = controller.getHiddenStateInsets().bottom;
+ final int shownBottom = controller.getShownStateInsets().bottom;
+ final int startBottom = isImeShownAtStart ? shownBottom : hiddenBottom;
+ final int endBottom = isImeShownAtStart ? hiddenBottom : shownBottom;
+
+ // We coerce the given inset within the limits of the hidden and shown insets
+ final int coercedBottom = coerceIn(inset, hiddenBottom, shownBottom);
+
+ final int consumedDy = controller.getCurrentInsets().bottom - coercedBottom;
+
+ // Finally update the insets in the WindowInsetsAnimationController using
+ // setInsetsAndAlpha().
+ controller.setInsetsAndAlpha(
+ // Here we update the animating insets. This is what controls where the IME is displayed.
+ // It is also passed through to views via their WindowInsetsAnimation.Callback.
+ Insets.of(0, 0, 0, coercedBottom),
+ // This controls the alpha value. We don't want to alter the alpha so use 1f
+ 1f,
+ // Finally we calculate the animation progress fraction. This value is passed through
+ // to any WindowInsetsAnimation.Callbacks, but it is not used by the system.
+ (coercedBottom - startBottom) / (float) (endBottom - startBottom)
+ );
+
+ return consumedDy;
+ }
+
+ /**
+ * Return `true` if an inset animation is in progress.
+ */
+ public boolean isInsetAnimationInProgress() {
+ return insetsAnimationController != null;
+ }
+
+ /**
+ * Return `true` if an inset animation is currently finishing.
+ */
+ public boolean isInsetAnimationFinishing() {
+ return currentSpringAnimation != null;
+ }
+
+ /**
+ * Return `true` if a request to control an inset animation is in progress.
+ */
+ public boolean isInsetAnimationRequestPending() {
+ return pendingRequestCancellationSignal != null;
+ }
+
+ /**
+ * Cancel the current [WindowInsetsAnimationControllerCompat]. We immediately finish
+ * the animation, reverting back to the state at the start of the gesture.
+ */
+ public void cancel() {
+ if (insetsAnimationController != null) {
+ insetsAnimationController.finish(isImeShownAtStart);
+ }
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ if (currentSpringAnimation != null) {
+ // Cancel the current spring animation
+ currentSpringAnimation.cancel();
+ }
+ reset();
+ }
+
+ /**
+ * Finish the current [WindowInsetsAnimationControllerCompat] immediately.
+ */
+ public void finish() {
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ if (controller == null) {
+ // If we don't currently have a controller, cancel any pending request and return
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ return;
+ }
+
+ final int current = controller.getCurrentInsets().bottom;
+ final int shown = controller.getShownStateInsets().bottom;
+ final int hidden = controller.getHiddenStateInsets().bottom;
+
+ // The current inset matches either the shown/hidden inset, finish() immediately
+ if (current == shown) {
+ controller.finish(true);
+ } else if (current == hidden) {
+ controller.finish(false);
+ } else {
+ // Otherwise, we'll look at the current position...
+ if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) {
+ // If the IME is past the 'threshold' we snap to the toggled state
+ controller.finish(!isImeShownAtStart);
+ } else {
+ // ...otherwise, we snap back to the original visibility
+ controller.finish(isImeShownAtStart);
+ }
+ }
+ }
+
+ /**
+ * Finish the current [WindowInsetsAnimationControllerCompat]. We finish the animation,
+ * animating to the end state if necessary.
+ *
+ * @param velocityY the velocity of the touch gesture which caused this call to [animateToFinish].
+ * Can be `null` if velocity is not available.
+ */
+ public void animateToFinish(@Nullable final Float velocityY) {
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ if (controller == null) {
+ // If we don't currently have a controller, cancel any pending request and return
+ if (pendingRequestCancellationSignal != null) {
+ pendingRequestCancellationSignal.cancel();
+ }
+ return;
+ }
+
+ final int current = controller.getCurrentInsets().bottom;
+ final int shown = controller.getShownStateInsets().bottom;
+ final int hidden = controller.getHiddenStateInsets().bottom;
+
+ if (velocityY != null) {
+ // If we have a velocity, we can use it's direction to determine
+ // the visibility. Upwards == visible
+ animateImeToVisibility(velocityY > 0, velocityY);
+ } else if (current == shown) {
+ // The current inset matches either the shown/hidden inset, finish() immediately
+ controller.finish(true);
+ } else if (current == hidden) {
+ controller.finish(false);
+ } else {
+ // Otherwise, we'll look at the current position...
+ if (controller.getCurrentFraction() >= SCROLL_THRESHOLD) {
+ // If the IME is past the 'threshold' we animate it to the toggled state
+ animateImeToVisibility(!isImeShownAtStart, null);
+ } else {
+ // ...otherwise, we animate it back to the original visibility
+ animateImeToVisibility(isImeShownAtStart, null);
+ }
+ }
+ }
+
+ private void onRequestReady(@NonNull final WindowInsetsAnimationControllerCompat controller) {
+ // The request is ready, so clear out the pending cancellation signal
+ pendingRequestCancellationSignal = null;
+ // Store the current WindowInsetsAnimationController
+ insetsAnimationController = controller;
+
+ // Call any pending callback
+ if (pendingRequestOnReadyListener != null) {
+ pendingRequestOnReadyListener.onRequestReady(controller);
+ }
+ pendingRequestOnReadyListener = null;
+ }
+
+ /**
+ * Resets all of our internal state.
+ */
+ private void reset() {
+ // Clear all of our internal state
+ insetsAnimationController = null;
+ pendingRequestCancellationSignal = null;
+ isImeShownAtStart = false;
+ if (currentSpringAnimation != null) {
+ currentSpringAnimation.cancel();
+ }
+ currentSpringAnimation = null;
+ pendingRequestOnReadyListener = null;
+ }
+
+ /**
+ * Animate the IME to a given visibility.
+ *
+ * @param visible `true` to animate the IME to it's fully shown state, `false` to it's
+ * fully hidden state.
+ * @param velocityY the velocity of the touch gesture which caused this call. Can be `null`
+ * if velocity is not available.
+ */
+ private void animateImeToVisibility(final boolean visible, @Nullable final Float velocityY) {
+ if (insetsAnimationController == null) {
+ throw new IllegalStateException("Controller should not be null");
+ }
+ final WindowInsetsAnimationControllerCompat controller = insetsAnimationController;
+
+ final FloatPropertyCompat