-
-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## 📜 Description Fixed a problem of keyboard movement not being detected in Modal window on Android. ## 💡 Motivation and Context The most challenging in this PR is getting an access to `dialog`. Initially I thought to create an additional view (`ModalKeyboardProvider`) and then through this view get an access to the `dialog`, but after some experiments I realized, that we'll get an access to `DialogRootViewGroup` and this class is not holding a reference to `dialog`, so this idea failed. Another approach was to listen to all events in `eventDispatcher` and when we detect `onShow` (`topShow`) event, then using `uiManager` we can resolve the view and get `ReactModalHostView`. This approach work, but it has one downside - we listen to all events. It doesn't hit the performance, but this approach looks like a workaround (it has too many transitive dependencies). A PR facebook/react-native#45534 introduces a new API for detection show/hide modals. I hope it'll be merged at some point of time and we can start to use new API. In a meantime I'll use the current approach and will incapsulate all work in `ModalAttachedWatcher` - in this case we have our own interface for dealing with modals, and we can change internals without affecting other files 😎 Closes #369 Potentially helps to fix #387 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### E2E - added new `modal` test; ### Android - added `ModalAttachedWatcher` class; - `KeyboardAnimationCallback` now accepts config to reduce amount of passed params (`KeyboardAnimationCallbackConfig`); - pass `eventPropagationView` to `KeyboardAnimationCallback` and `FocusedInputObserver`; - return `WindowInsetsCompat.CONSUMED` from `onApplyWindowInsets` instead of `insets`; - add `syncKeyboardPosition` to `KeyboardAnimationCallback`; - dispatch the same events through cycle instead of code duplication. ## 🤔 How Has This Been Tested? Tested manually on: - Xiaomi Redmi Note 5 Pro (Android 9); - Pixel 7 Pro (Android 14); - Pixel 3A (Android 13, emulator); - Pixel 2 (AOSP, Android 9, e2e tests); - Pixel 2 (Android 11, emulator, e2e tests). ## 📸 Screenshots (if appropriate): |Before|After| |-------|-----| |<img width="358" alt="Screenshot 2024-07-24 at 16 43 20" src="https://github.com/user-attachments/assets/bb3f5a2a-799a-4e46-af4f-4616d163ebbe">|<img width="360" alt="Screenshot 2024-07-24 at 16 41 39" src="https://github.com/user-attachments/assets/c87fffce-b8ef-4bf3-a4f3-0985e5a030ff">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
- Loading branch information
1 parent
a2abcd2
commit 4a796eb
Showing
13 changed files
with
219 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package com.reactnativekeyboardcontroller.modal | ||
|
||
import android.util.Log | ||
import android.view.WindowManager | ||
import androidx.core.view.ViewCompat | ||
import com.facebook.react.uimanager.ThemedReactContext | ||
import com.facebook.react.uimanager.UIManagerHelper | ||
import com.facebook.react.uimanager.common.UIManagerType | ||
import com.facebook.react.uimanager.events.Event | ||
import com.facebook.react.uimanager.events.EventDispatcherListener | ||
import com.facebook.react.views.modal.ReactModalHostView | ||
import com.facebook.react.views.view.ReactViewGroup | ||
import com.reactnativekeyboardcontroller.BuildConfig | ||
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback | ||
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig | ||
|
||
private val TAG = ModalAttachedWatcher::class.qualifiedName | ||
|
||
class ModalAttachedWatcher( | ||
private val view: ReactViewGroup, | ||
private val reactContext: ThemedReactContext, | ||
private val config: () -> KeyboardAnimationCallbackConfig, | ||
) : EventDispatcherListener { | ||
private val archType = if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) UIManagerType.FABRIC else UIManagerType.DEFAULT | ||
private val uiManager = UIManagerHelper.getUIManager(reactContext.reactApplicationContext, archType) | ||
private val eventDispatcher = UIManagerHelper.getEventDispatcher(reactContext.reactApplicationContext, archType) | ||
|
||
override fun onEventDispatch(event: Event<out Event<*>>?) { | ||
if (event?.eventName != MODAL_SHOW_EVENT) { | ||
return | ||
} | ||
|
||
val modal = try { | ||
uiManager?.resolveView(event.viewTag) as? ReactModalHostView | ||
} catch (ignore: Exception) { | ||
Log.w(TAG, "Can not resolve view for Modal#${event.viewTag}", ignore) | ||
null | ||
} | ||
|
||
if (modal == null) { | ||
return | ||
} | ||
|
||
val dialog = modal.dialog | ||
val window = dialog?.window | ||
val rootView = window?.decorView?.rootView | ||
|
||
if (rootView != null) { | ||
val callback = KeyboardAnimationCallback( | ||
view = rootView, | ||
eventPropagationView = view, | ||
context = reactContext, | ||
config = config(), | ||
) | ||
|
||
ViewCompat.setWindowInsetsAnimationCallback(rootView, callback) | ||
ViewCompat.setOnApplyWindowInsetsListener(rootView, callback) | ||
|
||
dialog.setOnDismissListener { | ||
callback.syncKeyboardPosition() | ||
callback.destroy() | ||
} | ||
|
||
// imitating edge-to-edge mode behavior | ||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) | ||
} | ||
} | ||
|
||
fun enable() { | ||
eventDispatcher?.addListener(this) | ||
} | ||
|
||
fun disable() { | ||
eventDispatcher?.removeListener(this) | ||
} | ||
|
||
companion object { | ||
private const val MODAL_SHOW_EVENT = "topShow" | ||
} | ||
} |
Oops, something went wrong.