mirror of
https://github.com/KokaKiwi/BarInsta
synced 2024-11-08 07:57:28 +00:00
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.
This commit is contained in:
parent
1ede8ad4bf
commit
cf71ca682e
@ -12,7 +12,7 @@ def getGitHash = { ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'me.austinhuang.instagrabber'
|
applicationId 'me.austinhuang.instagrabber'
|
||||||
@ -165,8 +165,6 @@ dependencies {
|
|||||||
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
|
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
|
||||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
|
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
|
||||||
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.0"
|
implementation "androidx.recyclerview:recyclerview:1.2.0"
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||||
@ -180,6 +178,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.google.guava:guava:27.0.1-android'
|
implementation 'com.google.guava:guava:27.0.1-android'
|
||||||
|
|
||||||
|
def core_version = "1.6.0-alpha03"
|
||||||
|
implementation "androidx.core:core:$core_version"
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
def room_version = "2.2.6"
|
def room_version = "2.2.6"
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
|
@ -27,8 +27,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:taskAffinity=".Main"
|
android:taskAffinity=".Main">
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
@ -31,6 +31,9 @@ import androidx.appcompat.widget.Toolbar;
|
|||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.core.provider.FontRequest;
|
import androidx.core.provider.FontRequest;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.emoji.text.EmojiCompat;
|
import androidx.emoji.text.EmojiCompat;
|
||||||
import androidx.emoji.text.FontRequestEmojiCompatConfig;
|
import androidx.emoji.text.FontRequestEmojiCompatConfig;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
@ -61,6 +64,7 @@ import awais.instagrabber.BuildConfig;
|
|||||||
import awais.instagrabber.R;
|
import awais.instagrabber.R;
|
||||||
import awais.instagrabber.asyncs.PostFetcher;
|
import awais.instagrabber.asyncs.PostFetcher;
|
||||||
import awais.instagrabber.customviews.emoji.EmojiVariantManager;
|
import awais.instagrabber.customviews.emoji.EmojiVariantManager;
|
||||||
|
import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback;
|
||||||
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
|
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
|
||||||
import awais.instagrabber.databinding.ActivityMainBinding;
|
import awais.instagrabber.databinding.ActivityMainBinding;
|
||||||
import awais.instagrabber.fragments.PostViewV2Fragment;
|
import awais.instagrabber.fragments.PostViewV2Fragment;
|
||||||
@ -137,11 +141,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
|
|||||||
instance = this;
|
instance = this;
|
||||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setupCookie();
|
setupCookie();
|
||||||
if (settingsHelper.getBoolean(Constants.FLAG_SECURE))
|
if (settingsHelper.getBoolean(Constants.FLAG_SECURE)) {
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
|
}
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
final Toolbar toolbar = binding.toolbar;
|
final Toolbar toolbar = binding.toolbar;
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
final RootViewDeferringInsetsCallback deferringInsetsCallback = new RootViewDeferringInsetsCallback(
|
||||||
|
WindowInsetsCompat.Type.systemBars(),
|
||||||
|
WindowInsetsCompat.Type.ime()
|
||||||
|
);
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(binding.getRoot(), deferringInsetsCallback);
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), deferringInsetsCallback);
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
createNotificationChannels();
|
createNotificationChannels();
|
||||||
try {
|
try {
|
||||||
final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams();
|
final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams();
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -25,11 +26,17 @@ import android.view.ViewGroup;
|
|||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.OnBackPressedCallback;
|
||||||
|
import androidx.activity.OnBackPressedDispatcher;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||||
|
import androidx.core.view.WindowInsetsAnimationControlListenerCompat;
|
||||||
|
import androidx.core.view.WindowInsetsAnimationControllerCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
@ -70,16 +77,21 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader;
|
|||||||
import awais.instagrabber.adapters.DirectReactionsAdapter;
|
import awais.instagrabber.adapters.DirectReactionsAdapter;
|
||||||
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
|
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
|
||||||
import awais.instagrabber.animations.CubicBezierInterpolator;
|
import awais.instagrabber.animations.CubicBezierInterpolator;
|
||||||
|
import awais.instagrabber.customviews.InsetsAnimationLinearLayout;
|
||||||
|
import awais.instagrabber.customviews.KeyNotifyingEmojiEditText;
|
||||||
import awais.instagrabber.customviews.RecordView;
|
import awais.instagrabber.customviews.RecordView;
|
||||||
import awais.instagrabber.customviews.Tooltip;
|
import awais.instagrabber.customviews.Tooltip;
|
||||||
import awais.instagrabber.customviews.emoji.Emoji;
|
import awais.instagrabber.customviews.emoji.Emoji;
|
||||||
import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog;
|
import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog;
|
||||||
import awais.instagrabber.customviews.emoji.EmojiPicker;
|
import awais.instagrabber.customviews.emoji.EmojiPicker;
|
||||||
|
import awais.instagrabber.customviews.helpers.ControlFocusInsetsAnimationCallback;
|
||||||
|
import awais.instagrabber.customviews.helpers.EmojiPickerInsetsAnimationCallback;
|
||||||
import awais.instagrabber.customviews.helpers.HeaderItemDecoration;
|
import awais.instagrabber.customviews.helpers.HeaderItemDecoration;
|
||||||
import awais.instagrabber.customviews.helpers.HeightProvider;
|
|
||||||
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
|
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
|
||||||
|
import awais.instagrabber.customviews.helpers.SimpleImeAnimationController;
|
||||||
import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback;
|
import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback;
|
||||||
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
|
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
|
||||||
|
import awais.instagrabber.customviews.helpers.TranslateDeferringInsetsAnimationCallback;
|
||||||
import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding;
|
import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding;
|
||||||
import awais.instagrabber.dialogs.DirectItemReactionDialogFragment;
|
import awais.instagrabber.dialogs.DirectItemReactionDialogFragment;
|
||||||
import awais.instagrabber.dialogs.GifPickerBottomDialogFragment;
|
import awais.instagrabber.dialogs.GifPickerBottomDialogFragment;
|
||||||
@ -111,9 +123,6 @@ import awais.instagrabber.viewmodels.AppStateViewModel;
|
|||||||
import awais.instagrabber.viewmodels.DirectThreadViewModel;
|
import awais.instagrabber.viewmodels.DirectThreadViewModel;
|
||||||
import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory;
|
import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory;
|
||||||
|
|
||||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
|
|
||||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
|
|
||||||
|
|
||||||
public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener,
|
public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener,
|
||||||
EmojiPicker.OnEmojiClickListener {
|
EmojiPicker.OnEmojiClickListener {
|
||||||
private static final String TAG = DirectMessageThreadFragment.class.getSimpleName();
|
private static final String TAG = DirectMessageThreadFragment.class.getSimpleName();
|
||||||
@ -125,7 +134,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
private DirectItemsAdapter itemsAdapter;
|
private DirectItemsAdapter itemsAdapter;
|
||||||
private MainActivity fragmentActivity;
|
private MainActivity fragmentActivity;
|
||||||
private DirectThreadViewModel viewModel;
|
private DirectThreadViewModel viewModel;
|
||||||
private ConstraintLayout root;
|
private InsetsAnimationLinearLayout root;
|
||||||
private boolean shouldRefresh = true;
|
private boolean shouldRefresh = true;
|
||||||
private List<DirectItemOrHeader> itemOrHeaders;
|
private List<DirectItemOrHeader> itemOrHeaders;
|
||||||
private List<User> users;
|
private List<User> users;
|
||||||
@ -135,14 +144,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
private ActionBar actionBar;
|
private ActionBar actionBar;
|
||||||
private AppStateViewModel appStateViewModel;
|
private AppStateViewModel appStateViewModel;
|
||||||
private Runnable prevTitleRunnable;
|
private Runnable prevTitleRunnable;
|
||||||
private int originalSoftInputMode;
|
|
||||||
private AnimatorSet animatorSet;
|
private AnimatorSet animatorSet;
|
||||||
private boolean isEmojiPickerShown;
|
|
||||||
private boolean isKbShown;
|
|
||||||
private HeightProvider heightProvider;
|
|
||||||
private boolean isRecording;
|
private boolean isRecording;
|
||||||
private boolean wasKbShowing;
|
|
||||||
private int keyboardHeight = Utils.convertDpToPx(250);
|
|
||||||
private DirectItemReactionDialogFragment reactionDialogFragment;
|
private DirectItemReactionDialogFragment reactionDialogFragment;
|
||||||
private DirectItem itemToForward;
|
private DirectItem itemToForward;
|
||||||
private MutableLiveData<Object> backStackSavedStateResultLiveData;
|
private MutableLiveData<Object> backStackSavedStateResultLiveData;
|
||||||
@ -163,6 +166,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
private MenuItem markAsSeenMenuItem;
|
private MenuItem markAsSeenMenuItem;
|
||||||
private Media tempMedia;
|
private Media tempMedia;
|
||||||
private DirectItem addReactionItem;
|
private DirectItem addReactionItem;
|
||||||
|
private TranslateDeferringInsetsAnimationCallback inputHolderAnimationCallback;
|
||||||
|
private TranslateDeferringInsetsAnimationCallback chatsAnimationCallback;
|
||||||
|
private EmojiPickerInsetsAnimationCallback emojiPickerAnimationCallback;
|
||||||
|
private boolean hasKbOpenedOnce;
|
||||||
|
private boolean wasToggled;
|
||||||
|
|
||||||
private final AppExecutors appExecutors = AppExecutors.getInstance();
|
private final AppExecutors appExecutors = AppExecutors.getInstance();
|
||||||
private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() {
|
private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() {
|
||||||
@ -304,7 +312,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG);
|
emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final DirectItemLongClickListener directItemLongClickListener = position -> {
|
private final DirectItemLongClickListener directItemLongClickListener = position -> {
|
||||||
// viewModel.setSelectedPosition(position);
|
// viewModel.setSelectedPosition(position);
|
||||||
};
|
};
|
||||||
@ -333,6 +340,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
backStackSavedStateResultLiveData.postValue(null);
|
backStackSavedStateResultLiveData.postValue(null);
|
||||||
};
|
};
|
||||||
private final MutableLiveData<Integer> inputLength = new MutableLiveData<>(0);
|
private final MutableLiveData<Integer> inputLength = new MutableLiveData<>(0);
|
||||||
|
private final MutableLiveData<Boolean> emojiPickerVisible = new MutableLiveData<>(false);
|
||||||
|
private final MutableLiveData<Boolean> kbVisible = new MutableLiveData<>(false);
|
||||||
|
private final OnBackPressedCallback onEmojiPickerBackPressedCallback = new OnBackPressedCallback(false) {
|
||||||
|
@Override
|
||||||
|
public void handleOnBackPressed() {
|
||||||
|
emojiPickerVisible.postValue(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
@ -371,13 +386,13 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
tooltip = new Tooltip(context, root, getResources().getColor(R.color.grey_400), getResources().getColor(R.color.black));
|
tooltip = new Tooltip(context, root, getResources().getColor(R.color.grey_400), getResources().getColor(R.color.black));
|
||||||
originalSoftInputMode = fragmentActivity.getWindow().getAttributes().softInputMode;
|
|
||||||
// todo check has camera and remove view
|
// todo check has camera and remove view
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||||
|
// WindowCompat.setDecorFitsSystemWindows(fragmentActivity.getWindow(), false);
|
||||||
if (!shouldRefresh) return;
|
if (!shouldRefresh) return;
|
||||||
init();
|
init();
|
||||||
binding.send.post(() -> initialSendX = binding.send.getX());
|
binding.send.post(() -> initialSendX = binding.send.getX());
|
||||||
@ -490,10 +505,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
binding.recordView.cancelRecording(binding.send);
|
binding.recordView.cancelRecording(binding.send);
|
||||||
}
|
}
|
||||||
if (isKbShown) {
|
emojiPickerVisible.postValue(false);
|
||||||
wasKbShowing = true;
|
kbVisible.postValue(false);
|
||||||
binding.emojiPicker.setAlpha(0);
|
binding.inputHolder.setTranslationY(0);
|
||||||
}
|
binding.chats.setTranslationY(0);
|
||||||
|
binding.emojiPicker.setTranslationY(0);
|
||||||
removeObservers();
|
removeObservers();
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
@ -501,16 +517,12 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
fragmentActivity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_NOTHING | SOFT_INPUT_STATE_HIDDEN);
|
|
||||||
if (wasKbShowing) {
|
|
||||||
binding.input.requestFocus();
|
|
||||||
binding.input.post(this::showKeyboard);
|
|
||||||
wasKbShowing = false;
|
|
||||||
}
|
|
||||||
if (initialSendX != 0) {
|
if (initialSendX != 0) {
|
||||||
binding.send.setX(initialSendX);
|
binding.send.setX(initialSendX);
|
||||||
}
|
}
|
||||||
binding.send.stopScale();
|
binding.send.stopScale();
|
||||||
|
final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher();
|
||||||
|
onBackPressedDispatcher.addCallback(onEmojiPickerBackPressedCallback);
|
||||||
setupBackStackResultObserver();
|
setupBackStackResultObserver();
|
||||||
setObservers();
|
setObservers();
|
||||||
// attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue());
|
// attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue());
|
||||||
@ -533,13 +545,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
if (prevTitleRunnable != null) {
|
if (prevTitleRunnable != null) {
|
||||||
appExecutors.mainThread().cancel(prevTitleRunnable);
|
appExecutors.mainThread().cancel(prevTitleRunnable);
|
||||||
}
|
}
|
||||||
if (heightProvider != null) {
|
|
||||||
// need to close the height provider popup before navigating back to prevent leak
|
|
||||||
heightProvider.dismiss();
|
|
||||||
}
|
|
||||||
if (originalSoftInputMode != 0) {
|
|
||||||
fragmentActivity.getWindow().setSoftInputMode(originalSoftInputMode);
|
|
||||||
}
|
|
||||||
for (int childCount = binding.chats.getChildCount(), i = 0; i < childCount; ++i) {
|
for (int childCount = binding.chats.getChildCount(), i = 0; i < childCount; ++i) {
|
||||||
final RecyclerView.ViewHolder holder = binding.chats.getChildViewHolder(binding.chats.getChildAt(i));
|
final RecyclerView.ViewHolder holder = binding.chats.getChildViewHolder(binding.chats.getChildAt(i));
|
||||||
if (holder == null) continue;
|
if (holder == null) continue;
|
||||||
@ -561,37 +566,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
actionBar = fragmentActivity.getSupportActionBar();
|
actionBar = fragmentActivity.getSupportActionBar();
|
||||||
setupList();
|
setupList();
|
||||||
root.post(this::setupInput);
|
root.post(this::setupInput);
|
||||||
// root.post(this::getInitialData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private void getInitialData() {
|
|
||||||
// final Bundle arguments = getArguments();
|
|
||||||
// if (arguments == null) return;
|
|
||||||
// final DirectMessageThreadFragmentArgs args = DirectMessageThreadFragmentArgs.fromBundle(arguments);
|
|
||||||
// final boolean pending = args.getPending();
|
|
||||||
// final NavController navController = NavHostFragment.findNavController(this);
|
|
||||||
// final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
|
|
||||||
// final List<DirectThread> threads;
|
|
||||||
// if (!pending) {
|
|
||||||
// final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
|
|
||||||
// threads = threadListViewModel.getThreads().getValue();
|
|
||||||
// } else {
|
|
||||||
// final DirectPendingInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectPendingInboxViewModel.class);
|
|
||||||
// threads = threadListViewModel.getThreads().getValue();
|
|
||||||
// }
|
|
||||||
// final Optional<DirectThread> first = threads != null
|
|
||||||
// ? threads.stream()
|
|
||||||
// .filter(thread -> thread.getThreadId().equals(viewModel.getThreadId()))
|
|
||||||
// .findFirst()
|
|
||||||
// : Optional.empty();
|
|
||||||
// if (first.isPresent()) {
|
|
||||||
// final DirectThread thread = first.get();
|
|
||||||
// viewModel.setThread(thread);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// viewModel.fetchChats();
|
|
||||||
// }
|
|
||||||
|
|
||||||
private void setupList() {
|
private void setupList() {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
@ -1100,23 +1076,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
|
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
|
||||||
final int length = s.length();
|
final int length = s.length();
|
||||||
inputLength.postValue(length);
|
inputLength.postValue(length);
|
||||||
// boolean showExtraInputOptionsChanged = false;
|
|
||||||
// if (prevLength != 0 && length == 0) {
|
|
||||||
// inputLength.postValue(true);
|
|
||||||
// showExtraInputOptionsChanged = true;
|
|
||||||
// binding.send.setListenForRecord(true);
|
|
||||||
// startIconAnimation();
|
|
||||||
// }
|
|
||||||
// if (prevLength == 0 && length != 0) {
|
|
||||||
// inputLength.postValue(false);
|
|
||||||
// showExtraInputOptionsChanged = true;
|
|
||||||
// binding.send.setListenForRecord(false);
|
|
||||||
// startIconAnimation();
|
|
||||||
// }
|
|
||||||
// if (!showExtraInputOptionsChanged) {
|
|
||||||
// showExtraInputOptions.postValue(length == 0);
|
|
||||||
// }
|
|
||||||
// prevLength = length;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
binding.send.setOnRecordClickListener(v -> {
|
binding.send.setOnRecordClickListener(v -> {
|
||||||
@ -1131,30 +1090,15 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
Log.d(TAG, "setOnRecordLongClickListener");
|
Log.d(TAG, "setOnRecordLongClickListener");
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
binding.input.setShowSoftInputOnFocus(false);
|
binding.input.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
binding.input.requestFocus();
|
if (!hasFocus) return;
|
||||||
binding.input.setOnKeyEventListener((keyCode, keyEvent) -> {
|
final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue();
|
||||||
if (keyCode != KeyEvent.KEYCODE_BACK) return false;
|
if (emojiPickerVisibleValue == null || !emojiPickerVisibleValue) return;
|
||||||
// We'll close the keyboard/emoji picker only when user releases the back button
|
inputHolderAnimationCallback.setShouldTranslate(false);
|
||||||
// return true so that system doesn't handle the event
|
chatsAnimationCallback.setShouldTranslate(false);
|
||||||
if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true;
|
emojiPickerAnimationCallback.setShouldTranslate(false);
|
||||||
if (!isKbShown && !isEmojiPickerShown) {
|
|
||||||
// if both keyboard and emoji picker are hidden, navigate back
|
|
||||||
if (heightProvider != null) {
|
|
||||||
// need to close the height provider popup before navigating back to prevent leak
|
|
||||||
heightProvider.dismiss();
|
|
||||||
}
|
|
||||||
NavHostFragment.findNavController(this).navigateUp();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
|
|
||||||
hideKeyboard(true);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
binding.input.setOnClickListener(v -> {
|
|
||||||
if (isKbShown) return;
|
|
||||||
showKeyboard();
|
|
||||||
});
|
});
|
||||||
|
setupInsetsCallback();
|
||||||
setupEmojiPicker();
|
setupEmojiPicker();
|
||||||
binding.gallery.setOnClickListener(v -> {
|
binding.gallery.setOnClickListener(v -> {
|
||||||
final MediaPickerBottomDialogFragment mediaPicker = MediaPickerBottomDialogFragment.newInstance();
|
final MediaPickerBottomDialogFragment mediaPicker = MediaPickerBottomDialogFragment.newInstance();
|
||||||
@ -1166,7 +1110,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
mediaPicker.show(getChildFragmentManager(), "MediaPicker");
|
mediaPicker.show(getChildFragmentManager(), "MediaPicker");
|
||||||
hideKeyboard(true);
|
|
||||||
});
|
});
|
||||||
binding.gif.setOnClickListener(v -> {
|
binding.gif.setOnClickListener(v -> {
|
||||||
final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance();
|
final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance();
|
||||||
@ -1176,7 +1119,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
handleSentMessage(viewModel.sendAnimatedMedia(giphyGif));
|
handleSentMessage(viewModel.sendAnimatedMedia(giphyGif));
|
||||||
});
|
});
|
||||||
gifPicker.show(getChildFragmentManager(), "GifPicker");
|
gifPicker.show(getChildFragmentManager(), "GifPicker");
|
||||||
hideKeyboard(true);
|
|
||||||
});
|
});
|
||||||
binding.camera.setOnClickListener(v -> {
|
binding.camera.setOnClickListener(v -> {
|
||||||
final Intent intent = new Intent(context, CameraActivity.class);
|
final Intent intent = new Intent(context, CameraActivity.class);
|
||||||
@ -1184,6 +1126,73 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupInsetsCallback() {
|
||||||
|
inputHolderAnimationCallback = new TranslateDeferringInsetsAnimationCallback(
|
||||||
|
binding.inputHolder,
|
||||||
|
WindowInsetsCompat.Type.systemBars(),
|
||||||
|
WindowInsetsCompat.Type.ime(),
|
||||||
|
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE
|
||||||
|
);
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(binding.inputHolder, inputHolderAnimationCallback);
|
||||||
|
chatsAnimationCallback = new TranslateDeferringInsetsAnimationCallback(
|
||||||
|
binding.chats,
|
||||||
|
WindowInsetsCompat.Type.systemBars(),
|
||||||
|
WindowInsetsCompat.Type.ime()
|
||||||
|
);
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(binding.chats, chatsAnimationCallback);
|
||||||
|
emojiPickerAnimationCallback = new EmojiPickerInsetsAnimationCallback(
|
||||||
|
binding.emojiPicker,
|
||||||
|
WindowInsetsCompat.Type.systemBars(),
|
||||||
|
WindowInsetsCompat.Type.ime()
|
||||||
|
);
|
||||||
|
emojiPickerAnimationCallback.setKbVisibilityListener(this::onKbVisibilityChange);
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(binding.emojiPicker, emojiPickerAnimationCallback);
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(
|
||||||
|
binding.input,
|
||||||
|
new ControlFocusInsetsAnimationCallback(binding.input)
|
||||||
|
);
|
||||||
|
final SimpleImeAnimationController imeAnimController = root.getImeAnimController();
|
||||||
|
if (imeAnimController != null) {
|
||||||
|
imeAnimController.setAnimationControlListener(new WindowInsetsAnimationControlListenerCompat() {
|
||||||
|
@Override
|
||||||
|
public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) {
|
||||||
|
checkKbVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) {
|
||||||
|
checkKbVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkKbVisibility() {
|
||||||
|
final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(binding.getRoot());
|
||||||
|
final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime());
|
||||||
|
onKbVisibilityChange(visible);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onKbVisibilityChange(final boolean kbVisible) {
|
||||||
|
this.kbVisible.postValue(kbVisible);
|
||||||
|
if (wasToggled) {
|
||||||
|
emojiPickerVisible.postValue(!kbVisible);
|
||||||
|
wasToggled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue();
|
||||||
|
if (kbVisible && emojiPickerVisibleValue != null && emojiPickerVisibleValue) {
|
||||||
|
emojiPickerVisible.postValue(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!kbVisible) {
|
||||||
|
emojiPickerVisible.postValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void startIconAnimation() {
|
private void startIconAnimation() {
|
||||||
final Drawable icon = binding.send.getIcon();
|
final Drawable icon = binding.send.getIcon();
|
||||||
if (icon instanceof Animatable) {
|
if (icon instanceof Animatable) {
|
||||||
@ -1230,15 +1239,87 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
private void setupEmojiPicker() {
|
private void setupEmojiPicker() {
|
||||||
root.post(() -> binding.emojiPicker.init(
|
root.post(() -> binding.emojiPicker.init(
|
||||||
root,
|
root,
|
||||||
(view, emoji) -> binding.input.append(emoji.getUnicode()),
|
(view, emoji) -> {
|
||||||
|
final KeyNotifyingEmojiEditText input = binding.input;
|
||||||
|
final int start = input.getSelectionStart();
|
||||||
|
final int end = input.getSelectionEnd();
|
||||||
|
if (start < 0) {
|
||||||
|
input.append(emoji.getUnicode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.getText().replace(
|
||||||
|
Math.min(start, end),
|
||||||
|
Math.max(start, end),
|
||||||
|
emoji.getUnicode(),
|
||||||
|
0,
|
||||||
|
emoji.getUnicode().length()
|
||||||
|
);
|
||||||
|
},
|
||||||
() -> binding.input.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
() -> binding.input.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||||
));
|
));
|
||||||
setupKbHeightProvider();
|
binding.emojiToggle.setOnClickListener(v -> {
|
||||||
if (keyboardHeight == 0) {
|
Boolean isEmojiPickerVisible = emojiPickerVisible.getValue();
|
||||||
keyboardHeight = Utils.convertDpToPx(250);
|
if (isEmojiPickerVisible == null) isEmojiPickerVisible = false;
|
||||||
|
Boolean isKbVisible = kbVisible.getValue();
|
||||||
|
if (isKbVisible == null) isKbVisible = false;
|
||||||
|
wasToggled = isEmojiPickerVisible || isKbVisible;
|
||||||
|
|
||||||
|
if (isEmojiPickerVisible) {
|
||||||
|
if (hasKbOpenedOnce && binding.emojiPicker.getTranslationY() != 0) {
|
||||||
|
inputHolderAnimationCallback.setShouldTranslate(false);
|
||||||
|
chatsAnimationCallback.setShouldTranslate(false);
|
||||||
|
emojiPickerAnimationCallback.setShouldTranslate(false);
|
||||||
}
|
}
|
||||||
setEmojiPickerBounds();
|
// trigger ime.
|
||||||
binding.emojiToggle.setOnClickListener(v -> toggleEmojiPicker());
|
// Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here
|
||||||
|
showKeyboard();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKbVisible) {
|
||||||
|
// hide the keyboard, but don't translate the views
|
||||||
|
// Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here
|
||||||
|
inputHolderAnimationCallback.setShouldTranslate(false);
|
||||||
|
chatsAnimationCallback.setShouldTranslate(false);
|
||||||
|
emojiPickerAnimationCallback.setShouldTranslate(false);
|
||||||
|
hideKeyboard();
|
||||||
|
}
|
||||||
|
emojiPickerVisible.postValue(true);
|
||||||
|
});
|
||||||
|
final LiveData<Pair<Boolean, Boolean>> emojiKbVisibilityLD = Utils.zipLiveData(emojiPickerVisible, kbVisible);
|
||||||
|
emojiKbVisibilityLD.observe(getViewLifecycleOwner(), pair -> {
|
||||||
|
Boolean isEmojiPickerVisible = pair.first;
|
||||||
|
Boolean isKbVisible = pair.second;
|
||||||
|
if (isEmojiPickerVisible == null) isEmojiPickerVisible = false;
|
||||||
|
if (isKbVisible == null) isKbVisible = false;
|
||||||
|
root.setScrollImeOffScreenWhenVisible(!isEmojiPickerVisible);
|
||||||
|
root.setScrollImeOnScreenWhenNotVisible(!isEmojiPickerVisible);
|
||||||
|
onEmojiPickerBackPressedCallback.setEnabled(isEmojiPickerVisible && !isKbVisible);
|
||||||
|
if (isEmojiPickerVisible && !isKbVisible) {
|
||||||
|
animatePan(binding.emojiPicker.getMeasuredHeight(), unused -> {
|
||||||
|
binding.emojiPicker.setAlpha(1);
|
||||||
|
binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24);
|
||||||
|
return null;
|
||||||
|
}, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isEmojiPickerVisible && !isKbVisible) {
|
||||||
|
animatePan(0, null, unused -> {
|
||||||
|
binding.emojiPicker.setAlpha(0);
|
||||||
|
binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// isKbVisible will always be true going forward
|
||||||
|
hasKbOpenedOnce = true;
|
||||||
|
if (!isEmojiPickerVisible) {
|
||||||
|
binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
|
||||||
|
binding.emojiPicker.setAlpha(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
binding.emojiPicker.setAlpha(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showKeyboard() {
|
public void showKeyboard() {
|
||||||
@ -1246,67 +1327,21 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
if (imm == null) return;
|
if (imm == null) return;
|
||||||
if (!isEmojiPickerShown) {
|
if (!binding.input.isFocused()) {
|
||||||
binding.emojiPicker.setAlpha(0);
|
binding.input.requestFocus();
|
||||||
}
|
}
|
||||||
final boolean shown = imm.showSoftInput(binding.input, InputMethodManager.SHOW_IMPLICIT);
|
final boolean shown = imm.showSoftInput(binding.input, InputMethodManager.SHOW_IMPLICIT);
|
||||||
if (!shown) {
|
if (!shown) {
|
||||||
Log.e(TAG, "showKeyboard: System did not display the keyboard");
|
Log.e(TAG, "showKeyboard: System did not display the keyboard");
|
||||||
}
|
}
|
||||||
if (!isEmojiPickerShown) {
|
|
||||||
animatePan(keyboardHeight);
|
|
||||||
}
|
|
||||||
isKbShown = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideKeyboard(final boolean shouldPan) {
|
public void hideKeyboard() {
|
||||||
final Context context = getContext();
|
final Context context = getContext();
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
if (imm == null) return;
|
if (imm == null) return;
|
||||||
if (shouldPan) {
|
|
||||||
binding.emojiPicker.setAlpha(0);
|
|
||||||
}
|
|
||||||
imm.hideSoftInputFromWindow(binding.input.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
imm.hideSoftInputFromWindow(binding.input.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
||||||
if (shouldPan) {
|
|
||||||
animatePan(0);
|
|
||||||
isEmojiPickerShown = false;
|
|
||||||
binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
|
|
||||||
}
|
|
||||||
isKbShown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle between emoji picker and keyboard
|
|
||||||
* If both are hidden, the emoji picker is shown first
|
|
||||||
*/
|
|
||||||
private void toggleEmojiPicker() {
|
|
||||||
if (isKbShown) {
|
|
||||||
binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24);
|
|
||||||
hideKeyboard(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isEmojiPickerShown) {
|
|
||||||
binding.emojiToggle.setIconResource(R.drawable.ic_face_24);
|
|
||||||
showKeyboard();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24);
|
|
||||||
animatePan(keyboardHeight);
|
|
||||||
isEmojiPickerShown = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set height of the emoji picker
|
|
||||||
*/
|
|
||||||
private void setEmojiPickerBounds() {
|
|
||||||
final ViewGroup.LayoutParams layoutParams = binding.emojiPicker.getLayoutParams();
|
|
||||||
layoutParams.height = keyboardHeight;
|
|
||||||
if (!isEmojiPickerShown) {
|
|
||||||
// If emoji picker is hidden reset the translationY so that it doesn't peek from bottom
|
|
||||||
binding.emojiPicker.setTranslationY(keyboardHeight);
|
|
||||||
}
|
|
||||||
binding.emojiPicker.requestLayout();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSendToMicIcon() {
|
private void setSendToMicIcon() {
|
||||||
@ -1375,40 +1410,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupKbHeightProvider() {
|
|
||||||
if (heightProvider != null) return;
|
|
||||||
heightProvider = new HeightProvider(fragmentActivity).init().setHeightListener(height -> {
|
|
||||||
if (height > 100 && keyboardHeight != height) {
|
|
||||||
// save the current keyboard height to settings to use later
|
|
||||||
keyboardHeight = height;
|
|
||||||
setEmojiPickerBounds();
|
|
||||||
animatePan(keyboardHeight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the translationY of views to height with animation
|
// Sets the translationY of views to height with animation
|
||||||
private void animatePan(final int height) {
|
private void animatePan(final int height,
|
||||||
|
@Nullable final Function<Void, Void> onAnimationStart,
|
||||||
|
@Nullable final Function<Void, Void> onAnimationEnd) {
|
||||||
if (animatorSet != null && animatorSet.isStarted()) {
|
if (animatorSet != null && animatorSet.isStarted()) {
|
||||||
animatorSet.cancel();
|
animatorSet.cancel();
|
||||||
}
|
}
|
||||||
final ImmutableList.Builder<Animator> builder = ImmutableList.builder();
|
final ImmutableList.Builder<Animator> builder = ImmutableList.builder();
|
||||||
builder.add(
|
builder.add(
|
||||||
ObjectAnimator.ofFloat(binding.chats, TRANSLATION_Y, -height),
|
ObjectAnimator.ofFloat(binding.chats, TRANSLATION_Y, -height),
|
||||||
ObjectAnimator.ofFloat(binding.input, TRANSLATION_Y, -height),
|
ObjectAnimator.ofFloat(binding.inputHolder, TRANSLATION_Y, -height),
|
||||||
ObjectAnimator.ofFloat(binding.inputBg, TRANSLATION_Y, -height),
|
ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, -height)
|
||||||
ObjectAnimator.ofFloat(binding.recordView, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.emojiToggle, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.gif, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.gallery, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.camera, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.send, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.replyBg, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.replyInfo, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.replyCancel, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.replyPreviewImage, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.replyPreviewText, TRANSLATION_Y, -height),
|
|
||||||
ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, keyboardHeight - height)
|
|
||||||
);
|
);
|
||||||
// if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) {
|
// if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) {
|
||||||
// builder.add(ObjectAnimator.ofFloat(headerItemDecoration.getCurrentHeader(), TRANSLATION_Y, height));
|
// builder.add(ObjectAnimator.ofFloat(headerItemDecoration.getCurrentHeader(), TRANSLATION_Y, height));
|
||||||
@ -1418,10 +1431,21 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
|
|||||||
animatorSet.setDuration(200);
|
animatorSet.setDuration(200);
|
||||||
animatorSet.setInterpolator(CubicBezierInterpolator.EASE_IN);
|
animatorSet.setInterpolator(CubicBezierInterpolator.EASE_IN);
|
||||||
animatorSet.addListener(new AnimatorListenerAdapter() {
|
animatorSet.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(final Animator animation) {
|
||||||
|
super.onAnimationStart(animation);
|
||||||
|
if (onAnimationStart != null) {
|
||||||
|
onAnimationStart.apply(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(final Animator animation) {
|
public void onAnimationEnd(final Animator animation) {
|
||||||
binding.emojiPicker.setAlpha(1);
|
super.onAnimationEnd(animation);
|
||||||
animatorSet = null;
|
animatorSet = null;
|
||||||
|
if (onAnimationEnd != null) {
|
||||||
|
onAnimationEnd.apply(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
animatorSet.start();
|
animatorSet.start();
|
||||||
|
@ -38,6 +38,8 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MediatorLiveData;
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
@ -562,4 +564,39 @@ public final class Utils {
|
|||||||
display.getRealSize(size);
|
display.getRealSize(size);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <F, S> LiveData<Pair<F, S>> zipLiveData(@NonNull final LiveData<F> firstLiveData,
|
||||||
|
@NonNull final LiveData<S> secondLiveData) {
|
||||||
|
final ZippedLiveData<F, S> zippedLiveData = new ZippedLiveData<>();
|
||||||
|
zippedLiveData.addFirstSource(firstLiveData);
|
||||||
|
zippedLiveData.addSecondSource(secondLiveData);
|
||||||
|
return zippedLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ZippedLiveData<F, S> extends MediatorLiveData<Pair<F, S>> {
|
||||||
|
private F lastF;
|
||||||
|
private S lastS;
|
||||||
|
|
||||||
|
private void update() {
|
||||||
|
F localLastF = lastF;
|
||||||
|
S localLastS = lastS;
|
||||||
|
if (localLastF != null && localLastS != null) {
|
||||||
|
setValue(new Pair<>(localLastF, localLastS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFirstSource(@NonNull final LiveData<F> firstLiveData) {
|
||||||
|
addSource(firstLiveData, f -> {
|
||||||
|
lastF = f;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSecondSource(@NonNull final LiveData<S> secondLiveData) {
|
||||||
|
addSource(secondLiveData, s -> {
|
||||||
|
lastS = s;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
package awais.instagrabber.utils;
|
package awais.instagrabber.utils;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.GradientDrawable;
|
import android.graphics.drawable.GradientDrawable;
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
|
import android.os.Build;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.res.ResourcesCompat;
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
import androidx.core.util.Pair;
|
import androidx.core.util.Pair;
|
||||||
|
import androidx.dynamicanimation.animation.FloatPropertyCompat;
|
||||||
|
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import kotlin.jvm.internal.Intrinsics;
|
||||||
|
|
||||||
public final class ViewUtils {
|
public final class ViewUtils {
|
||||||
|
|
||||||
@ -69,4 +79,45 @@ public final class ViewUtils {
|
|||||||
public static float getTextViewValueWidth(final TextView textView, final String text) {
|
public static float getTextViewValueWidth(final TextView textView, final String text) {
|
||||||
return textView.getPaint().measureText(text);
|
return textView.getPaint().measureText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates [SpringAnimation] for object.
|
||||||
|
* If finalPosition is not [Float.NaN] then create [SpringAnimation] with
|
||||||
|
* [SpringForce.mFinalPosition].
|
||||||
|
*
|
||||||
|
* @param object Object
|
||||||
|
* @param property object's property to be animated.
|
||||||
|
* @param finalPosition [SpringForce.mFinalPosition] Final position of spring.
|
||||||
|
* @return [SpringAnimation]
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static SpringAnimation springAnimationOf(final Object object,
|
||||||
|
final FloatPropertyCompat<Object> property,
|
||||||
|
@Nullable final Float finalPosition) {
|
||||||
|
return finalPosition == null ? new SpringAnimation(object, property) : new SpringAnimation(object, property, finalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void suppressLayoutCompat(@NotNull ViewGroup $this$suppressLayoutCompat, boolean suppress) {
|
||||||
|
Intrinsics.checkNotNullParameter($this$suppressLayoutCompat, "$this$suppressLayoutCompat");
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
$this$suppressLayoutCompat.suppressLayout(suppress);
|
||||||
|
} else {
|
||||||
|
hiddenSuppressLayout($this$suppressLayoutCompat, suppress);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean tryHiddenSuppressLayout = true;
|
||||||
|
|
||||||
|
@SuppressLint({"NewApi"})
|
||||||
|
private static void hiddenSuppressLayout(ViewGroup group, boolean suppress) {
|
||||||
|
if (tryHiddenSuppressLayout) {
|
||||||
|
try {
|
||||||
|
group.suppressLayout(suppress);
|
||||||
|
} catch (NoSuchMethodError var3) {
|
||||||
|
tryHiddenSuppressLayout = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<awais.instagrabber.customviews.InsetsNotifyingCoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main_container"
|
android:id="@+id/main_container"
|
||||||
@ -62,6 +62,7 @@
|
|||||||
android:id="@+id/main_nav_host"
|
android:id="@+id/main_nav_host"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<!--app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior"-->
|
<!--app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior"-->
|
||||||
@ -71,4 +72,4 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
app:labelVisibilityMode="auto" />
|
app:labelVisibilityMode="auto" />
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</awais.instagrabber.customviews.InsetsNotifyingCoordinatorLayout>
|
@ -1,22 +1,25 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false">
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/chats"
|
android:id="@+id/chats"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
android:scrollbars="none"
|
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" />
|
tools:listitem="@layout/layout_dm_base" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/input_holder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/chats_barrier"
|
android:id="@+id/chats_barrier"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -252,17 +255,6 @@
|
|||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle"
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle"
|
||||||
tools:visibility="visible" />
|
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" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
android:id="@+id/accept_pending_request_question"
|
android:id="@+id/accept_pending_request_question"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -310,3 +302,14 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question"
|
app:layout_constraintTop_toBottomOf="@id/accept_pending_request_question"
|
||||||
tools:visibility="gone" />
|
tools:visibility="gone" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<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_toStartOf="parent" />
|
||||||
|
</awais.instagrabber.customviews.InsetsAnimationLinearLayout>
|
Loading…
Reference in New Issue
Block a user