Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[useAnimatedKeyboard][Android] Keyboard refactor #5665

Merged
merged 10 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions android/src/main/cpp/NativeProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -350,16 +350,14 @@ void NativeProxy::setGestureState(int handlerTag, int newState) {
}

int NativeProxy::subscribeForKeyboardEvents(
std::function<void(int, int)> keyboardEventDataUpdater,
std::function<void(int, int)> callback,
bool isStatusBarTranslucent) {
static const auto method =
getJniMethod<int(KeyboardEventDataUpdater::javaobject, bool)>(
getJniMethod<int(KeyboardWorkletWrapper::javaobject, bool)>(
"subscribeForKeyboardEvents");
return method(
javaPart_.get(),
KeyboardEventDataUpdater::newObjectCxxArgs(
std::move(keyboardEventDataUpdater))
.get(),
KeyboardWorkletWrapper::newObjectCxxArgs(std::move(callback)).get(),
isStatusBarTranslucent);
}

Expand Down
14 changes: 6 additions & 8 deletions android/src/main/cpp/NativeProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,27 +117,25 @@ class SensorSetter : public HybridClass<SensorSetter> {
std::function<void(double[], int)> callback_;
};

class KeyboardEventDataUpdater : public HybridClass<KeyboardEventDataUpdater> {
class KeyboardWorkletWrapper : public HybridClass<KeyboardWorkletWrapper> {
public:
static auto constexpr kJavaDescriptor =
"Lcom/swmansion/reanimated/nativeProxy/KeyboardEventDataUpdater;";
"Lcom/swmansion/reanimated/keyboard/KeyboardWorkletWrapper;";

void keyboardEventDataUpdater(int keyboardState, int height) {
void invoke(int keyboardState, int height) {
callback_(keyboardState, height);
}

static void registerNatives() {
javaClassStatic()->registerNatives({
makeNativeMethod(
"keyboardEventDataUpdater",
KeyboardEventDataUpdater::keyboardEventDataUpdater),
makeNativeMethod("invoke", KeyboardWorkletWrapper::invoke),
});
}

private:
friend HybridBase;

explicit KeyboardEventDataUpdater(std::function<void(int, int)> callback)
explicit KeyboardWorkletWrapper(std::function<void(int, int)> callback)
: callback_(std::move(callback)) {}

std::function<void(int, int)> callback_;
Expand Down Expand Up @@ -206,7 +204,7 @@ class NativeProxy : public jni::HybridClass<NativeProxy> {
std::function<void(double[], int)> setter);
void unregisterSensor(int sensorId);
int subscribeForKeyboardEvents(
std::function<void(int, int)> keyboardEventDataUpdater,
std::function<void(int, int)> callback,
bool isStatusBarTranslucent);
void unsubscribeFromKeyboardEvents(int listenerId);
#ifdef RCT_NEW_ARCH_ENABLED
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/cpp/OnLoad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
reanimated::AndroidUIScheduler::registerNatives();
reanimated::LayoutAnimations::registerNatives();
reanimated::SensorSetter::registerNatives();
reanimated::KeyboardEventDataUpdater::registerNatives();
reanimated::KeyboardWorkletWrapper::registerNatives();
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.swmansion.reanimated.keyboard;

import androidx.core.view.WindowInsetsCompat;
import com.facebook.react.uimanager.PixelUtil;

public class Keyboard {
private KeyboardState mState;
private int mHeight = 0;
private static final int CONTENT_TYPE_MASK = WindowInsetsCompat.Type.ime();
private static final int SYSTEM_BAR_TYPE_MASK = WindowInsetsCompat.Type.systemBars();

public KeyboardState getState() {
return mState;
}

public int getHeight() {
return mHeight;
}

public void updateHeight(WindowInsetsCompat insets) {
int contentBottomInset = insets.getInsets(CONTENT_TYPE_MASK).bottom;
int systemBarBottomInset = insets.getInsets(SYSTEM_BAR_TYPE_MASK).bottom;
int keyboardHeightDip = contentBottomInset - systemBarBottomInset;
mHeight = (int) PixelUtil.toDIPFromPixel(Math.max(0, keyboardHeightDip));
}

public void onAnimationStart() {
mState = mHeight == 0 ? KeyboardState.OPENING : KeyboardState.CLOSING;
}

public void onAnimationEnd() {
mState = mHeight == 0 ? KeyboardState.CLOSED : KeyboardState.OPEN;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.swmansion.reanimated.keyboard;

import androidx.annotation.NonNull;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import java.util.List;

public class KeyboardAnimationCallback extends WindowInsetsAnimationCompat.Callback {
private final Keyboard mKeyboard;
private final NotifyAboutKeyboardChangeFunction mNotifyAboutKeyboardChange;

public KeyboardAnimationCallback(
Keyboard keyboard, NotifyAboutKeyboardChangeFunction notifyAboutKeyboardChange) {
super(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE);
mNotifyAboutKeyboardChange = notifyAboutKeyboardChange;
mKeyboard = keyboard;
}

@NonNull
@Override
public WindowInsetsAnimationCompat.BoundsCompat onStart(
@NonNull WindowInsetsAnimationCompat animation,
@NonNull WindowInsetsAnimationCompat.BoundsCompat bounds) {
mKeyboard.onAnimationStart();
mNotifyAboutKeyboardChange.call();
return super.onStart(animation, bounds);
}

@NonNull
@Override
public WindowInsetsCompat onProgress(
@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
mKeyboard.updateHeight(insets);
mNotifyAboutKeyboardChange.call();
return insets;
}

@Override
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
mKeyboard.onAnimationEnd();
mNotifyAboutKeyboardChange.call();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.swmansion.reanimated.keyboard;

import com.facebook.react.bridge.ReactApplicationContext;
import java.lang.ref.WeakReference;
import java.util.HashMap;

@FunctionalInterface
interface NotifyAboutKeyboardChangeFunction {
void call();
}

public class KeyboardAnimationManager {
private int mNextListenerId = 0;
private final HashMap<Integer, KeyboardWorkletWrapper> mListeners = new HashMap<>();
private final Keyboard mKeyboard = new Keyboard();
private final WindowsInsetsManager mWindowsInsetsManager;

public KeyboardAnimationManager(WeakReference<ReactApplicationContext> reactContext) {
mWindowsInsetsManager =
new WindowsInsetsManager(reactContext, mKeyboard, this::notifyAboutKeyboardChange);
}

public int subscribeForKeyboardUpdates(
KeyboardWorkletWrapper callback, boolean isStatusBarTranslucent) {
int listenerId = mNextListenerId++;
if (mListeners.isEmpty()) {
KeyboardAnimationCallback keyboardAnimationCallback =
new KeyboardAnimationCallback(mKeyboard, this::notifyAboutKeyboardChange);
mWindowsInsetsManager.startObservingChanges(
keyboardAnimationCallback, isStatusBarTranslucent);
}
mListeners.put(listenerId, callback);
return listenerId;
}

public void unsubscribeFromKeyboardUpdates(int listenerId) {
mListeners.remove(listenerId);
if (mListeners.isEmpty()) {
mWindowsInsetsManager.stopObservingChanges();
}
}

public void notifyAboutKeyboardChange() {
for (KeyboardWorkletWrapper listener : mListeners.values()) {
listener.invoke(mKeyboard.getState().asInt(), mKeyboard.getHeight());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.swmansion.reanimated.keyboard;

public enum KeyboardState {
UNKNOWN(0),
OPENING(1),
OPEN(2),
CLOSING(3),
CLOSED(4);

private final int mValue;

KeyboardState(int value) {
mValue = value;
}

public int asInt() {
return mValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.swmansion.reanimated.keyboard;

import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;

@DoNotStrip
public class KeyboardWorkletWrapper {
@DoNotStrip private final HybridData mHybridData;

@DoNotStrip
private KeyboardWorkletWrapper(HybridData hybridData) {
mHybridData = hybridData;
}

public native void invoke(int keyboardState, int height);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.swmansion.reanimated.keyboard;

import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.swmansion.reanimated.BuildConfig;
import java.lang.ref.WeakReference;

public class WindowsInsetsManager {

private boolean mIsStatusBarTranslucent = false;
private final WeakReference<ReactApplicationContext> mReactContext;
private final Keyboard mKeyboard;
private final NotifyAboutKeyboardChangeFunction mNotifyAboutKeyboardChange;

public WindowsInsetsManager(
WeakReference<ReactApplicationContext> reactContext,
Keyboard keyboard,
NotifyAboutKeyboardChangeFunction notifyAboutKeyboardChange) {
mReactContext = reactContext;
mKeyboard = keyboard;
mNotifyAboutKeyboardChange = notifyAboutKeyboardChange;
}

private Window getWindow() {
return mReactContext.get().getCurrentActivity().getWindow();
}

private View getRootView() {
return getWindow().getDecorView();
}

public void startObservingChanges(
KeyboardAnimationCallback keyboardAnimationCallback, boolean isStatusBarTranslucent) {
mIsStatusBarTranslucent = isStatusBarTranslucent;
updateWindowDecor(false);
ViewCompat.setOnApplyWindowInsetsListener(getRootView(), this::onApplyWindowInsetsListener);
ViewCompat.setWindowInsetsAnimationCallback(getRootView(), keyboardAnimationCallback);
}

public void stopObservingChanges() {
updateWindowDecor(!mIsStatusBarTranslucent);
updateInsets(0, 0);
View rootView = getRootView();
ViewCompat.setWindowInsetsAnimationCallback(rootView, null);
ViewCompat.setOnApplyWindowInsetsListener(rootView, null);
}

private void updateWindowDecor(boolean decorFitsSystemWindow) {
new Handler(Looper.getMainLooper())
.post(() -> WindowCompat.setDecorFitsSystemWindows(getWindow(), decorFitsSystemWindow));
}

private WindowInsetsCompat onApplyWindowInsetsListener(View view, WindowInsetsCompat insets) {
if (mKeyboard.getState() == KeyboardState.OPEN) {
mKeyboard.updateHeight(insets);
mNotifyAboutKeyboardChange.call();
}
setWindowInsets(insets);
return insets;
}

private void setWindowInsets(WindowInsetsCompat insets) {
int paddingBottom = 0;
boolean isOldPaperImplementation =
!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && BuildConfig.REACT_NATIVE_MINOR_VERSION < 70;
if (isOldPaperImplementation) {
int navigationBarTypeMask = WindowInsetsCompat.Type.navigationBars();
paddingBottom = insets.getInsets(navigationBarTypeMask).bottom;
}
int systemBarsTypeMask = WindowInsetsCompat.Type.systemBars();
int paddingTop = insets.getInsets(systemBarsTypeMask).top;
updateInsets(paddingTop, paddingBottom);
}

private void updateInsets(int paddingTop, int paddingBottom) {
new Handler(Looper.getMainLooper())
.post(
() -> {
FrameLayout.LayoutParams params = getLayoutParams(paddingTop, paddingBottom);
int actionBarId = androidx.appcompat.R.id.action_bar_root;
View actionBarRootView = getRootView().findViewById(actionBarId);
actionBarRootView.setLayoutParams(params);
});
}

private FrameLayout.LayoutParams getLayoutParams(int paddingTop, int paddingBottom) {
int matchParentFlag = FrameLayout.LayoutParams.MATCH_PARENT;
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(matchParentFlag, matchParentFlag);
if (mIsStatusBarTranslucent) {
params.setMargins(0, 0, 0, 0);
} else {
params.setMargins(0, paddingTop, 0, paddingBottom);
}
return params;
}
}
Loading
Loading