drogel / keyboard_attachable

A Flutter package to build widgets that can be attached to the soft keyboard.
MIT License
58 stars 24 forks source link

Add builder suggestions #4

Closed YeungKC closed 4 years ago

YeungKC commented 4 years ago

hi, I think this is very good, thank you very much, but I think the current use is limited, maybe it can be done like this: KeyboardAttachable (builder: (double keyboardHeight) { return someWidget (...); }) What do you think?

drogel commented 4 years ago

Hi @YeungKC, thank you for using the package and for taking the time to open this suggestion, I really appreciate it.

I have been taking a look at the code, and I think it would be completely reasonable to add some kind of builder method to KeyboardAttachable to let the consumer have more control over the animations. So I wonder what is your use case? What would you use such a builder for? I am asking you this because two cases come to mind:

KeyboardAttachable (transitionBuilder: (BuildContext context, Animation<double> keyboardAnimation) {
    return someWidget (...);
})

where keyboardAnimation would run from 0 (dismissed keyboard) to 1 (shown keyboard). That way you would be able to use the keyboard animation for any widget returned by the builder. If you wanted to know the exact keyboard height for each frame, then all you would need to do is multiply MediaQuery.of(context).viewInsets.bottom by keyboardAnimation.value.

Hope that clarifies a bit the approach I am trying to take from your suggestion. What do you think? Is that something that would be useful for you?

YeungKC commented 4 years ago

I am sorry I did not elaborate: KeyboardAttachable (builder: (double keyboardHeight) { return someWidget (...); }) I hope keyboardHeight can return min height ~ max height, he will call back many times.

This will make him more flexible to use. Sometimes my TextField is not at the bottom, and sometimes it needs to be at a certain height to start matching the offset.

    Column(
      children: [
        TextFormField(), //  I want this widget to be above the keyboard when the keyboard pops up.
        SomeWidget(),
      ],
    );

and

maybe in iOS can use keyboardWillShowNotification, android in can use https://developer.android.com/preview/features#control-ime

using native interaction will make the effect even better!

I only use Android, if necessary, I can help access native interaction.

I don't know if I can make you understand that my English is poor.

drogel commented 4 years ago

Okay, so just to be clear, I understand that you have a TextField which is not at the bottom of the screen, and you want it to stick to the soft keyboard, and, naturally, you want the TextField to start moving up as soon as the keyboard "hits" the TextField, right?

If that is the case, and since you can already access the min (zero) and max (MediaQuery.of(context).viewInsets.bottom) height of the keyboard, I guess that you need to know the height of the keyboard for every frame the keyboard animation is running, right?

Regarding the native keyboard notifications, keyboard_attachabe is already built upon the keyboard_visibility package, which uses the native notifications you mention to notify about soft keyboard events. If what you mean is that it is possible to use the services you mention to get the exact animation curves for every platform, that is something that I did not know, and I would love to know how to handle that! :-)

YeungKC commented 4 years ago

It is completely correct. It is difficult to achieve perfect results just by knowing whether to display the display keyboard. I want to know the height of each frame, then it can be done easily, no matter what the situation. If we use keyboardWillShowNotification to get the curve and duration and height of the animation, we can call back the height of each frame by calculation. This is the information I found https://developer.apple.com/documentation/uikit/uiresponder/1621576-keyboardwillshownotification

drogel commented 4 years ago

Okay, I see what you mean now! And I completely agree with you, it should be easy to implement, and I think it would be a very useful enhancement. I am going to work on it as soon as I have some free time.

Regarding the native services for capturing the keyboard animation curves, definitely worth taking a look at the documentation again, and see if there is something I missed there when I first built the package.

Thank you for the suggestion, I will let you know when this feature is ready!

YeungKC commented 4 years ago

I'm so happy ~~ This improvement is not simulated animation, but native feedback, which will be great. Even, I think flutter should be built in.

After you finish, I can help to complete the android part.

drogel commented 4 years ago

Hey, I just wanted to give you a quick update on this issue.

Today I worked a bit on the builder you suggested for keyboard_attacher, and I came up with a method that you can pass to KeyboardAttacher to have more control over the animations that are involved in the keyboard hiding and showing.

The method is called keyboardTransitionBuilder and it is of type

Widget Function(Widget child, Animation<double> animation)

It will give you access to the child that you passed as the child parameter, and to the animation object that contains the information of the keyboard hiding and showing, very similar to the AnimatedSwitcher widget. This way you can add your own logic to the transition. It can be used like this:

KeyboardAttachable(
    backgroundColor: Colors.blue,
    keyboardTransitionBuilder: _transitionBuilder
    child: MyWidget(),
);

Widget _transitionBuilder(Widget child, Animation<double> animation) {
    return SizeTransition(
        sizeFactor: animation,
        child: Container(height: 100),
    );
}

This would add a container that would expand to a height of 100 when the keyboard shows and hides, matching the keyboard animation curve.

Here is a full example that I wrote for you, you can paste into a project importing the package:

import 'package:flutter/material.dart';
import 'package:keyboard_attachable/keyboard_attachable.dart';

void main() => runApp(Example());

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Keyboard Attachable Demo',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(primarySwatch: Colors.blue),
        home: KeyboardAttachablePage(),
      );
}

/// Builds a [Scaffold] that lays out a footer at the bottom of the page.
class KeyboardAttachablePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: AppBar(title: const Text("Keyboard Attachable demo")),
        body: FooterLayout(
          footer: KeyboardAttachableFooter(),
          child: ColorsList(),
        ),
      );
}

/// Builds a footer that animates its bottom space when the keyboard is shown.
class KeyboardAttachableFooter extends StatelessWidget {
  @override
  Widget build(BuildContext context) => KeyboardAttachable(
        backgroundColor: Colors.blue,
        keyboardTransitionBuilder: (child, animation) => Column(
          children: <Widget>[
            child,
            SizeTransition(
              sizeFactor: Tween<double>(begin: 1, end: 0).animate(animation),
              child: Container(
                height: 100,
                child: Center(
                  child: FadeTransition(
                    opacity: Tween<double>(begin: 1, end: 0).animate(animation),
                    child: Text(
                      "I am afraid of the keyboard!",
                      style: Theme.of(context).textTheme.headline5,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
        child: Container(
          padding: const EdgeInsets.all(16),
          color: Colors.blue,
          child: TextField(
            decoration: InputDecoration(
              hintText: "Tap me!",
              fillColor: Colors.white,
              filled: true,
              border: const OutlineInputBorder(),
            ),
          ),
        ),
      );
}

/// Builds a [ListView] made of colored containers that fill the page.
class ColorsList extends StatelessWidget {
  @override
  Widget build(BuildContext context) => ListView.builder(
        itemExtent: 66,
        itemCount: Colors.primaries.length,
        itemBuilder: (_, i) => Container(
          color: Colors.primaries[i].withOpacity(0.2),
        ),
      );
}

And it would look like this!

keyboard_attachable transition

I have also made the dismiss animation go a bit faster for Android, I think it matches the keyboard better.

Right now, this change is in a branch called transition_builder in the project, you can switch to that branch and test it yourself, to see if it suits your needs. Once I test what I need to test and document the changes, I bring that to the main branch if all goes good!

YeungKC commented 4 years ago

Thank you very much.

But I'm sorry I don't like this. It look is good, but it's not elegant ... it's easy to mess up the hierarchy.

I still think that the result of yesterday ’s discussion was the best solution.

drogel commented 4 years ago

Don't worry! That is why I did not merge any of this yet, this is just a proposal for the improvement.

What is it you don't like? The builder-child approach? It is a similar approach to AnimatedSwitcher, so it will be familiar to developers that have used the Animated widgets in other projects. Do you think it would be better not to have a child parameter at all, and simply have the keyboardTransitionBuilder only?

Regarding the curve capture from native services, that is something that I was not proposing to merge from this branch, that is something that I will research later, but first I am interested in having the builder set up :-)

YeungKC commented 4 years ago

AnimatedSwitcher doesn't have animation parameter callback, and I can't change the duration and curve, and I don't need animation parameter callback ... just need every frame size.

I always think that these things should become simpler, do not need to care about the user through padding? positioned? To achieve the effect they want, just give the builder callback.

Do you agree?

drogel commented 4 years ago

I am sorry but I don't understand what you mean, AnimatedSwitcher does have a transitionBuilder callback parameter of type Widget Function(Widget child, Animation<double> animation), and you can change its curve or duration. From the AnimatedSwitcher documentation:

transitionBuilder → AnimatedSwitcherTransitionBuilder A function that wraps a new child with an animation that transitions the child in when the animation runs in the forward direction and out when the animation runs in the reverse direction.

That is the exact same approach I took with KeyboardAttacher, I think it is better to mantain consistency among widgets, and this is a common pattern for animated widgets from the Flutter libraries.

The good thing about this approach is that you can simply pass a child to KeyboardSwitcher if you do not care about the animations, and you can tune your transitions further with keyboardTransitionBuilder if you want more control. This separates the behaviors quite neatly, and it is elegant because you don't need to know about specific height values that might be different for several devices.

If you want to know the specific height value of the keyboard you can always call MediaQuery.of(context).viewInsets.bottom, so I don't see the need in adding that to the builder.

Could you provide a minimal example in which I can see clearly why is it better to pass the keyboard height in the callback?

YeungKC commented 4 years ago

I'm sorry I ignored the TransitionBuilder of AnimatedSwitcher, but I think that it is not flexible enough because I only need the height of each frame. As for how I cooperate with other widgets, this is the user's business, lib only needs to provide the simplest functions That's it. for example:

Column(
      children: [
        KeyboardAttachable(
            builder: (height) {
              BlocProvider.of<KeyboardBloc>(context)
                  .add(KeyboardHeightChangeEvent(height));
              return Column(
                children: <Widget>[
                  Container(
                    padding: EdgeInsets.only(
                      bottom: max(0, height - bottomHeight),
                    ), // bottomHeight default = 0
                    child: TextField(),
                  ),
                ],
              );
            },
          ),
        MeasureSize(
          onChanged: (size) {
            bottomHeiget = size.height;
          },
          child: Container(
            // some widget
          ),
        ),
      ],
    );

Then the widget of rebuilder can be controlled more finely.

drogel commented 4 years ago

Okay, I see, I can pass the height from the KeyboardAttacher builder, but I am not sure if that would be called and updated for every frame... What would happen if in your snippet you computed the height of the keyboard from the animation value? Like this:

Column(
      children: [
        KeyboardAttachable(
            builder: (child, animation) {
              final height = animation.value * MediaQuery.of(context).viewInsets.bottom;
              BlocProvider.of<KeyboardBloc>(context)
                  .add(KeyboardHeightChangeEvent(height));
              return Column(
                children: <Widget>[
                  Container(
                    padding: EdgeInsets.only(
                      bottom: max(0, height - bottomHeight),
                    ), // bottomHeight default = 0
                    child: TextField(),
                  ),
                ],
              );
            },
          ),
        MeasureSize(
          onChanged: (size) {
            bottomHeiget = size.height;
          },
          child: Container(
            // some widget
          ),
        ),
      ],
    );

It looks to me like you are the one who is updating the view by calling BlocProvider.of<KeyboardBloc>(context).add(KeyboardHeightChangeEvent(height)); in the builder, right?

Let me give it a thought and play around with it, I think a common ground can be found between both approaches.

YeungKC commented 4 years ago

Yes, I can do it, but ... My original intention was to interact with native to get a more realistic animation curve and return every frame ...

drogel commented 4 years ago

Yeah, that would be optimal, but my intention is to use the private animation controllers set up in the package as wrappers for the native animation curves, if we can get the exact animations from the native platforms.

But the first step is to get the builder right :-)

So, all in all, do you think a builder like this would be the way to go?

KeyboardAttachable(
    backgroundColor: Colors.blue,
    keyboardTransitionBuilder: (child, animation, keyboardHeight) {
         // Some widgets here...
    }
    child: MyWidget(),
);

I am sorry it took so long, but it has been an interesting discussion! We are almost there :-)

YeungKC commented 4 years ago

look is good, but if there is keyboardHeight, animation is not necessary. I don't quite understand why you want to use animation? According to the iOS documentation, animation can be obtained according to native, But in Android 11, it is not possible in this way, he will only return the rect of each frame. https://developer.android.com/preview/features#control-ime

If you want to access native in the future to improve better results, you should refer to the callback difference between the two ends.