Animate UIView沿键盘出现动画

时间:2013-10-31 14:52:33

标签: ios animation uikeyboard

我正在使用UIKeyboardWillShowNotificationUIKeyboardWillHideNotification为键盘上的视图设置动画,使用UIKeyboardAnimationDurationUserInfoKeyUIKeyboardAnimationCurveUserInfoKeyUIKeyboardFrameEndUserInfoKey显示动画。

一切正常,只要元素开始位置在屏幕的底部。我的元素(屏幕截图中的输入框)从UITabBarController上方开始,所以如果我的动画开始,键盘和UITextField之间会有间隙,它会沿着动画收缩,直到它到达终点。

我正在寻找的是:“动画具有相同的动画曲线,但如果键盘达到我的最大位置,则开始移动”。

如果我要为启动动画添加延迟,那么缓动不正确,这可能会在未来的iOS版本中中断。

如果你与我分享你的想法会很棒。 : - )

Keyboard closed Keyboard opened Keyboard animating (see the gap

2 个答案:

答案 0 :(得分:14)

通常有两种方法可以将视图保持在键盘上方,因为它会动画到位。如您所知,第一个是监听UIKeyboardWillShowNotification并使用userData中的伴随持续时间/曲线/帧值来帮助您在键盘上方定位和设置视图动画。

第二种方法是为调用键盘的视图(此处为inputAccessoryView)提供UITextField(我意识到这不会提供你要求的效果,一旦键盘进入它就会“推”工具栏/文本字段。但稍后会更多。) iOS会将您的inputAccessoryView作为父级键盘并将它们组合在一起的视图的父级。根据我的经验,这提供了最好看的动画。我不认为我曾经使用UIKeyboardWillShowNotification方法获得完美动画,特别是现在在iOS7中,键盘动画结束时有一点反弹。 UIKit Dynamics可能还有一种方法可以将这种反弹应用到您的视图中,但是使其与键盘完美同步将很难。

以下是我过去为类似于您的方案所做的事情:在customView栏按钮项目中有一个UIToolbar位于底部的UITextField用于输入。在您的情况下,它位于UITabBar之上。 ITextField设置了自定义inputAccessoryView另一个 UIToolbar 另一个 UITextField

当用户点击文本字段并成为第一响应者时,键盘会随着第二个工具栏/文本字段一起动画到位(这个转换看起来非常好!)。当我们注意到这种情况发生时,我们将firstResponder从第一个文本字段转换到第二个文本字段,以便在键盘就位后它具有闪烁的插入符号。

诀窍是当你确定结束编辑的时间时要做什么。首先,你必须在第二个文本字段上使用resignFirstResponder,但如果你不小心,那么系统会将第一个响应者状态传回原始文本字段!所以你必须防止这种情况,因为否则你将处于向前传递第一个响应者的无限循环中,键盘将永远不会消失。其次,您需要将任何文本输入镜像到第二个文本字段。

以下是此方法的代码:

@implementation TSViewController
{
    IBOutlet UIToolbar*     _toolbar; // parented in your view somewhere

    IBOutlet UITextField*   _textField; // the customView of a UIBarButtonItem in the toolbar

    IBOutlet UIToolbar*     _inputAccessoryToolbar; // not parented.  just owned by the view controller.

    IBOutlet UITextField*   _inputAccessoryTextField; // the customView of a UIBarButtonItem in the inputAccessoryToolbar
}

- (void) viewDidLoad
{
    [super viewDidLoad];

    _textField.delegate = self;
    _inputAccessoryTextField.delegate = self;

    _textField.inputAccessoryView = _inputAccessoryToolbar;
}

- (void) textFieldDidBeginEditing: (UITextField *) textField
{
    if ( textField == _textField )
    {
        // can't change responder directly during textFieldDidBeginEditing.  postpone:
        dispatch_async(dispatch_get_main_queue(), ^{

            _inputAccessoryTextField.text = textField.text;

            [_inputAccessoryTextField becomeFirstResponder];
        });
    }
}

- (BOOL) textFieldShouldBeginEditing: (UITextField *) textField
{
    if ( textField == _textField )
    {
        // only become first responder if the inputAccessoryTextField isn't the first responder.
        return ![_inputAccessoryTextField isFirstResponder];
    }
    return YES;
}

- (void) textFieldDidEndEditing: (UITextField *) textField
{
    if ( textField == _inputAccessoryTextField )
    {
        _textField.text = textField.text;
    }
}

// invoke this when you want to dismiss the keyboard!
- (IBAction) done: (id) sender
{
    [_inputAccessoryTextField resignFirstResponder];
}

@end

我能想到最后一种可能性。上述方法具有两个独立工具栏/文本字段的缺点。你理想的只是其中的一组,你希望它看起来键盘“推”它们(或拉下它们)。实际上动画足够快,我认为大多数人都不会注意到上面的方法有两套,但也许你不喜欢它..

此最终方法侦听键盘显示/隐藏,并使用CADisplayLink同步动画工具栏/文本字段,因为它实时检测键盘位置的变化。在我的测试中它看起来很不错。我看到的主要缺点是工具栏的位置稍微滞后。我正在使用自动布局,转换到传统的帧定位可能会更快。另一个缺点是键盘视图层次结构的依赖性没有显着变化。这可能是最大的风险。

还有另外一个技巧。工具栏使用约束定位在我的故事板中。距离视图底部的距离有两个约束条件。一个绑定到IBOutlet“_toolbarBottomDistanceConstraint”,这是代码用于移动工具栏的内容。该约束是具有“等于”关系的“垂直空间”约束。我将优先级设置为500.存在第二个并行的“垂直空间”约束,其具有“大于或等于”关系。此常量是到视图底部的最小距离(例如,在标签栏上方),优先级为1000.有了这两个约束,我可以设置工具栏从底部到任意值的距离我喜欢,但它永远不会低于我的最低价值。这是使键盘正在推/拉工具栏的关键,但让它在某一点“掉落”动画。

最后,也许您可​​以将这种方法与您已经获得的方法混合使用:使用CADisplayLink回调来检测键盘何时“遇到”您的工具栏,然后手动定位工具栏以用于其余部分动画,使用真实的UIView动画将工具栏动画到位。您可以将持续时间设置为键盘显示动画持续时间减去已经发生的时间。

@implementation TSViewController
{
    IBOutlet UITextField*           _textField;

    IBOutlet UIToolbar*             _toolbar;

    IBOutlet NSLayoutConstraint*    _toolbarBottomDistanceConstraint;

    CADisplayLink*                  _displayLink;
}

- (void) dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver: self];
}

- (void) viewDidLoad
{
    [super viewDidLoad];

    [self.view addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector( dismiss:) ]];

    _textField.inputAccessoryView = [[UIView alloc] init];

    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(keyboardWillShowHide:)
                                                 name: UIKeyboardWillShowNotification
                                               object: nil];

    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(keyboardWillShowHide:)
                                                 name: UIKeyboardWillHideNotification
                                               object: nil];

    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(keyboardDidShowHide:)
                                                 name: UIKeyboardDidShowNotification
                                               object: nil];

    [[NSNotificationCenter defaultCenter] addObserver: self
                                             selector: @selector(keyboardDidShowHide:)
                                                 name: UIKeyboardDidHideNotification
                                               object: nil];
}

- (void) keyboardWillShowHide: (NSNotification*) n
{
    _displayLink = [CADisplayLink displayLinkWithTarget: self selector:  @selector( tick: )];
    [_displayLink addToRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
}

- (void) keyboardDidShowHide: (NSNotification*) n
{
    [_displayLink removeFromRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
}

- (void) tick: (CADisplayLink*) dl
{
    CGRect r = [_textField.inputAccessoryView.superview.layer.presentationLayer frame];
    r = [self.view convertRect: r fromView: _textField.inputAccessoryView.superview.superview];

    CGFloat fromBottom = self.view.bounds.size.height - r.origin.y;
    _toolbarBottomDistanceConstraint.constant = fromBottom;
}

- (IBAction) dismiss: (id) sender
{
    [self.view endEditing: YES];
}

@end

这是视图层次结构和约束:

enter image description here

答案 1 :(得分:1)

我没有汤姆提供的详细解决方案,但我确实有一个想法,你可以玩。我一直在做很多有趣的事情,包括自动布局和约束,你可以做一些很棒的事情。请注意,您不能将滚动视图中的项目限制在一个项目中。

所以你有了主视图,我假设它是一个表视图或scrollView中的其他视图,所以你必须处理它。我建议的方法是拍摄视图的快照,将当前视图保存在ivar(您的表格)中,并将其替换为锚定在底部的“非常高的容器视图”,将包含快照的UIImageView放入这个视图,它与容器视图之间的约束为constant = 0。对用户没有任何改变。

在“inputAccessoryView”中,当视图添加到superView时(并且当存在窗口属性时),您可以删除图像和容器视图之间的约束,并添加一个新的约束底部inputAccessoryView顶部的文本字段,其距离必须大于某个值。您必须四处寻找值,因为它将是您为任何contentValue调整的scrollView中的textField的偏移量。然后,您很可能必须将该约束添加到窗口(保留一个ivar,以便稍后将其删除)。

过去我玩过键盘,你可以看到它被添加到窗口,其帧偏移因此它刚好低于屏幕底部(在iOS5中) - 它不在scrollView中

当键盘完成滚动时,您可以看到图像视图滚动的位置,确定偏移,然后从图像视图切换回真实的滚动视图。

请注意,我确实做了这个快照,动画,最后非常成功地替换过去的视图。你将花费一些时间在这上面,也许它会工作,也许不会 - 但如果你把一个简单的演示项目放在一起,你可以快速验证你是否可以在容器视图中获取imageView本身以使用键盘输入附件上的约束来移动视图。一旦有效,你可以“真实地”做到这一点。

编辑:正如Tom Swift指出的那样,键盘位于另一个更高“Z”级别的窗口中,因此无法直接连接真实键盘和用户视图之间的约束。

然而 - 在键盘通知中,我们可以得到它的大小,动画持续时间,甚至是动画曲线。因此,当您获得第一个键盘通知时,请创建一个新的透明视图并将其放置在其顶部位于特殊“imageView”(快照)视图的底部。使用键盘长度和曲线的UIView动画,透明视图将完全像键盘动画一样动画 - 但在窗口中。将约束放在透明视图上,您​​应该能够实现所需的确切行为(在iOS6中)。真的,此时支持iOS 5 - 对于尚未升级的10个人?!?!?。

如果你必须支持iOS5,并且你想要这种“碰撞”行为,那么计算动画何时达到“击中”你的textField的大小,当键盘开始移动时使用UIView动画有延迟,所以它不会立即开始移动,但当它移动时,它会跟踪键盘。