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
-
563app/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; |
|||
} |
|||
} |
@ -1,312 +1,315 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
<awais.instagrabber.customviews.InsetsAnimationLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|||
xmlns:tools="http://schemas.android.com/tools" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="match_parent" |
|||
android:clipToPadding="false"> |
|||
android:clipToPadding="false" |
|||
android:orientation="vertical"> |
|||
|
|||
<androidx.recyclerview.widget.RecyclerView |
|||
android:id="@+id/chats" |
|||
android:layout_width="0dp" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="0dp" |
|||
android:layout_weight="1" |
|||
android:scrollbars="none" |
|||
app:layout_constraintBottom_toTopOf="@id/chats_barrier" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="parent" |
|||
tools:listitem="@layout/layout_dm_base" /> |
|||
|
|||
<androidx.constraintlayout.widget.Barrier |
|||
android:id="@+id/chats_barrier" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
app:barrierDirection="bottom" /> |
|||
<androidx.constraintlayout.widget.ConstraintLayout |
|||
android:id="@+id/input_holder" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content"> |
|||
|
|||
<View |
|||
android:id="@+id/reply_bg" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
android:background="@drawable/bg_input" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toEndOf="@id/input_bg" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toTopOf="@id/reply_info" |
|||
tools:visibility="gone" /> |
|||
<androidx.constraintlayout.widget.Barrier |
|||
android:id="@+id/chats_barrier" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
app:barrierDirection="bottom" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/reply_info" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:ellipsize="end" |
|||
android:paddingStart="16dp" |
|||
android:paddingTop="8dp" |
|||
android:paddingEnd="16dp" |
|||
android:paddingBottom="4dp" |
|||
android:singleLine="true" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toTopOf="@id/reply_preview_text" |
|||
app:layout_constraintEnd_toStartOf="@id/reply_preview_image" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toBottomOf="@id/chats_barrier" |
|||
tools:text="Replying to yourself" |
|||
tools:visibility="gone" /> |
|||
<View |
|||
android:id="@+id/reply_bg" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
android:background="@drawable/bg_input" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toEndOf="@id/input_bg" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toTopOf="@id/reply_info" |
|||
tools:visibility="gone" /> |
|||
|
|||
<androidx.emoji.widget.EmojiAppCompatTextView |
|||
android:id="@+id/reply_preview_text" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:ellipsize="end" |
|||
android:paddingStart="16dp" |
|||
android:paddingTop="4dp" |
|||
android:paddingEnd="16dp" |
|||
android:paddingBottom="8dp" |
|||
android:singleLine="true" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toTopOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/reply_preview_image" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toBottomOf="@id/reply_info" |
|||
app:layout_goneMarginTop="8dp" |
|||
tools:text="Post" |
|||
tools:visibility="gone" /> |
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/reply_info" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:ellipsize="end" |
|||
android:paddingStart="16dp" |
|||
android:paddingTop="8dp" |
|||
android:paddingEnd="16dp" |
|||
android:paddingBottom="4dp" |
|||
android:singleLine="true" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toTopOf="@id/reply_preview_text" |
|||
app:layout_constraintEnd_toStartOf="@id/reply_preview_image" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toBottomOf="@id/chats_barrier" |
|||
tools:text="Replying to yourself" |
|||
tools:visibility="gone" /> |
|||
|
|||
<com.facebook.drawee.view.SimpleDraweeView |
|||
android:id="@+id/reply_preview_image" |
|||
android:layout_width="@dimen/dm_inbox_avatar_size_small" |
|||
android:layout_height="@dimen/dm_inbox_avatar_size_small" |
|||
android:layout_marginEnd="8dp" |
|||
android:visibility="gone" |
|||
app:actualImageScaleType="centerCrop" |
|||
app:layout_constraintBottom_toBottomOf="@id/reply_preview_text" |
|||
app:layout_constraintEnd_toStartOf="@id/reply_cancel" |
|||
app:layout_constraintStart_toEndOf="@id/reply_preview_text" |
|||
app:layout_constraintTop_toTopOf="@id/reply_info" |
|||
tools:background="@mipmap/ic_launcher" |
|||
tools:visibility="gone" /> |
|||
<androidx.emoji.widget.EmojiAppCompatTextView |
|||
android:id="@+id/reply_preview_text" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:ellipsize="end" |
|||
android:paddingStart="16dp" |
|||
android:paddingTop="4dp" |
|||
android:paddingEnd="16dp" |
|||
android:paddingBottom="8dp" |
|||
android:singleLine="true" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toTopOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/reply_preview_image" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toBottomOf="@id/reply_info" |
|||
app:layout_goneMarginTop="8dp" |
|||
tools:text="Post" |
|||
tools:visibility="gone" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatImageView |
|||
android:id="@+id/reply_cancel" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:layout_marginEnd="12dp" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/reply_preview_text" |
|||
app:layout_constraintEnd_toEndOf="@id/input_bg" |
|||
app:layout_constraintStart_toEndOf="@id/reply_preview_image" |
|||
app:layout_constraintTop_toTopOf="@id/reply_info" |
|||
app:srcCompat="@drawable/ic_close_24" |
|||
tools:visibility="gone" /> |
|||
<com.facebook.drawee.view.SimpleDraweeView |
|||
android:id="@+id/reply_preview_image" |
|||
android:layout_width="@dimen/dm_inbox_avatar_size_small" |
|||
android:layout_height="@dimen/dm_inbox_avatar_size_small" |
|||
android:layout_marginEnd="8dp" |
|||
android:visibility="gone" |
|||
app:actualImageScaleType="centerCrop" |
|||
app:layout_constraintBottom_toBottomOf="@id/reply_preview_text" |
|||
app:layout_constraintEnd_toStartOf="@id/reply_cancel" |
|||
app:layout_constraintStart_toEndOf="@id/reply_preview_text" |
|||
app:layout_constraintTop_toTopOf="@id/reply_info" |
|||
tools:background="@mipmap/ic_launcher" |
|||
tools:visibility="gone" /> |
|||
|
|||
<!--<androidx.constraintlayout.widget.Group--> |
|||
<!-- android:id="@+id/reply_group"--> |
|||
<!-- android:layout_width="0dp"--> |
|||
<!-- android:layout_height="0dp"--> |
|||
<!-- android:visibility="gone"--> |
|||
<!-- app:constraint_referenced_ids="reply_bg,reply_cancel,reply_info,reply_item_type,reply_preview"--> |
|||
<!-- tools:visibility="visible" />--> |
|||
<androidx.appcompat.widget.AppCompatImageView |
|||
android:id="@+id/reply_cancel" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:layout_marginEnd="12dp" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/reply_preview_text" |
|||
app:layout_constraintEnd_toEndOf="@id/input_bg" |
|||
app:layout_constraintStart_toEndOf="@id/reply_preview_image" |
|||
app:layout_constraintTop_toTopOf="@id/reply_info" |
|||
app:srcCompat="@drawable/ic_close_24" |
|||
tools:visibility="gone" /> |
|||
|
|||
<View |
|||
android:id="@+id/input_bg" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
android:layout_marginStart="4dp" |
|||
android:layout_marginEnd="4dp" |
|||
android:background="@drawable/bg_input" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input" |
|||
app:layout_constraintEnd_toStartOf="@id/send" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
tools:visibility="visible" /> |
|||
<!--<androidx.constraintlayout.widget.Group--> |
|||
<!-- android:id="@+id/reply_group"--> |
|||
<!-- android:layout_width="0dp"--> |
|||
<!-- android:layout_height="0dp"--> |
|||
<!-- android:visibility="gone"--> |
|||
<!-- app:constraint_referenced_ids="reply_bg,reply_cancel,reply_info,reply_item_type,reply_preview"--> |
|||
<!-- tools:visibility="visible" />--> |
|||
|
|||
<com.google.android.material.button.MaterialButton |
|||
android:id="@+id/emoji_toggle" |
|||
style="@style/Widget.MaterialComponents.Button.Icon.NoInsets" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:layout_marginStart="8dp" |
|||
android:layout_marginEnd="2dp" |
|||
android:background="@android:color/transparent" |
|||
android:scrollbars="none" |
|||
android:visibility="gone" |
|||
app:icon="@drawable/ic_face_24" |
|||
app:iconGravity="textStart" |
|||
app:iconSize="24dp" |
|||
app:iconTint="@color/grey_700" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/input" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
app:rippleColor="@color/grey_500" |
|||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle" |
|||
app:strokeColor="@color/black" |
|||
app:strokeWidth="1dp" |
|||
tools:visibility="visible" /> |
|||
<View |
|||
android:id="@+id/input_bg" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
android:layout_marginStart="4dp" |
|||
android:layout_marginEnd="4dp" |
|||
android:background="@drawable/bg_input" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input" |
|||
app:layout_constraintEnd_toStartOf="@id/send" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
tools:visibility="visible" /> |
|||
|
|||
<awais.instagrabber.customviews.KeyNotifyingEmojiEditText |
|||
android:id="@+id/input" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="4dp" |
|||
android:background="@android:color/transparent" |
|||
android:hint="@string/message" |
|||
android:paddingTop="12dp" |
|||
android:paddingBottom="12dp" |
|||
android:textColor="?dmInputTextColor" |
|||
android:textColorHint="@color/grey_500" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toStartOf="@id/gif" |
|||
app:layout_constraintStart_toEndOf="@id/emoji_toggle" |
|||
app:layout_constraintTop_toBottomOf="@id/reply_preview_text" |
|||
app:layout_goneMarginBottom="4dp" |
|||
app:layout_goneMarginEnd="24dp" |
|||
tools:visibility="visible" /> |
|||
<com.google.android.material.button.MaterialButton |
|||
android:id="@+id/emoji_toggle" |
|||
style="@style/Widget.MaterialComponents.Button.Icon.NoInsets" |
|||
android:layout_width="24dp" |
|||
android:layout_height="24dp" |
|||
android:layout_marginStart="8dp" |
|||
android:layout_marginEnd="2dp" |
|||
android:background="@android:color/transparent" |
|||
android:scrollbars="none" |
|||
android:visibility="gone" |
|||
app:icon="@drawable/ic_face_24" |
|||
app:iconGravity="textStart" |
|||
app:iconSize="24dp" |
|||
app:iconTint="@color/grey_700" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/input" |
|||
app:layout_constraintStart_toStartOf="@id/input_bg" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
app:rippleColor="@color/grey_500" |
|||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle" |
|||
app:strokeColor="@color/black" |
|||
app:strokeWidth="1dp" |
|||
tools:visibility="visible" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatImageButton |
|||
android:id="@+id/gif" |
|||
android:layout_width="32dp" |
|||
android:layout_height="0dp" |
|||
android:background="@android:color/transparent" |
|||
android:scaleType="fitCenter" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/camera" |
|||
app:layout_constraintStart_toEndOf="@id/input" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
app:srcCompat="@drawable/ic_round_gif_24" |
|||
tools:visibility="visible" /> |
|||
<awais.instagrabber.customviews.KeyNotifyingEmojiEditText |
|||
android:id="@+id/input" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:layout_marginStart="4dp" |
|||
android:background="@android:color/transparent" |
|||
android:hint="@string/message" |
|||
android:paddingTop="12dp" |
|||
android:paddingBottom="12dp" |
|||
android:textColor="?dmInputTextColor" |
|||
android:textColorHint="@color/grey_500" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toStartOf="@id/gif" |
|||
app:layout_constraintStart_toEndOf="@id/emoji_toggle" |
|||
app:layout_constraintTop_toBottomOf="@id/reply_preview_text" |
|||
app:layout_goneMarginBottom="4dp" |
|||
app:layout_goneMarginEnd="24dp" |
|||
tools:visibility="visible" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatImageButton |
|||
android:id="@+id/camera" |
|||
android:layout_width="32dp" |
|||
android:layout_height="0dp" |
|||
android:layout_marginStart="4dp" |
|||
android:background="@android:color/transparent" |
|||
android:paddingStart="4dp" |
|||
android:paddingEnd="4dp" |
|||
android:scaleType="fitCenter" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/gallery" |
|||
app:layout_constraintStart_toEndOf="@id/gif" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
app:srcCompat="@drawable/ic_camera_24" |
|||
tools:visibility="visible" /> |
|||
<androidx.appcompat.widget.AppCompatImageButton |
|||
android:id="@+id/gif" |
|||
android:layout_width="32dp" |
|||
android:layout_height="0dp" |
|||
android:background="@android:color/transparent" |
|||
android:scaleType="fitCenter" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/camera" |
|||
app:layout_constraintStart_toEndOf="@id/input" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
app:srcCompat="@drawable/ic_round_gif_24" |
|||
tools:visibility="visible" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatImageButton |
|||
android:id="@+id/gallery" |
|||
android:layout_width="32dp" |
|||
android:layout_height="0dp" |
|||
android:layout_marginStart="4dp" |
|||
android:layout_marginEnd="16dp" |
|||
android:background="@android:color/transparent" |
|||
android:paddingStart="4dp" |
|||
android:paddingEnd="4dp" |
|||
android:scaleType="fitCenter" |
|||
android:src="@drawable/ic_image_24" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/send" |
|||
app:layout_constraintStart_toEndOf="@id/camera" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
tools:visibility="visible" /> |
|||
<androidx.appcompat.widget.AppCompatImageButton |
|||
android:id="@+id/camera" |
|||
android:layout_width="32dp" |
|||
android:layout_height="0dp" |
|||
android:layout_marginStart="4dp" |
|||
android:background="@android:color/transparent" |
|||
android:paddingStart="4dp" |
|||
android:paddingEnd="4dp" |
|||
android:scaleType="fitCenter" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/gallery" |
|||
app:layout_constraintStart_toEndOf="@id/gif" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
app:srcCompat="@drawable/ic_camera_24" |
|||
tools:visibility="visible" /> |
|||
|
|||
<awais.instagrabber.customviews.RecordView |
|||
android:id="@+id/record_view" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
android:visibility="gone" |
|||
app:counter_time_color="@color/white" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toEndOf="@id/input_bg" |
|||
app:layout_constraintStart_toStartOf="@id/input" |
|||
app:layout_constraintTop_toBottomOf="@id/chats_barrier" |
|||
app:slide_to_cancel_arrow="@drawable/recv_ic_arrow" |
|||
app:slide_to_cancel_arrow_color="@color/white" |
|||
app:slide_to_cancel_bounds="0dp" |
|||
app:slide_to_cancel_margin_right="16dp" |
|||
app:slide_to_cancel_text="Slide To Cancel" |
|||
app:slide_to_cancel_text_color="@color/white" |
|||
tools:visibility="visible" /> |
|||
<androidx.appcompat.widget.AppCompatImageButton |
|||
android:id="@+id/gallery" |
|||
android:layout_width="32dp" |
|||
android:layout_height="0dp" |
|||
android:layout_marginStart="4dp" |
|||
android:layout_marginEnd="16dp" |
|||
android:background="@android:color/transparent" |
|||
android:paddingStart="4dp" |
|||
android:paddingEnd="4dp" |
|||
android:scaleType="fitCenter" |
|||
android:src="@drawable/ic_image_24" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toStartOf="@id/send" |
|||
app:layout_constraintStart_toEndOf="@id/camera" |
|||
app:layout_constraintTop_toTopOf="@id/input" |
|||
tools:visibility="visible" /> |
|||
|
|||
<awais.instagrabber.customviews.RecordButton |
|||
android:id="@+id/send" |
|||
style="@style/Widget.MaterialComponents.Button.Icon.NoInsets" |
|||
android:layout_width="48dp" |
|||
android:layout_height="48dp" |
|||
android:visibility="gone" |
|||
app:backgroundTint="@color/blue_900" |
|||
app:elevation="4dp" |
|||
app:icon="@drawable/avd_mic_to_send_anim" |
|||
app:iconGravity="textStart" |
|||
app:iconSize="24dp" |
|||
app:iconTint="@color/white" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toEndOf="@id/input_bg" |
|||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle" |
|||
tools:visibility="visible" /> |
|||
<awais.instagrabber.customviews.RecordView |
|||
android:id="@+id/record_view" |
|||
android:layout_width="0dp" |
|||
android:layout_height="0dp" |
|||
android:visibility="gone" |
|||
app:counter_time_color="@color/white" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toEndOf="@id/input_bg" |
|||
app:layout_constraintStart_toStartOf="@id/input" |
|||
app:layout_constraintTop_toBottomOf="@id/chats_barrier" |
|||
app:slide_to_cancel_arrow="@drawable/recv_ic_arrow" |
|||
app:slide_to_cancel_arrow_color="@color/white" |
|||
app:slide_to_cancel_bounds="0dp" |
|||
app:slide_to_cancel_margin_right="16dp" |
|||
app:slide_to_cancel_text="Slide To Cancel" |
|||
app:slide_to_cancel_text_color="@color/white" |
|||
tools:visibility="visible" /> |
|||
|
|||
<awais.instagrabber.customviews.emoji.EmojiPicker |
|||
android:id="@+id/emoji_picker" |
|||
android:layout_width="0dp" |
|||
android:layout_height="250dp" |
|||
android:translationY="250dp" |
|||
android:visibility="visible" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
tools:visibility="visible" /> |
|||
<awais.instagrabber.customviews.RecordButton |
|||
android:id="@+id/send" |
|||
style="@style/Widget.MaterialComponents.Button.Icon.NoInsets" |
|||
android:layout_width="48dp" |
|||
android:layout_height="48dp" |
|||
android:visibility="gone" |
|||
app:backgroundTint="@color/blue_900" |
|||
app:elevation="4dp" |
|||
app:icon="@drawable/avd_mic_to_send_anim" |
|||
app:iconGravity="textStart" |
|||
app:iconSize="24dp" |
|||
app:iconTint="@color/white" |
|||
app:layout_constraintBottom_toBottomOf="@id/input_bg" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toEndOf="@id/input_bg" |
|||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle" |
|||
tools:visibility="visible" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/accept_pending_request_question" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:paddingTop="16dp" |
|||
android:paddingBottom="8dp" |
|||
android:text="@string/accept_request_from_user" |
|||
android:textAlignment="center" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toTopOf="@id/decline" |
|||
app:layout_constraintTop_toBottomOf="@id/chats_barrier" |
|||
tools:visibility="gone" /> |
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/accept_pending_request_question" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="wrap_content" |
|||
android:paddingTop="16dp" |
|||
android:paddingBottom="8dp" |
|||
android:text="@string/accept_request_from_user" |
|||
android:textAlignment="center" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toTopOf="@id/decline" |
|||
app:layout_constraintTop_toBottomOf="@id/chats_barrier" |
|||
tools:visibility="gone" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/decline" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:background="?selectableItemBackground" |
|||
android:padding="16dp" |
|||
android:text="@string/decline" |
|||
android:textAlignment="center" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
|||
android:textColor="@color/red_500" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toStartOf="@id/accept" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question" |
|||
tools:visibility="gone" /> |
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/decline" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:background="?selectableItemBackground" |
|||
android:padding="16dp" |
|||
android:text="@string/decline" |
|||
android:textAlignment="center" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
|||
android:textColor="@color/red_500" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toStartOf="@id/accept" |
|||
app:layout_constraintStart_toStartOf="parent" |
|||
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question" |
|||
tools:visibility="gone" /> |
|||
|
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/accept" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:background="?selectableItemBackground" |
|||
android:padding="16dp" |
|||
android:text="@string/accept" |
|||
android:textAlignment="center" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
|||
android:visibility="gone" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toEndOf="@id/decline" |
|||
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question" |
|||
tools:visibility="gone" /> |
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
|||
|
|||
<androidx.appcompat.widget.AppCompatTextView |
|||
android:id="@+id/accept" |
|||
android:layout_width="0dp" |
|||
android:layout_height="wrap_content" |
|||
android:background="?selectableItemBackground" |
|||
android:padding="16dp" |
|||
android:text="@string/accept" |
|||
android:textAlignment="center" |
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
|||
android:visibility="gone" |
|||
<awais.instagrabber.customviews.emoji.EmojiPicker |
|||
android:id="@+id/emoji_picker" |
|||
android:layout_width="match_parent" |
|||
android:layout_height="250dp" |
|||
android:layout_marginBottom="-250dp" |
|||
android:alpha="0" |
|||
app:layout_constraintBottom_toBottomOf="parent" |
|||
app:layout_constraintEnd_toEndOf="parent" |
|||
app:layout_constraintStart_toEndOf="@id/decline" |
|||
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question" |
|||
tools:visibility="gone" /> |
|||
</androidx.constraintlayout.widget.ConstraintLayout> |
|||
app:layout_constraintStart_toStartOf="parent" /> |
|||
</awais.instagrabber.customviews.InsetsAnimationLinearLayout> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue