mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 03:25:34 +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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user