Skip to content

Commit

Permalink
feat: duration (#173)
Browse files Browse the repository at this point in the history
## 📜 Description

Added `duration` to plain and worklet events.

## 💡 Motivation and Context

As part of enhancement events metadata it would be good to have
`duration` prop. It may allow to create kind of parallax effect (when
animation duration is equal, but easing functions are different).

## 📢 Changelog

### JS
- added `duration` to spec file;
- updated example app;

### iOS
- extract duration and send it in eventemitter and in view events;

### Android
- extract duration and send it in eventemitter and in view events;

## 🤔 How Has This Been Tested?

Tested manually on:
- Pixel 7 Pro (Android 13);
- Xiaomi Redmi Note 5 Pro (Android 9);
- iPhone 14 Pro (iOS 16.5);

## 📸 Screenshots (if appropriate):

<img
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/f954325c-cd7e-4d03-9272-7479301cc650"
width="250px">

## 📝 Checklist

- [x] CI successfully passed
  • Loading branch information
kirillzyusko committed Jul 13, 2023
1 parent d349403 commit 88ff6e9
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 39 deletions.
8 changes: 4 additions & 4 deletions FabricExample/src/screens/Examples/Events/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,28 @@ function EventsListener() {
Toast.show({
type: 'info',
text1: '⬆️ ⌨️ Keyboard will show',
text2: `📲 Height: ${e.height}`,
text2: `📲 Height: ${e.height}, duration: ${e.duration}ms`,
});
});
const shown = KeyboardEvents.addListener('keyboardDidShow', (e) => {
Toast.show({
type: 'success',
text1: '⌨️ Keyboard is shown',
text2: `👋 Height: ${e.height}`,
text2: `👋 Height: ${e.height}, duration: ${e.duration}ms`,
});
});
const hide = KeyboardEvents.addListener('keyboardWillHide', (e) => {
Toast.show({
type: 'info',
text1: '⬇️ ⌨️ Keyboard will hide',
text2: `📲 Height: ${e.height}`,
text2: `📲 Height: ${e.height}, duration: ${e.duration}ms`,
});
});
const hid = KeyboardEvents.addListener('keyboardDidHide', (e) => {
Toast.show({
type: 'error',
text1: '⌨️ Keyboard is hidden',
text2: `🤐 Height: ${e.height}`,
text2: `🤐 Height: ${e.height}, duration: ${e.duration}ms`,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class KeyboardAnimationCallback(
private var persistentKeyboardHeight = 0.0
private var isKeyboardVisible = false
private var isTransitioning = false
private var duration = 0

init {
require(persistentInsetTypes and deferredInsetTypes == 0) {
Expand Down Expand Up @@ -65,20 +66,22 @@ class KeyboardAnimationCallback(
// (for example it happens when user changes keyboard type from 'text' to 'emoji' input
if (isKeyboardVisible && isKeyboardVisible() && !isTransitioning && Build.VERSION.SDK_INT >= 30 && !InteractiveKeyboardProvider.isInteractive) {
val keyboardHeight = getCurrentKeyboardHeight()
val durationL = 250L
val duration = durationL.toInt()

this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveStart", keyboardHeight, 1.0))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveStart", keyboardHeight, 1.0, duration))

val animation = ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
animation.addUpdateListener { animator ->
val toValue = animator.animatedValue as Float
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMove", toValue.toDouble(), toValue.toDouble() / keyboardHeight))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMove", toValue.toDouble(), toValue.toDouble() / keyboardHeight, duration))
}
animation.doOnEnd {
this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveEnd", keyboardHeight, 1.0))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveEnd", keyboardHeight, 1.0, duration))
}
animation.setDuration(250).startDelay = 0
animation.setDuration(durationL).startDelay = 0
animation.start()

this.persistentKeyboardHeight = keyboardHeight
Expand All @@ -93,6 +96,7 @@ class KeyboardAnimationCallback(
): WindowInsetsAnimationCompat.BoundsCompat {
isTransitioning = true
isKeyboardVisible = isKeyboardVisible()
duration = animation.durationMillis.toInt()
val keyboardHeight = getCurrentKeyboardHeight()

if (isKeyboardVisible) {
Expand All @@ -103,7 +107,7 @@ class KeyboardAnimationCallback(
this.emitEvent("KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow", getEventParams(keyboardHeight))

Log.i(TAG, "HEIGHT:: $keyboardHeight")
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveStart", keyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveStart", keyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0, duration))

return super.onStart(animation, bounds)
}
Expand Down Expand Up @@ -136,7 +140,7 @@ class KeyboardAnimationCallback(
Log.i(TAG, "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive}")

val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove"
this.sendEventToJS(KeyboardTransitionEvent(view.id, event, height, progress))
this.sendEventToJS(KeyboardTransitionEvent(view.id, event, height, progress, duration))

return insets
}
Expand All @@ -145,6 +149,7 @@ class KeyboardAnimationCallback(
super.onEnd(animation)

isTransitioning = false
duration = animation.durationMillis.toInt()

var keyboardHeight = this.persistentKeyboardHeight
// if keyboard becomes shown after interactive animation completion
Expand All @@ -161,7 +166,10 @@ class KeyboardAnimationCallback(
isKeyboardVisible = isKeyboardVisible || isKeyboardShown

this.emitEvent("KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow", getEventParams(keyboardHeight))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveEnd", keyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0))
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveEnd", keyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0, duration))

// reset to initial state
duration = 0
}

private fun isKeyboardVisible(): Boolean {
Expand Down Expand Up @@ -194,6 +202,7 @@ class KeyboardAnimationCallback(
private fun getEventParams(height: Double): WritableMap {
val params: WritableMap = Arguments.createMap()
params.putDouble("height", height)
params.putInt("duration", duration)

return params
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

class KeyboardTransitionEvent(viewId: Int, private val event: String, private val height: Double, private val progress: Double) : Event<KeyboardTransitionEvent>(viewId) {
class KeyboardTransitionEvent(
viewId: Int,
private val event: String,
private val height: Double,
private val progress: Double,
private val duration: Int,
) : Event<KeyboardTransitionEvent>(viewId) {
override fun getEventName() = event

// TODO: All events for a given view can be coalesced?
Expand All @@ -14,6 +20,7 @@ class KeyboardTransitionEvent(viewId: Int, private val event: String, private va
val map = Arguments.createMap()
map.putDouble("progress", progress)
map.putDouble("height", height)
map.putInt("duration", duration)
rctEventEmitter.receiveEvent(viewTag, eventName, map)
}
}
6 changes: 3 additions & 3 deletions example/__tests__/keyboard-handler.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,21 @@ describe('keyboard handler specification', () => {

expect(getByTestId('view')).toHaveStyle({ transform: [{ translateY: 0 }] });

onStart({ height: 100, progress: 1 });
onStart({ height: 100, progress: 1, duration: 250 });
jest.advanceTimersByTime(100);

expect(getByTestId('view')).toHaveAnimatedStyle({
transform: [{ translateY: 100 }],
});

onMove({ height: 20, progress: 0.2 });
onMove({ height: 20, progress: 0.2, duration: 250 });
jest.advanceTimersByTime(100);

expect(getByTestId('view')).toHaveAnimatedStyle({
transform: [{ translateY: 20 }],
});

onEnd({ height: 100, progress: 1 });
onEnd({ height: 100, progress: 1, duration: 250 });
jest.advanceTimersByTime(100);

expect(getByTestId('view')).toHaveAnimatedStyle({
Expand Down
8 changes: 4 additions & 4 deletions example/src/screens/Examples/Events/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,28 @@ function EventsListener() {
Toast.show({
type: 'info',
text1: '⬆️ ⌨️ Keyboard will show',
text2: `📲 Height: ${e.height}`,
text2: `📲 Height: ${e.height}, duration: ${e.duration}ms`,
});
});
const shown = KeyboardEvents.addListener('keyboardDidShow', (e) => {
Toast.show({
type: 'success',
text1: '⌨️ Keyboard is shown',
text2: `👋 Height: ${e.height}`,
text2: `👋 Height: ${e.height}, duration: ${e.duration}ms`,
});
});
const hide = KeyboardEvents.addListener('keyboardWillHide', (e) => {
Toast.show({
type: 'info',
text1: '⬇️ ⌨️ Keyboard will hide',
text2: `📲 Height: ${e.height}`,
text2: `📲 Height: ${e.height}, duration: ${e.duration}ms`,
});
});
const hid = KeyboardEvents.addListener('keyboardDidHide', (e) => {
Toast.show({
type: 'error',
text1: '⌨️ Keyboard is hidden',
text2: `🤐 Height: ${e.height}`,
text2: `🤐 Height: ${e.height}, duration: ${e.duration}ms`,
});
});

Expand Down
22 changes: 16 additions & 6 deletions ios/KeyboardControllerView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -44,37 +44,46 @@ - (instancetype)initWithFrame:(CGRect)frame
_props = defaultProps;

observer = [[KeyboardMovementObserver alloc]
initWithHandler:^(NSString *event, NSNumber *height, NSNumber *progress) {
initWithHandler:^(
NSString *event, NSNumber *height, NSNumber *progress, NSNumber *duration) {
if (self->_eventEmitter) {
// TODO: use reflection to reduce code duplication?
if ([event isEqualToString:@"onKeyboardMoveStart"]) {
std::dynamic_pointer_cast<const facebook::react::KeyboardControllerViewEventEmitter>(
self->_eventEmitter)
->onKeyboardMoveStart(
facebook::react::KeyboardControllerViewEventEmitter::OnKeyboardMoveStart{
.height = [height doubleValue], .progress = [progress doubleValue]});
.height = [height doubleValue],
.progress = [progress doubleValue],
.duration = [duration intValue]});
}
if ([event isEqualToString:@"onKeyboardMove"]) {
std::dynamic_pointer_cast<const facebook::react::KeyboardControllerViewEventEmitter>(
self->_eventEmitter)
->onKeyboardMove(
facebook::react::KeyboardControllerViewEventEmitter::OnKeyboardMove{
.height = [height doubleValue], .progress = [progress doubleValue]});
.height = [height doubleValue],
.progress = [progress doubleValue],
.duration = [duration intValue]});
}
if ([event isEqualToString:@"onKeyboardMoveEnd"]) {
std::dynamic_pointer_cast<const facebook::react::KeyboardControllerViewEventEmitter>(
self->_eventEmitter)
->onKeyboardMoveEnd(
facebook::react::KeyboardControllerViewEventEmitter::OnKeyboardMoveEnd{
.height = [height doubleValue], .progress = [progress doubleValue]});
.height = [height doubleValue],
.progress = [progress doubleValue],
.duration = [duration intValue]});
}
}
if ([event isEqualToString:@"onKeyboardMoveInteractive"]) {
std::dynamic_pointer_cast<const facebook::react::KeyboardControllerViewEventEmitter>(
self->_eventEmitter)
->onKeyboardMoveInteractive(
facebook::react::KeyboardControllerViewEventEmitter::OnKeyboardMoveInteractive{
.height = [height doubleValue], .progress = [progress doubleValue]});
.height = [height doubleValue],
.progress = [progress doubleValue],
.duration = [duration intValue]});
}

// TODO: use built-in _eventEmitter once NativeAnimated module will use ModernEventemitter
Expand All @@ -84,7 +93,8 @@ - (instancetype)initWithFrame:(CGRect)frame
[[KeyboardMoveEvent alloc] initWithReactTag:@(self.tag)
event:event
height:height
progress:progress];
progress:progress
duration:duration];
[bridge.eventDispatcher sendEvent:keyboardMoveEvent];
}
}
Expand Down
5 changes: 3 additions & 2 deletions ios/KeyboardControllerViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class KeyboardControllerView: UIView {
}
}

func onEvent(event: NSString, height: NSNumber, progress: NSNumber) {
func onEvent(event: NSString, height: NSNumber, progress: NSNumber, duration: NSNumber) {
// we don't want to send event to JS before the JS thread is ready
if bridge.value(forKey: "_jsThread") == nil {
return
Expand All @@ -56,7 +56,8 @@ class KeyboardControllerView: UIView {
reactTag: reactTag,
event: event as String,
height: height,
progress: progress
progress: progress,
duration: duration
)
)
}
Expand Down
3 changes: 2 additions & 1 deletion ios/KeyboardMoveEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- (instancetype)initWithReactTag:(NSNumber *)reactTag
event:(NSString *)event
height:(NSNumber *)height
progress:(NSNumber *)progress;
progress:(NSNumber *)progress
duration:(NSNumber *)duration;

@end
4 changes: 4 additions & 0 deletions ios/KeyboardMoveEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@implementation KeyboardMoveEvent {
NSNumber *_progress;
NSNumber *_height;
NSNumber *_duration;
uint16_t _coalescingKey;
}

Expand All @@ -22,6 +23,7 @@ - (instancetype)initWithReactTag:(NSNumber *)reactTag
event:(NSString *)event
height:(NSNumber *)height
progress:(NSNumber *)progress
duration:(NSNumber *)duration
{
RCTAssertParam(reactTag);

Expand All @@ -30,6 +32,7 @@ - (instancetype)initWithReactTag:(NSNumber *)reactTag
_viewTag = reactTag;
_progress = progress;
_height = height;
_duration = duration;
_coalescingKey = 0;
}
return self;
Expand All @@ -47,6 +50,7 @@ - (NSDictionary *)body
NSDictionary *body = @{
@"progress" : _progress,
@"height" : _height,
@"duration" : _duration,
};

return body;
Expand Down
Loading

0 comments on commit 88ff6e9

Please sign in to comment.