UIView поверх клавиатуры, похожей на приложение iMessage
В настоящее время я пытаюсь в основном реализовать и точную копию Apples iMessage App.
Это означает, что мне нужен UITextView, который закреплен в нижней части экрана и перемещается вверх, когда он становится firstResponder. - Это довольно легко на самом деле. Существует несколько способов сделать это, и два из наиболее распространенных, конечно, анимируют представление вверх или вниз, если получено уведомление. Другой заключается в том, чтобы сделать это через inputAccessoryView. К сожалению, некоторые функции у одного есть, у другого нет. И они кажутся взаимоисключающими.
Большая проблема - вращение.
Я пробежал примерно по крайней мере семь различных проектов github, каждый из которых повторно реализовывал ту же функциональность / поведение, которого я пытаюсь достичь, но буквально все они терпели неудачу с треском.
Например, HPGrowingTextView, который используют официальные приложения Facebook/FacebookMessenger/(и, возможно, WhatsApp), представляет собой один большой кусок нежелательного кода. Возьмите iDevice, откройте приложение Facebook, перейдите в чат, нажмите на клавиатуру и поверните устройство. Если вы обратите внимание, вы заметите, что панель ввода слегка подпрыгивает и оставляет некоторое пространство между рамкой клавиатуры и ее собственной. Затем посмотрите на реализацию Apples в iMessage, когда клавиатура отображается. Это идеально.
Кроме этого, взлом содержимого Offset и EdgeInset, которые использует библиотека HPGrowingTextView, дает мне кошмары.
Поэтому я хотел сделать это сам и начать с нуля.
Прямо сейчас у меня есть очень гладкая, элегантная и беззащитная реализация растущего UITextView, но одна часть отсутствует.
Элегантное вращение.
Когда я просто подгоняю кадры к их соответствующим новым позициям в методе willRotateToInterfaceOrientation:duration: все в итоге работает отлично, НО у меня та же проблема, что и у HPGrowingTextView(см. Приложение Facebook). Небольшое пространство между входным обзором и клавиатурой во время вращения.
Я обнаружил, что при повороте устройства в альбомную ориентацию портретная клавиатура, которая отображается в данный момент, не "трансформируется", а скорее исчезает (отправляет уведомление "willHide"), и вновь появляется альбомная версия (отправляя уведомление "willShow"). Переход является очень тонким исчезновением и, возможно, некоторым изменением размера.
Я повторно реализовал свой проект, используя inputAccessoryView, чтобы увидеть, что происходит потом, и я был приятно удивлен. InputAccessoryView вращается в идеальной синхронизации с клавиатурой. Там нет места / промежуток между двумя.
К сожалению, мне еще предстоит придумать идею, как разместить док-станцию inputAccessoryView в нижней части экрана и НЕ исчезать / не выходить из нее рядом с клавиатурой...
Чего я не хочу, так это хакерских решений, таких как... "немного уменьшив фрейм в CoordinateSystem toInterfaceOrientation, а затем переместив его обратно вверх, когда был вызван didRotateFrom...".
Я знаю еще одно приложение, которому удалось реализовать такое поведение, и это "Kik Messenger".
У кого-нибудь есть идея, совет или ссылка, которую я еще не видел по этой теме?
Огромное спасибо!
Примечание: как только эта проблема будет решена, я открою проект с открытым исходным кодом для всех, чтобы получить прибыль, потому что почти каждая реализация, которую я смог найти в течение последних нескольких дней, - беспорядок.
5 ответов
Я преуспел в решении проблемы довольно элегантным способом (я думаю,...).
Код будет выпущен на Github на следующей неделе и связан с этим ответом.
-
Как это сделано: я сделал ротацию, выбрав inputAccessoryView-способ сделать это.
Номенклатура:
- MessageInputView - это UIView, содержащий мой "GrowingUITextView" (он также содержит кнопку "Отправить" и фоновое изображение).
- "ChatView" - это представление, которое принадлежит ChatViewController, которое отображает все Chatbubbles и имеет мой "MessageInputView", прикрепленный внизу.
- 'keyboardAccessoryView' - это пустой размер UIView: CGRect(0,0,0,0).
Мне нужно было выяснить, как заставить MessageInputView держаться на экране, когда клавиатура была закрыта. Это была сложная часть. Я сделал это, создав другое представление (keyboardAccessoryView), и мой GrowingUITextView использовал его в качестве своего inputAccessoryView. Я сохранил Keyboard AccessoryView, потому что мне понадобится ссылка на него позже.
Затем я вспомнил кое-что из того, что сделал в другой попытке (анимация рамок MessageInputView по всему экрану при получении уведомления от клавиатуры).
Я добавил свой MessageInputView в качестве подпредставления к моему ChatView (в самом низу). Всякий раз, когда он активируется и методы willShow: вызываются уведомлением с клавиатуры, я вручную анимирую кадр MessageInputView в его назначенную позицию вверху. Когда анимация заканчивается и блок завершения выполняется, я удаляю подпредставление из ChatView и добавляю его в keyboardAccessoryView. Это приводит к тому, что другое уведомление запускается, потому что клавиатура перезагружается КАЖДЫЙ раз, когда изменяются фрейм / границы inputAccessoryView!. Вы должны знать об этом и обращаться с этим соответствующим образом!
Когда клавиатура собирается закрыться, я конвертирую кадр моего MessageInputView в систему координат моего ChatView и добавляю его как подпредставление. Таким образом он удаляется из моей клавиатуры AccessView. Затем я изменяю размер кадра keyboardAccessoryView обратно на CGRect (0,0,0,0), потому что в противном случае UIViewAnimationDuration не будет совпадать! Затем я позволяю отклонить клавиатуру, и мой MessageInputView следует за ней сверху и в конце закрепляется внизу экрана.
Это довольно много работы, но очень мало.
-
Береги себя.
PS: Если кто-то найдет более простой способ сделать это (идеально), дайте мне знать.
Недавно я столкнулся с той же проблемой, и мне пришлось создать собственное решение, так как я был не совсем доволен доступными сторонними библиотеками. Я разделил эту реализацию на собственный проект GitHub:
После некоторого простого тестирования на симуляторах iOS 6.1 7 и 8, кажется, что вращения правильно следуют за клавиатурой. Представление также будет расти вместе с текстом и автоматически изменять его размер при повороте.
Вы можете использовать очень простой init
Функция так, чтобы создать его с шириной экрана и высотой по умолчанию, например:
self.messageComposerView = [[MessageComposerView alloc] init];
self.messageComposerView.delegate = self;
[self.view addSubview:self.messageComposerView];
Существует также несколько других инициализаторов, которые позволяют настроить рамку, смещение клавиатуры и максимальную высоту просмотра текста. Смотрите readme для больше!
Вот UITextView
подкласс, который работает должным образом на iOS 9.3.1 и 8.3.1. Он заботится о росте и сжатии с ограничениями, сохраняя при этом каретку всегда в нужном месте и плавно анимируя.
Накладывать вид на клавиатуру тривиально, многие решения легко найти, так что это не покрыто...
Я не мог найти готовых решений, готовых к производству, поэтому закончил работать над этим с нуля. Я должен был решить много маленьких проблем на этом пути.
Комментарии к коду должны дать вам представление о том, что происходит.
Я поделился этим на своем Github.
Заметки
- Не протестировано для поддержки ландшафта
- Не тестировался на i6+
демонстрация
(после того, как максимальная высота элемента становится прокручиваемой. Забыл перетащить демо, но это также работает как ожидалось...)
Подкласс
class ruuiDynamicTextView: UITextView {
var dynamicDelegate: ruuiDynamicTextViewDelegate?
var minHeight: CGFloat!
var maxHeight: CGFloat?
private var contentOffsetCenterY: CGFloat!
init(frame: CGRect, offset: CGFloat = 0.0) {
super.init(frame: frame, textContainer: nil)
minHeight = frame.size.height
//center first line
let size = self.sizeThatFits(CGSizeMake(self.bounds.size.width, CGFloat.max))
contentOffsetCenterY = (-(frame.size.height - size.height * self.zoomScale) / 2.0) + offset
//listen for text changes
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(textChanged), name: UITextViewTextDidChangeNotification, object: nil)
//update offsets
layoutSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
//Use content size if more than min size, compensate for Y offset
var height = max(self.contentSize.height - (contentOffsetCenterY * 2.0), minHeight)
var updateContentOffsetY: CGFloat?
//Max Height
if maxHeight != nil && height > maxHeight {
//Cap at maxHeight
height = maxHeight!
} else {
//constrain Y to prevent odd skip and center content to view.
updateContentOffsetY = contentOffsetCenterY
}
//update frame if needed & notify delegate
if self.frame.size.height != height {
self.frame.size.height = height
dynamicDelegate?.dynamicTextViewDidResizeHeight(self, height: height)
}
//constrain Y must be done after setting frame
if updateContentOffsetY != nil {
self.contentOffset.y = updateContentOffsetY!
}
}
func textChanged() {
let caretRect = self.caretRectForPosition(self.selectedTextRange!.start)
let overflow = caretRect.size.height + caretRect.origin.y - (self.contentOffset.y + self.bounds.size.height - self.contentInset.bottom - self.contentInset.top)
if overflow > 0 {
//Fix wrong offset when cursor jumps to next line un explisitly
let seekEndY = self.contentSize.height - self.bounds.size.height
if self.contentOffset.y != seekEndY {
self.contentOffset.y = seekEndY
}
}
}
}
protocol ruuiDynamicTextViewDelegate {
func dynamicTextViewDidResizeHeight(textview: ruuiDynamicTextView, height: CGFloat)
}
Как мне решить эту проблему для меня:
я имею ChatViewController
а также FooterViewController
как UIContainerView
, Кроме того, у меня есть выход ContentView в FooterViewController
, Затем в ChatViewController
Я имею:
override func becomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView? {
if let childViewController = childViewControllers.first as? FooterViewController {
childViewController.contentView.removeFromSuperview()
return childViewController.contentView
}
return nil
}
Другой способ - создать представление программно и вернуть как inputAccessoryView.
Недавно я написал сообщение в блоге об этой конкретной проблеме, которую вы описали, и о том, как решить ее коротким и элегантным способом с помощью клавиатурных уведомлений, но без использования inputAccessoryView
, И хотя этот вопрос довольно старый, эта тема все еще актуальна, поэтому вот ссылка на пост: Синхронизация анимации вращения между клавиатурой и приложенным представлением
Если вы не хотите углубляться в подробное объяснение, описанное в сообщении в блоге, вот краткое описание с примером кода:
Основной принцип состоит в том, чтобы использовать тот же метод, который используют все - наблюдение за клавиатурными уведомлениями для анимации прикрепленного вида вверх и вниз. Но в дополнение к этому вы должны отменить эти анимации, когда клавиатурные уведомления запускаются вследствие изменения ориентации интерфейса.
Пример поворота без пользовательской отмены анимации при изменении ориентации интерфейса:
Пример поворота с отменой анимации при изменении ориентации интерфейса:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(adjustViewForKeyboardNotification:)
name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(adjustViewForKeyboardNotification:)
name:UIKeyboardWillHideNotification object:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[[NSNotificationCenter defaultCenter]
removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
self.animatingRotation = YES;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
self.animatingRotation = NO;
}
- (void)adjustViewForKeyboardNotification:(NSNotification *)notification {
NSDictionary *notificationInfo = [notification userInfo];
// Get the end frame of the keyboard in screen coordinates.
CGRect finalKeyboardFrame = [[notificationInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
// Convert the finalKeyboardFrame to view coordinates to take into account any rotation
// factors applied to the window’s contents as a result of interface orientation changes.
finalKeyboardFrame = [self.view convertRect:finalKeyboardFrame fromView:self.view.window];
// Calculate new position of the commentBar
CGRect commentBarFrame = self.commentBar.frame;
commentBarFrame.origin.y = finalKeyboardFrame.origin.y - commentBarFrame.size.height;
// Update tableView height.
CGRect tableViewFrame = self.tableView.frame;
tableViewFrame.size.height = commentBarFrame.origin.y;
if (!self.animatingRotation) {
// Get the animation curve and duration
UIViewAnimationCurve animationCurve = (UIViewAnimationCurve) [[notificationInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
NSTimeInterval animationDuration = [[notificationInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// Animate view size synchronously with the appearance of the keyboard.
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:animationDuration];
[UIView setAnimationCurve:animationCurve];
[UIView setAnimationBeginsFromCurrentState:YES];
self.commentBar.frame = commentBarFrame;
self.tableView.frame = tableViewFrame;
[UIView commitAnimations];
} else {
self.commentBar.frame = commentBarFrame;
self.tableView.frame = tableViewFrame;
}
}