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

KeyboardAwareScrollView: scroll only when needed #168

Open
MarceloPrado opened this issue Jun 6, 2023 · 22 comments
Open

KeyboardAwareScrollView: scroll only when needed #168

MarceloPrado opened this issue Jun 6, 2023 · 22 comments
Assignees
Labels
📚 components Anything related to the exported components of this library sponsor 💖 Someone pays money for the issue to be resolved 💸

Comments

@MarceloPrado
Copy link
Contributor

MarceloPrado commented Jun 6, 2023

Describe the bug
I'm trying to replicate the KeyboardAwareScrollView behavior fromreact-native-keyboard-aware-scroll-view. Using the KeyboardAwareScrollView found in /examples, I noticed the scroll view scrolls more than it needs to:

CleanShot.2023-06-06.at.09.27.54.mp4

Notice how once I press the input with the scroll begins here placeholder, it scrolls much further than it should. I'm trying to make the input align with my sticky action bar (it should be a fixed amount above the sticky bar).

Code snippet
I used the code from your examples folder, with one modification:

const maybeScroll = useWorkletCallback(
  (e: number) => {
    "worklet";

    const visibleRect = height - keyboardHeight.value;

    if (visibleRect - click.value <= extraScrollHeight) {
      fakeViewHeight.value = e;

      const interpolatedScrollTo = interpolate(
        e,
        [0, keyboardHeight.value],
        [
          0,
          keyboardHeight.value - (height - click.value) + extraScrollHeight,
        ]
      );
      const targetScrollY =
        Math.max(interpolatedScrollTo, 0) + scrollPosition.value;

      scrollTo(scrollViewAnimatedRef, 0, targetScrollY, false);
    } else {
      fakeViewHeight.value = 0;
    }
  },
  [extraScrollHeight]
);

I added an if/else branch that skips scrolling when the click wouldn't overlap with the keyboard + bottom offset. Now, I need to figure out the right interpolation math that causes the view to scroll only by the right/minimum amount.

note: extraScrollHeight is a prop, similar to your BOTTOM_OFFSET.

Let me know if I can help with more details! I believe this would be a great addition to the component, since it enables a more seamless migration from react-native-keyboard-aware-scroll-view.

@kirillzyusko
Copy link
Owner

kirillzyusko commented Jun 6, 2023

Hi @MarceloPrado

It seems like interpolatedScrollTo has incorrect values. In example app I didn't encounter such behaviour. Try to console.log the value and try to understand why it's bigger than expected :) The idea was to interpolate keyboardHeight to distance (like keyboard height is 200, but you'll need to scroll only 20px, so we interpolate 200 to 20).

I believe this would be a great addition to the component, since it enables a more seamless migration from react-native-keyboard-aware-scroll-view

Yes, I agree with it. I had a plan to include this component in the library (like an alternative to react-native-keyboard-aware-scroll-view), but with better animations. However I don't like the approach with capturing touch point on a screen - instead we should get coordinates of text input and I'm currently working on it. The near plan is to release 1.6.0 version with the support for synchronous calculation of layout in worklets and then release 1.7.0 with pre-bundled components (KeyboardAvoidingView/KeyboardAwareScrollView).

P. S. that's how KeyboardAwareScrollView works in example app:

demo-kasv.mp4

@kirillzyusko kirillzyusko added the example Anything related to example project label Jun 6, 2023
@MarceloPrado
Copy link
Contributor Author

Awesome, I agree with your take on capturing the input coordinates instead of the view. And thanks for the demo and explanation, will debug what I'm doing wrong.

@MarceloPrado
Copy link
Contributor Author

Figured out what happened. I was passing flex: 1 to the scroll view's contentContainerStyle. This caused a lot of issues. Once I removed it, the base code from /examples worked great!

@MarceloPrado
Copy link
Contributor Author

@kirillzyusko I think I need your input here. I noticed the provided example doesn't work properly if you need the scroll view's content to fill the available space.

Once I add flexGrow: 1 to the contentContainerStyle, and center the content, here's what happens:

CleanShot.2023-06-08.at.08.12.28.mp4

While it works smoothly without the flex-grow, if you need to center the content of the scroll view, you have no workaround. Have you seen this before?

Minimal repro:

const styles = StyleSheet.create({
  centered: {
    alignItems: "center",
    flex: 1,
    justifyContent: "center",
  },
  container: {
    flex: 1,
  },
  contentContainer: {
    backgroundColor: "#f7d7d7",
    flexGrow: 1,
  },
});

const Centered: FC<{ children: ReactNode }> = ({ children }) => (
  <View style={styles.centered}>{children}</View>
);

function randomColor() {
  return "#" + Math.random().toString(16).slice(-6);
}

export function AwareScrollView() {
  useResizeMode();

  return (
    <KeyboardAwareScrollView
      contentContainerStyle={styles.contentContainer}
      style={styles.container}
    >
      <Centered>
        {new Array(4).fill(0).map((_, i) => (
          <TextInput
            key={i}
            placeholder={`${i}`}
            placeholderTextColor="black"
            style={{
              width: "100%",
              height: 50,
              backgroundColor: randomColor(),
              marginTop: 50,
            }}
          />
        ))}
      </Centered>
    </KeyboardAwareScrollView>
  );
}

@kirillzyusko
Copy link
Owner

Hi @MarceloPrado
No, I haven't seen this before. I'll try to have a look on your code tomorrow or in nearest days 👍

That's strange - current behaviour looks like a KeyboardAvoidingView 🤷‍♂️
I think it should be fixable anyway, because you have all variables to calculate the trajectory of content movement, but I'll try to have a look when I have free time for that!

@kirillzyusko kirillzyusko reopened this Jun 8, 2023
@MarceloPrado
Copy link
Contributor Author

Hi @kirillzyusko, just a friendly ping - had you had any time to investigate this issue? Thanks in advance!

@kirillzyusko
Copy link
Owner

Hi @MarceloPrado

Not exactly this issue, but I've got some requests of what could be improved in the library when you have to deal with avoiding functionality and I was busy with that - was trying to design a new API/integrate new functionality into existing methods and got some success.

New KeyboardAwareScrollView handles more cases - it has stable bottom-padding (right now it depends on touch area and sometimes keyboard can be very close to the input), it handles TextInput switches when keyboard is open, and I believe can even handle case when multiline TextInput grows😎

Overall the new version of KeyboardAwareScrollView feels like a much better version/revision of what I had before, so my plan is to prepare a new release (1.6.0) and include a new enhanced API, and once it's done - I'll get back to this issue 👀

My expectation is that new release preparation will take about a month and after that I will switch to resolving all opened issues including this one☺️

@MarceloPrado
Copy link
Contributor Author

That's awesome @kirillzyusko, I'm happy to hear you're coming up with a more powerful API. This is such an important (and hard) problem in the current React Native ecosystem 🙂

I hope everything works out as you expect in the new API. Let me know if you need any help testing these cases, happy to help.

@NguyenHoangMinhkkkk
Copy link

video.mp4

when i focus on TextInput 5, it is a space between textinput and keyboard. how can i make it exaclty fit ?

@kirillzyusko
Copy link
Owner

@NguyenHoangMinhkkkk this is because everything depends on touch area (if you are using version below 1.5.8).

In 1.6.0 it'll be possible to measure layout without relying on touch area - just for reference 95a5376

@kirillzyusko
Copy link
Owner

@MarceloPrado I had a look on this problem. It happens because AwareScrollView adds <Reanimated.View style={view} /> below all children.

Since your content is in center and you add an empty view - your content will be pushed up to stay in the center. To overcome this problem you can use contentInset:

const props = useAnimatedProps(() => ({
    contentInset: {
      bottom: fakeViewHeight.value,
    },
  }));

  return (
    <Reanimated.ScrollView
      ref={scrollViewAnimatedRef}
      {...rest}
      onScroll={onScroll}
      animatedProps={props}
      scrollEventThrottle={16}
    >
      {children}
      {/*<Reanimated.View style={view} />*/}
    </Reanimated.ScrollView>

Such inset don't affect content position, but it works only on iOS (since contentInset is iOS specific property). I've tested and this problem is present on Android too, so I need more time to find a proper solution 👀

BTW if you have any suggestions how to fix this problem - I'll be glad to hear them 😊

@VladyslavMartynov10
Copy link

VladyslavMartynov10 commented Sep 9, 2023

@kirillzyusko

New version 1.7.0 with Aware Scroll View & KeyboardAvoidingView is amazing. The only problem that I figure out while testing is IOS behavior.

Aware Scroll View issue:

  • For instance we got focus on input and then scroll to top, input right now is under keyboard. Type any symbol we got automatic "scroll to" effect to correspondent input. Then try to trigger the same behavior again nothing happens.
  • The second tricky case is connected with already focused input with existed value, scroll to top. Start typing, "scroll to" effect doesn't trigger.

Attaching detailed demo:

Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-09-09.at.14.59.34.mp4

As you said before I believe it takes much more time to investigate all these moments including multiline input handling.

Thanks a lot for your job, you made a huge impact for resolving a painful Keyboard handling for react-native 🙂.

@kirillzyusko
Copy link
Owner

@VladyslavMartynov10 is it iOS feature to scroll to the input if it's not visible and you are typing something? Or iOS just dispatched onStart/onEnd events from keyboard lifecycle?

Basically, if you need to maintain TextInput visible while typing - you can achieve that just by calling maybeScroll when onTextChanged is fired (of course with some debounce in order not to overuse CPU).

@VladyslavMartynov10
Copy link

VladyslavMartynov10 commented Sep 9, 2023

I'm not sure if iOS has this functionality out of the box, but it seems that only 'onStart' and 'onEnd' events are dispatched during the keyboard lifecycle. Thanks for the maybeScroll idea, I will investigate.

@kirillzyusko
Copy link
Owner

@VladyslavMartynov10 I investigated this topic a little bit more and it seems like iOS fires keyboardWillShow/keyboardDidShow events when user types a first symbol in TextInput:

Screen.Recording.2023-09-11.at.13.02.25.mov

I don't know why it happens in this way, but it seems like Apple engineers needed in this functionality 😅

So I'd suggest you to go with calling maybeScroll when onTextChanged event is fired (with debounce/throttle). In this case your focused TextInput will always remain above the keyboard even if the user scrolled it to invisible part of the screen.

@VladyslavMartynov10
Copy link

VladyslavMartynov10 commented Sep 11, 2023

@kirillzyusko Thanks!

Recently I've created the new Swift UI project in order to be sure that the problem lies in IOS implementation itself. This feature is not supported out of box, so we have to create our own logic. I don't know why Apple doesn't include it, sounds funny 😅.

So yeah maybeScroll is the most convenient solution in this case :)

@MarceloPrado
Copy link
Contributor Author

MarceloPrado commented Sep 19, 2023

@kirillzyusko sorry for the delay, if possible, let's try keeping this issue to the original thread to ensure we're all talking about the same thing 🙂

To recap for everyone, the existing KeyboardAwareScrollView adds a "dummy" Reanimated view at the bottom that grows in size relative to the keyboard progress. This is what enables the view to actually scroll once the keyboard is shown. However, there's a downside: if you need a vertically centered scroll view, this implementation causes your view to scroll even in unwanted cases as seen here.

@kirillzyusko I have to spend some time prototyping. One immediate approach I can think of is to add a top "dummy" view to counter the bottom one in this case. I'm not sure how the "syncing" would happen. Not very fond of this since it's easy to get messy.

One other way: when the keyboard is shown, I think it's valid to assume we don't want to "respect" the vertically centered layout. I wonder if we could de-activate flexGrow: 1/flex: 1 once progress > 0 ?

@kirillzyusko
Copy link
Owner

I have to spend some time prototyping. One immediate approach I can think of is to add a top "dummy" view to counter the bottom one in this case. I'm not sure how the "syncing" would happen. Not very fond of this since it's easy to get messy.

@MarceloPrado Cool idea. I think it will compensate the movement, but in casual scenarios (when you have a lot of TextInputs and they take more space than height of the screen) after keyboard is shown you'll be able to scroll to top and you will see this "fake" view.

One other way: when the keyboard is shown, I think it's valid to assume we don't want to "respect" the vertically centered layout. I wonder if we could de-activate flexGrow: 1/flex: 1 once progress > 0 ?

@MarceloPrado we can de-activate these styles by setting undefined and I think it could be a good option to try 👍 Need to experiment to see whether such approach is not causing additional problems (such as layout jump, etc.).

I've also tried to use react-native-keyboard-aware-scrollview to see whether this package has the same problem. And on iOS this package doesn't have this problem because they are setting contentInset (I haven't tested Android, but may assume, that this OS has the same problem).

Another approach that I was going to check was to use react-native-avoid-softinput and see how such layout is handled there and whether it has the same problem as described here 👀

@VladyslavMartynov10
Copy link

@kirillzyusko

Recently I was trying to migrate to new version 1.9.4 and lost scroll-to-focused input effect at least on Android. For now when the input is focused and under keyboard, maybeScrollCallback never fires.

I've realised that we've got a discussion before #168 (comment) with the support of this feature, but I think it should be handled somehow on native side in order to avoid multiple calls of the same callback in RN.

Any ideas how it can be achieved without reinventing the wheel & performance lost? Thanks beforehand 🙂!

@kirillzyusko
Copy link
Owner

Hello @VladyslavMartynov10 👋

Would you mind to create a new issue? I remember your problem (to keep focused input in visible area while user is typing) and I have some ideas on how to handle it on a KeyboardAwareScrollView level without adding code on user components level.

So, please, create a new issue and we will discuss an approach with you there (don't want to mix different problems in this single issue).

@VladyslavMartynov10
Copy link

Hello @VladyslavMartynov10 👋

Would you mind to create a new issue? I remember your problem (to keep focused input in visible area while user is typing) and I have some ideas on how to handle it on a KeyboardAwareScrollView level without adding code on user components level.

So, please, create a new issue and we will discuss an approach with you there (don't want to mix different problems in this single issue).

@kirillzyusko Did it

@kirillzyusko kirillzyusko added 📚 components Anything related to the exported components of this library and removed example Anything related to example project labels Dec 14, 2023
@kirillzyusko kirillzyusko added the sponsor 💖 Someone pays money for the issue to be resolved 💸 label Dec 29, 2023
@kirillzyusko
Copy link
Owner

Another workaround was discovered in #405 - you can specify minHeight so that the content in KeyboardAwareScrollView will not be resized. So code can look like:

const STATUS_BAR_HEIGHT = 44;
const HEADER_HEIGHT = 56;
const styles = StyleSheet.create({
  centered: {
    alignItems: "center",
    flex: 1,
    justifyContent: "center",
    minHeight:
      Dimensions.get("window").height - STATUS_BAR_HEIGHT - HEADER_HEIGHT, // <- fix is here
  },
  container: {
    flex: 1,
  },
  contentContainer: {
    backgroundColor: "#f7d7d7",
    flexGrow: 1,
  },
});

const Centered: FC<{ children: ReactNode }> = ({ children }) => (
  <View style={styles.centered}>{children}</View>
);

function randomColor() {
  return "#" + Math.random().toString(16).slice(-6);
}

export default function AwareScrollView() {
  useResizeMode();

  return (
    <KeyboardAwareScrollView
      contentContainerStyle={styles.contentContainer}
      style={styles.container}
    >
      <Centered>
        {new Array(4).fill(0).map((_, i) => (
          <TextInput
            key={i}
            placeholder={`${i}`}
            placeholderTextColor="black"
            style={{
              width: "100%",
              height: 50,
              backgroundColor: randomColor(),
              marginTop: 50,
            }}
          />
        ))}
      </Centered>
    </KeyboardAwareScrollView>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📚 components Anything related to the exported components of this library sponsor 💖 Someone pays money for the issue to be resolved 💸
Projects
None yet
Development

No branches or pull requests

4 participants