Browse Source
Update keyboard/emojipicker visiblity logic. Fixes austinhuang0131/barinsta#1181. Also check description.
Update keyboard/emojipicker visiblity logic. Fixes austinhuang0131/barinsta#1181. Also check description.
This commits adds some special handling for Android 11+ users regarding keyboard visibility. Check https://github.com/android/user-interface-samples/tree/master/WindowInsetsAnimation.renovate/org.robolectric-robolectric-4.x
Ammar Githam
4 years ago
16 changed files with 1830 additions and 474 deletions
-
7app/build.gradle
-
3app/src/main/AndroidManifest.xml
-
14app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
246app/src/main/java/awais/instagrabber/customviews/InsetsAnimationLinearLayout.java
-
33app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingCoordinatorLayout.java
-
35app/src/main/java/awais/instagrabber/customviews/InsetsNotifyingLinearLayout.java
-
87app/src/main/java/awais/instagrabber/customviews/helpers/ControlFocusInsetsAnimationCallback.java
-
117app/src/main/java/awais/instagrabber/customviews/helpers/EmojiPickerInsetsAnimationCallback.java
-
139app/src/main/java/awais/instagrabber/customviews/helpers/RootViewDeferringInsetsCallback.java
-
443app/src/main/java/awais/instagrabber/customviews/helpers/SimpleImeAnimationController.java
-
128app/src/main/java/awais/instagrabber/customviews/helpers/TranslateDeferringInsetsAnimationCallback.java
-
396app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
-
37app/src/main/java/awais/instagrabber/utils/Utils.java
-
51app/src/main/java/awais/instagrabber/utils/ViewUtils.java
-
5app/src/main/res/layout/activity_main.xml
-
39app/src/main/res/layout/fragment_direct_messages_thread.xml
@ -0,0 +1,246 @@ |
|||||
|
package awais.instagrabber.customviews; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.os.Build; |
||||
|
import android.util.AttributeSet; |
||||
|
import android.view.View; |
||||
|
import android.view.WindowInsetsAnimation; |
||||
|
import android.widget.LinearLayout; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.core.view.NestedScrollingParent3; |
||||
|
import androidx.core.view.NestedScrollingParentHelper; |
||||
|
import androidx.core.view.ViewCompat; |
||||
|
import androidx.core.view.WindowInsetsCompat; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
|
||||
|
import awais.instagrabber.customviews.helpers.SimpleImeAnimationController; |
||||
|
import awais.instagrabber.utils.ViewUtils; |
||||
|
|
||||
|
import static androidx.core.view.ViewCompat.TYPE_TOUCH; |
||||
|
|
||||
|
public final class InsetsAnimationLinearLayout extends LinearLayout implements NestedScrollingParent3 { |
||||
|
private final NestedScrollingParentHelper nestedScrollingParentHelper = new NestedScrollingParentHelper(this); |
||||
|
private final SimpleImeAnimationController imeAnimController = new SimpleImeAnimationController(); |
||||
|
private final int[] tempIntArray2 = new int[2]; |
||||
|
private final int[] startViewLocation = new int[2]; |
||||
|
|
||||
|
private View currentNestedScrollingChild; |
||||
|
private int dropNextY; |
||||
|
private boolean scrollImeOffScreenWhenVisible = true; |
||||
|
private boolean scrollImeOnScreenWhenNotVisible = true; |
||||
|
private boolean scrollImeOffScreenWhenVisibleOnFling = false; |
||||
|
private boolean scrollImeOnScreenWhenNotVisibleOnFling = false; |
||||
|
|
||||
|
public InsetsAnimationLinearLayout(final Context context, @Nullable final AttributeSet attrs) { |
||||
|
super(context, attrs); |
||||
|
} |
||||
|
|
||||
|
public InsetsAnimationLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
||||
|
super(context, attrs, defStyleAttr); |
||||
|
} |
||||
|
|
||||
|
public final boolean getScrollImeOffScreenWhenVisible() { |
||||
|
return scrollImeOffScreenWhenVisible; |
||||
|
} |
||||
|
|
||||
|
public final void setScrollImeOffScreenWhenVisible(boolean scrollImeOffScreenWhenVisible) { |
||||
|
this.scrollImeOffScreenWhenVisible = scrollImeOffScreenWhenVisible; |
||||
|
} |
||||
|
|
||||
|
public final boolean getScrollImeOnScreenWhenNotVisible() { |
||||
|
return scrollImeOnScreenWhenNotVisible; |
||||
|
} |
||||
|
|
||||
|
public final void setScrollImeOnScreenWhenNotVisible(boolean scrollImeOnScreenWhenNotVisible) { |
||||
|
this.scrollImeOnScreenWhenNotVisible = scrollImeOnScreenWhenNotVisible; |
||||
|
} |
||||
|
|
||||
|
public boolean getScrollImeOffScreenWhenVisibleOnFling() { |
||||
|
return scrollImeOffScreenWhenVisibleOnFling; |
||||
|
} |
||||
|
|
||||
|
public void setScrollImeOffScreenWhenVisibleOnFling(final boolean scrollImeOffScreenWhenVisibleOnFling) { |
||||
|
this.scrollImeOffScreenWhenVisibleOnFling = scrollImeOffScreenWhenVisibleOnFling; |
||||
|
} |
||||
|
|
||||
|
public boolean getScrollImeOnScreenWhenNotVisibleOnFling() { |
||||
|
return scrollImeOnScreenWhenNotVisibleOnFling; |
||||
|
} |
||||
|
|
||||
|
public void setScrollImeOnScreenWhenNotVisibleOnFling(final boolean scrollImeOnScreenWhenNotVisibleOnFling) { |
||||
|
this.scrollImeOnScreenWhenNotVisibleOnFling = scrollImeOnScreenWhenNotVisibleOnFling; |
||||
|
} |
||||
|
|
||||
|
public SimpleImeAnimationController getImeAnimController() { |
||||
|
return imeAnimController; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean onStartNestedScroll(@NonNull final View child, |
||||
|
@NonNull final View target, |
||||
|
final int axes, |
||||
|
final int type) { |
||||
|
return (axes & SCROLL_AXIS_VERTICAL) != 0 && type == TYPE_TOUCH; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onNestedScrollAccepted(@NonNull final View child, |
||||
|
@NonNull final View target, |
||||
|
final int axes, |
||||
|
final int type) { |
||||
|
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type); |
||||
|
currentNestedScrollingChild = child; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onNestedPreScroll(@NonNull final View target, |
||||
|
final int dx, |
||||
|
final int dy, |
||||
|
@NonNull final int[] consumed, |
||||
|
final int type) { |
||||
|
if (imeAnimController.isInsetAnimationRequestPending()) { |
||||
|
consumed[0] = dx; |
||||
|
consumed[1] = dy; |
||||
|
} else { |
||||
|
int deltaY = dy; |
||||
|
if (dropNextY != 0) { |
||||
|
consumed[1] = dropNextY; |
||||
|
deltaY = dy - dropNextY; |
||||
|
dropNextY = 0; |
||||
|
} |
||||
|
|
||||
|
if (deltaY < 0) { |
||||
|
if (imeAnimController.isInsetAnimationInProgress()) { |
||||
|
consumed[1] -= imeAnimController.insetBy(-deltaY); |
||||
|
} else if (scrollImeOffScreenWhenVisible && !imeAnimController.isInsetAnimationRequestPending()) { |
||||
|
WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this); |
||||
|
if (rootWindowInsets != null) { |
||||
|
if (rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) { |
||||
|
startControlRequest(); |
||||
|
consumed[1] = deltaY; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onNestedScroll(@NonNull final View target, |
||||
|
final int dxConsumed, |
||||
|
final int dyConsumed, |
||||
|
final int dxUnconsumed, |
||||
|
final int dyUnconsumed, |
||||
|
final int type, |
||||
|
@NonNull final int[] consumed) { |
||||
|
if (dyUnconsumed > 0) { |
||||
|
if (imeAnimController.isInsetAnimationInProgress()) { |
||||
|
consumed[1] = -imeAnimController.insetBy(-dyUnconsumed); |
||||
|
} else if (scrollImeOnScreenWhenNotVisible && !imeAnimController.isInsetAnimationRequestPending()) { |
||||
|
WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this); |
||||
|
if (rootWindowInsets != null) { |
||||
|
if (!rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) { |
||||
|
startControlRequest(); |
||||
|
consumed[1] = dyUnconsumed; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean onNestedFling(@NonNull final View target, |
||||
|
final float velocityX, |
||||
|
final float velocityY, |
||||
|
final boolean consumed) { |
||||
|
if (imeAnimController.isInsetAnimationInProgress()) { |
||||
|
imeAnimController.animateToFinish(velocityY); |
||||
|
return true; |
||||
|
} else { |
||||
|
boolean imeVisible = false; |
||||
|
final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this); |
||||
|
if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) { |
||||
|
imeVisible = true; |
||||
|
} |
||||
|
if (velocityY > 0 && scrollImeOnScreenWhenNotVisibleOnFling && !imeVisible) { |
||||
|
imeAnimController.startAndFling(this, velocityY); |
||||
|
return true; |
||||
|
} else if (velocityY < 0 && scrollImeOffScreenWhenVisibleOnFling && imeVisible) { |
||||
|
imeAnimController.startAndFling(this, velocityY); |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onStopNestedScroll(@NonNull final View target, final int type) { |
||||
|
nestedScrollingParentHelper.onStopNestedScroll(target, type); |
||||
|
if (imeAnimController.isInsetAnimationInProgress() && !imeAnimController.isInsetAnimationFinishing()) { |
||||
|
imeAnimController.animateToFinish(null); |
||||
|
} |
||||
|
reset(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void dispatchWindowInsetsAnimationPrepare(@NonNull final WindowInsetsAnimation animation) { |
||||
|
super.dispatchWindowInsetsAnimationPrepare(animation); |
||||
|
ViewUtils.suppressLayoutCompat(this, false); |
||||
|
} |
||||
|
|
||||
|
private void startControlRequest() { |
||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { |
||||
|
return; |
||||
|
} |
||||
|
ViewUtils.suppressLayoutCompat(this, true); |
||||
|
if (currentNestedScrollingChild != null) { |
||||
|
currentNestedScrollingChild.getLocationInWindow(startViewLocation); |
||||
|
} |
||||
|
imeAnimController.startControlRequest(this, windowInsetsAnimationControllerCompat -> onControllerReady()); |
||||
|
} |
||||
|
|
||||
|
private void onControllerReady() { |
||||
|
if (currentNestedScrollingChild != null) { |
||||
|
imeAnimController.insetBy(0); |
||||
|
int[] location = tempIntArray2; |
||||
|
currentNestedScrollingChild.getLocationInWindow(location); |
||||
|
dropNextY = location[1] - startViewLocation[1]; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private void reset() { |
||||
|
dropNextY = 0; |
||||
|
Arrays.fill(startViewLocation, 0); |
||||
|
ViewUtils.suppressLayoutCompat(this, false); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onNestedScrollAccepted(@NonNull final View child, |
||||
|
@NonNull final View target, |
||||
|
final int axes) { |
||||
|
onNestedScrollAccepted(child, target, axes, TYPE_TOUCH); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onNestedScroll(@NonNull final View target, |
||||
|
final int dxConsumed, |
||||
|
final int dyConsumed, |
||||
|
final int dxUnconsumed, |
||||
|
final int dyUnconsumed, |
||||
|
final int type) { |
||||
|
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, tempIntArray2); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onStopNestedScroll(@NonNull final View target) { |
||||
|
onStopNestedScroll(target, TYPE_TOUCH); |
||||
|
} |
||||
|
} |
||||
|
|
@ -0,0 +1,33 @@ |
|||||
|
package awais.instagrabber.customviews; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.util.AttributeSet; |
||||
|
import android.view.WindowInsets; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout; |
||||
|
|
||||
|
public class InsetsNotifyingCoordinatorLayout extends CoordinatorLayout { |
||||
|
|
||||
|
public InsetsNotifyingCoordinatorLayout(@NonNull final Context context) { |
||||
|
super(context); |
||||
|
} |
||||
|
|
||||
|
public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) { |
||||
|
super(context, attrs); |
||||
|
} |
||||
|
|
||||
|
public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { |
||||
|
super(context, attrs, defStyleAttr); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public WindowInsets onApplyWindowInsets(WindowInsets insets) { |
||||
|
int childCount = getChildCount(); |
||||
|
for (int index = 0; index < childCount; index++) { |
||||
|
getChildAt(index).dispatchApplyWindowInsets(insets); |
||||
|
} |
||||
|
return insets; |
||||
|
} |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
package awais.instagrabber.customviews; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.util.AttributeSet; |
||||
|
import android.view.WindowInsets; |
||||
|
import android.widget.LinearLayout; |
||||
|
|
||||
|
import androidx.annotation.Nullable; |
||||
|
|
||||
|
public class InsetsNotifyingLinearLayout extends LinearLayout { |
||||
|
public InsetsNotifyingLinearLayout(final Context context) { |
||||
|
super(context); |
||||
|
} |
||||
|
|
||||
|
public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs) { |
||||
|
super(context, attrs); |
||||
|
} |
||||
|
|
||||
|
public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { |
||||
|
super(context, attrs, defStyleAttr); |
||||
|
} |
||||
|
|
||||
|
public InsetsNotifyingLinearLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { |
||||
|
super(context, attrs, defStyleAttr, defStyleRes); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public WindowInsets onApplyWindowInsets(WindowInsets insets) { |
||||
|
int childCount = getChildCount(); |
||||
|
for (int index = 0; index < childCount; index++) { |
||||
|
getChildAt(index).dispatchApplyWindowInsets(insets); |
||||
|
} |
||||
|
return insets; |
||||
|
} |
||||
|
} |
@ -0,0 +1,87 @@ |
|||||
|
/* |
||||
|
* Copyright 2020 The Android Open Source Project |
||||
|
* |
||||
|
* 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 awais.instagrabber.customviews.helpers; |
||||
|
|
||||
|
import android.view.View; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.core.view.ViewCompat; |
||||
|
import androidx.core.view.WindowInsetsAnimationCompat; |
||||
|
import androidx.core.view.WindowInsetsCompat; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* A [WindowInsetsAnimationCompat.Callback] which will request and clear focus on the given view, |
||||
|
* depending on the [WindowInsetsCompat.Type.ime] visibility state when an IME |
||||
|
* [WindowInsetsAnimationCompat] has finished. |
||||
|
* <p> |
||||
|
* 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<WindowInsetsAnimationCompat> runningAnimations) { |
||||
|
// no-op and return the insets |
||||
|
return insets; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onEnd(final WindowInsetsAnimationCompat animation) { |
||||
|
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) { |
||||
|
// The animation has now finished, so we can check the view's focus state. |
||||
|
// We post the check because the rootWindowInsets has not yet been updated, but will |
||||
|
// be in the next message traversal |
||||
|
view.post(this::checkFocus); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void checkFocus() { |
||||
|
final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); |
||||
|
boolean imeVisible = false; |
||||
|
if (rootWindowInsets != null) { |
||||
|
imeVisible = rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); |
||||
|
} |
||||
|
if (imeVisible && view.getRootView().findFocus() == null) { |
||||
|
// If the IME will be visible, and there is not a currently focused view in |
||||
|
// the hierarchy, request focus on our view |
||||
|
view.requestFocus(); |
||||
|
} else if (!imeVisible && view.isFocused()) { |
||||
|
// If the IME will not be visible and our view is currently focused, clear the focus |
||||
|
view.clearFocus(); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,117 @@ |
|||||
|
package awais.instagrabber.customviews.helpers; |
||||
|
|
||||
|
import android.view.View; |
||||
|
import android.view.ViewGroup; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.core.graphics.Insets; |
||||
|
import androidx.core.view.ViewCompat; |
||||
|
import androidx.core.view.WindowInsetsAnimationCompat; |
||||
|
import androidx.core.view.WindowInsetsCompat; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* A customized {@link TranslateDeferringInsetsAnimationCallback} for the emoji picker |
||||
|
*/ |
||||
|
public class EmojiPickerInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback { |
||||
|
private static final String TAG = EmojiPickerInsetsAnimationCallback.class.getSimpleName(); |
||||
|
|
||||
|
private final View view; |
||||
|
private final int persistentInsetTypes; |
||||
|
private final int deferredInsetTypes; |
||||
|
|
||||
|
private int kbHeight; |
||||
|
private onKbVisibilityChangeListener listener; |
||||
|
private boolean shouldTranslate; |
||||
|
|
||||
|
public EmojiPickerInsetsAnimationCallback(final View view, |
||||
|
final int persistentInsetTypes, |
||||
|
final int deferredInsetTypes) { |
||||
|
this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP); |
||||
|
} |
||||
|
|
||||
|
public EmojiPickerInsetsAnimationCallback(final View view, |
||||
|
final int persistentInsetTypes, |
||||
|
final int deferredInsetTypes, |
||||
|
final int dispatchMode) { |
||||
|
super(dispatchMode); |
||||
|
if ((persistentInsetTypes & deferredInsetTypes) != 0) { |
||||
|
throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " + |
||||
|
"any of same WindowInsetsCompat.Type values"); |
||||
|
} |
||||
|
this.view = view; |
||||
|
this.persistentInsetTypes = persistentInsetTypes; |
||||
|
this.deferredInsetTypes = deferredInsetTypes; |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
@Override |
||||
|
public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets, |
||||
|
@NonNull final List<WindowInsetsAnimationCompat> runningAnimations) { |
||||
|
// onProgress() is called when any of the running animations progress... |
||||
|
|
||||
|
// First we get the insets which are potentially deferred |
||||
|
final Insets typesInset = insets.getInsets(deferredInsetTypes); |
||||
|
// Then we get the persistent inset types which are applied as padding during layout |
||||
|
final Insets otherInset = insets.getInsets(persistentInsetTypes); |
||||
|
|
||||
|
// Now that we subtract the two insets, to calculate the difference. We also coerce |
||||
|
// the insets to be >= 0, to make sure we don't use negative insets. |
||||
|
final Insets subtract = Insets.subtract(typesInset, otherInset); |
||||
|
final Insets diff = Insets.max(subtract, Insets.NONE); |
||||
|
|
||||
|
// The resulting `diff` insets contain the values for us to apply as a translation |
||||
|
// to the view |
||||
|
view.setTranslationX(diff.left - diff.right); |
||||
|
view.setTranslationY(shouldTranslate ? diff.top - diff.bottom : -kbHeight); |
||||
|
|
||||
|
return insets; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) { |
||||
|
try { |
||||
|
final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); |
||||
|
if (kbHeight == 0) { |
||||
|
if (rootWindowInsets == null) return; |
||||
|
final Insets imeInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime()); |
||||
|
final Insets navBarInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()); |
||||
|
kbHeight = imeInsets.bottom - navBarInsets.bottom; |
||||
|
final ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); |
||||
|
if (layoutParams != null) { |
||||
|
layoutParams.height = kbHeight; |
||||
|
layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin, layoutParams.rightMargin, -kbHeight); |
||||
|
} |
||||
|
} |
||||
|
view.setTranslationX(0f); |
||||
|
final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); |
||||
|
float translationY = 0; |
||||
|
if (!shouldTranslate) { |
||||
|
translationY = -kbHeight; |
||||
|
if (visible) { |
||||
|
translationY = 0; |
||||
|
} |
||||
|
} |
||||
|
view.setTranslationY(translationY); |
||||
|
|
||||
|
if (listener != null && rootWindowInsets != null) { |
||||
|
listener.onChange(visible); |
||||
|
} |
||||
|
} finally { |
||||
|
shouldTranslate = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void setShouldTranslate(final boolean shouldTranslate) { |
||||
|
this.shouldTranslate = shouldTranslate; |
||||
|
} |
||||
|
|
||||
|
public void setKbVisibilityListener(final onKbVisibilityChangeListener listener) { |
||||
|
this.listener = listener; |
||||
|
} |
||||
|
|
||||
|
public interface onKbVisibilityChangeListener { |
||||
|
void onChange(boolean isVisible); |
||||
|
} |
||||
|
} |
@ -0,0 +1,139 @@ |
|||||
|
package awais.instagrabber.customviews.helpers;/* |
||||
|
* Copyright 2020 The Android Open Source Project |
||||
|
* |
||||
|
* 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. |
||||
|
*/ |
||||
|
|
||||
|
import android.view.View; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.core.graphics.Insets; |
||||
|
import androidx.core.view.OnApplyWindowInsetsListener; |
||||
|
import androidx.core.view.ViewCompat; |
||||
|
import androidx.core.view.WindowInsetsAnimationCompat; |
||||
|
import androidx.core.view.WindowInsetsCompat; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* A class which extends/implements both [WindowInsetsAnimationCompat.Callback] and |
||||
|
* [View.OnApplyWindowInsetsListener], which should be set on the root view in your layout. |
||||
|
* <p> |
||||
|
* This class enables the root view is selectively defer handling any insets which match |
||||
|
* [deferredInsetTypes], to enable better looking [WindowInsetsAnimationCompat]s. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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: |
||||
|
* <p> |
||||
|
* ``` |
||||
|
* val callback = RootViewDeferringInsetsCallback( |
||||
|
* persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), |
||||
|
* deferredInsetTypes = WindowInsetsCompat.Type.ime() |
||||
|
* ) |
||||
|
* ``` |
||||
|
* <p> |
||||
|
* 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<WindowInsetsAnimationCompat> runningAnims) { |
||||
|
// This is a no-op. We don't actually want to handle any WindowInsetsAnimations |
||||
|
return insets; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) { |
||||
|
if (deferredInsets && (animation.getTypeMask() & deferredInsetTypes) != 0) { |
||||
|
// If we deferred the IME insets and an IME animation has finished, we need to reset |
||||
|
// the flag |
||||
|
deferredInsets = false; |
||||
|
|
||||
|
// And finally dispatch the deferred insets to the view now. |
||||
|
// Ideally we would just call view.requestApplyInsets() and let the normal dispatch |
||||
|
// cycle happen, but this happens too late resulting in a visual flicker. |
||||
|
// Instead we manually dispatch the most recent WindowInsets to the view. |
||||
|
if (lastWindowInsets != null && view != null) { |
||||
|
ViewCompat.dispatchApplyWindowInsets(view, lastWindowInsets); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,443 @@ |
|||||
|
/* |
||||
|
* Copyright 2020 The Android Open Source Project |
||||
|
* |
||||
|
* 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 awais.instagrabber.customviews.helpers; |
||||
|
|
||||
|
import android.os.CancellationSignal; |
||||
|
import android.util.Log; |
||||
|
import android.view.View; |
||||
|
import android.view.animation.LinearInterpolator; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.annotation.Nullable; |
||||
|
import androidx.core.graphics.Insets; |
||||
|
import androidx.core.view.ViewCompat; |
||||
|
import androidx.core.view.WindowInsetsAnimationControlListenerCompat; |
||||
|
import androidx.core.view.WindowInsetsAnimationControllerCompat; |
||||
|
import androidx.core.view.WindowInsetsCompat; |
||||
|
import androidx.core.view.WindowInsetsControllerCompat; |
||||
|
import androidx.dynamicanimation.animation.FloatPropertyCompat; |
||||
|
import androidx.dynamicanimation.animation.SpringAnimation; |
||||
|
import androidx.dynamicanimation.animation.SpringForce; |
||||
|
|
||||
|
import awais.instagrabber.utils.ViewUtils; |
||||
|
|
||||
|
/** |
||||
|
* A wrapper around the [WindowInsetsAnimationControllerCompat] APIs in AndroidX Core, to simplify |
||||
|
* the implementation of common use-cases around the IME. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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. |
||||
|
* <p> |
||||
|
* 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<Object> property = new FloatPropertyCompat<Object>("property") { |
||||
|
@Override |
||||
|
public float getValue(final Object object) { |
||||
|
return controller.getCurrentInsets().bottom; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void setValue(final Object object, final float value) { |
||||
|
if (insetsAnimationController == null) { |
||||
|
return; |
||||
|
} |
||||
|
insetTo((int) value); |
||||
|
} |
||||
|
}; |
||||
|
final float finalPosition = visible ? controller.getShownStateInsets().bottom |
||||
|
: controller.getHiddenStateInsets().bottom; |
||||
|
final SpringForce force = new SpringForce(finalPosition) |
||||
|
// Tweak the damping value, to remove any bounciness. |
||||
|
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) |
||||
|
// The stiffness value controls the strength of the spring animation, which |
||||
|
// controls the speed. Medium (the default) is a good value, but feel free to |
||||
|
// play around with this value. |
||||
|
.setStiffness(SpringForce.STIFFNESS_MEDIUM); |
||||
|
ViewUtils.springAnimationOf(this, property, finalPosition) |
||||
|
.setSpring(force) |
||||
|
.setStartVelocity(velocityY != null ? velocityY : 0) |
||||
|
.addEndListener((animation, canceled, value, velocity) -> { |
||||
|
if (animation == currentSpringAnimation) { |
||||
|
currentSpringAnimation = null; |
||||
|
} |
||||
|
// Once the animation has ended, finish the controller |
||||
|
finish(); |
||||
|
}).start(); |
||||
|
} |
||||
|
|
||||
|
private int coerceIn(final int v, final int min, final int max) { |
||||
|
if (v >= min && v <= max) { |
||||
|
return v; |
||||
|
} |
||||
|
if (v < min) { |
||||
|
return min; |
||||
|
} |
||||
|
return max; |
||||
|
} |
||||
|
|
||||
|
public void setAnimationControlListener(final WindowInsetsAnimationControlListenerCompat listener) { |
||||
|
fwdListener = listener; |
||||
|
} |
||||
|
|
||||
|
public interface OnRequestReadyListener { |
||||
|
void onRequestReady(WindowInsetsAnimationControllerCompat windowInsetsAnimationControllerCompat); |
||||
|
} |
||||
|
} |
@ -0,0 +1,128 @@ |
|||||
|
/* |
||||
|
* Copyright 2020 The Android Open Source Project |
||||
|
* |
||||
|
* 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 awais.instagrabber.customviews.helpers; |
||||
|
|
||||
|
import android.view.View; |
||||
|
|
||||
|
import androidx.annotation.NonNull; |
||||
|
import androidx.core.graphics.Insets; |
||||
|
import androidx.core.view.ViewCompat; |
||||
|
import androidx.core.view.WindowInsetsAnimationCompat; |
||||
|
import androidx.core.view.WindowInsetsCompat; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* A [WindowInsetsAnimationCompat.Callback] which will translate/move the given view during any |
||||
|
* inset animations of the given inset type. |
||||
|
* <p> |
||||
|
* This class works in tandem with [RootViewDeferringInsetsCallback] to support the deferring of |
||||
|
* certain [WindowInsetsCompat.Type] values during a [WindowInsetsAnimationCompat], provided in |
||||
|
* [deferredInsetTypes]. The values passed into this constructor should match those which |
||||
|
* the [RootViewDeferringInsetsCallback] is created with. |
||||
|
*/ |
||||
|
public class TranslateDeferringInsetsAnimationCallback extends WindowInsetsAnimationCompat.Callback { |
||||
|
private final View view; |
||||
|
private final int persistentInsetTypes; |
||||
|
private final int deferredInsetTypes; |
||||
|
|
||||
|
private boolean shouldTranslate = true; |
||||
|
private int kbHeight; |
||||
|
|
||||
|
public TranslateDeferringInsetsAnimationCallback(final View view, |
||||
|
final int persistentInsetTypes, |
||||
|
final int deferredInsetTypes) { |
||||
|
this(view, persistentInsetTypes, deferredInsetTypes, DISPATCH_MODE_STOP); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param view the view to translate from it's start to end state |
||||
|
* @param persistentInsetTypes the bitmask of any inset types which were handled as part of the |
||||
|
* layout |
||||
|
* @param deferredInsetTypes the bitmask of insets types which should be deferred until after |
||||
|
* any [WindowInsetsAnimationCompat]s have ended |
||||
|
* @param dispatchMode The dispatch mode for this callback. |
||||
|
* See [WindowInsetsAnimationCompat.Callback.getDispatchMode]. |
||||
|
*/ |
||||
|
public TranslateDeferringInsetsAnimationCallback(final View view, |
||||
|
final int persistentInsetTypes, |
||||
|
final int deferredInsetTypes, |
||||
|
final int dispatchMode) { |
||||
|
super(dispatchMode); |
||||
|
if ((persistentInsetTypes & deferredInsetTypes) != 0) { |
||||
|
throw new IllegalArgumentException("persistentInsetTypes and deferredInsetTypes can not contain " + |
||||
|
"any of same WindowInsetsCompat.Type values"); |
||||
|
} |
||||
|
this.view = view; |
||||
|
this.persistentInsetTypes = persistentInsetTypes; |
||||
|
this.deferredInsetTypes = deferredInsetTypes; |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
@Override |
||||
|
public WindowInsetsCompat onProgress(@NonNull final WindowInsetsCompat insets, |
||||
|
@NonNull final List<WindowInsetsAnimationCompat> runningAnimations) { |
||||
|
// onProgress() is called when any of the running animations progress... |
||||
|
|
||||
|
// First we get the insets which are potentially deferred |
||||
|
final Insets typesInset = insets.getInsets(deferredInsetTypes); |
||||
|
// Then we get the persistent inset types which are applied as padding during layout |
||||
|
final Insets otherInset = insets.getInsets(persistentInsetTypes); |
||||
|
|
||||
|
// Now that we subtract the two insets, to calculate the difference. We also coerce |
||||
|
// the insets to be >= 0, to make sure we don't use negative insets. |
||||
|
final Insets subtract = Insets.subtract(typesInset, otherInset); |
||||
|
final Insets diff = Insets.max(subtract, Insets.NONE); |
||||
|
|
||||
|
// The resulting `diff` insets contain the values for us to apply as a translation |
||||
|
// to the view |
||||
|
view.setTranslationX(diff.left - diff.right); |
||||
|
view.setTranslationY(shouldTranslate ? diff.top - diff.bottom : -kbHeight); |
||||
|
|
||||
|
return insets; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onEnd(@NonNull final WindowInsetsAnimationCompat animation) { |
||||
|
try { |
||||
|
final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(view); |
||||
|
if (kbHeight == 0) { |
||||
|
if (rootWindowInsets == null) return; |
||||
|
final Insets imeInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime()); |
||||
|
final Insets navBarInsets = rootWindowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()); |
||||
|
kbHeight = imeInsets.bottom - navBarInsets.bottom; |
||||
|
} |
||||
|
// Once the animation has ended, reset the translation values |
||||
|
view.setTranslationX(0f); |
||||
|
final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); |
||||
|
float translationY = 0; |
||||
|
if (!shouldTranslate) { |
||||
|
translationY = -kbHeight; |
||||
|
if (visible) { |
||||
|
translationY = 0; |
||||
|
} |
||||
|
} |
||||
|
view.setTranslationY(translationY); |
||||
|
} finally { |
||||
|
shouldTranslate = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void setShouldTranslate(final boolean shouldTranslate) { |
||||
|
this.shouldTranslate = shouldTranslate; |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue