Захват касаний на подвью вне рамки его надзора с помощью hitTest: withEvent:
Моя проблема: У меня есть супервизор EditView
, который занимает в основном весь кадр приложения, а subview MenuView
, который занимает только нижний ~ 20%, а затем MenuView
содержит свой собственный subview ButtonView
, который фактически находится за пределами границ MenuView
(примерно так: ButtonView.frame.origin.y = -100
).
(примечание: EditView
имеет другие подзапросы, которые не являются частью иерархии представления MenuView
, но могут повлиять на ответ.)
Вероятно, вы уже знаете проблему: когда ButtonView
находится в пределах MenuView
(или, более конкретно, когда мои касания находятся в пределах MenuView
), ButtonView
отвечает на события касания. Когда мои касания находятся за пределами границ MenuView
(но все еще в пределах ButtonView
), событие ButtonView
не получает событие касания.
Пример:
- (E)
EditView
, родительский элемент всех представлений
- (M)
MenuView
, подзаголовок EditView
- (B) -
ButtonView
, подвид меню MenuView
Диаграмма:
+------------------------------+
|E |
| |
| |
| |
| |
|+-----+ |
||B | |
|+-----+ |
|+----------------------------+|
||M ||
|| ||
|+----------------------------+|
+------------------------------+
Поскольку (B) находится снаружи (M), кратковременное нажатие в области (B) никогда не будет отправлено на (M) - на самом деле, (M) никогда не анализирует прикосновение в этом случае, и прикосновение посылается к следующему объекту в иерархии.
Цель: Я понимаю, что переопределение hitTest:withEvent:
может решить эту проблему, но я точно не понимаю. В моем случае, если hitTest:withEvent:
будет переопределено в EditView
(мой "супервизор" )? Или он должен быть переопределен в MenuView
, прямой просмотр кнопки, которая не получает штрихи? Или я думаю об этом неправильно?
Если для этого требуется длинное объяснение, полезный онлайн-ресурс будет полезен - за исключением документов Apple UIView, которые не дали мне понять.
Спасибо!
Ответы
Ответ 1
Я изменил принятый код ответа на более общий - он обрабатывает случаи, когда представление делает подвидные клики на своих границах, может быть скрыто и, что более важно: если subviews представляют собой сложные иерархии представлений, правильное подзапрос будет вернулся.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.clipsToBounds) {
return nil;
}
if (self.hidden) {
return nil;
}
if (self.alpha == 0) {
return nil;
}
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result) {
return result;
}
}
return nil;
}
SWIFT 3
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if clipsToBounds || isHidden || alpha == 0 {
return nil
}
for subview in subviews.reversed() {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
Я надеюсь, что это поможет любому, кто пытается использовать это решение для более сложных случаев использования.
Ответ 2
Хорошо, я немного поработал и тестировал, как работает hitTest:withEvent
- по крайней мере, на высоком уровне. Выполните этот сценарий:
- (E) - EditView, родительский элемент всех представлений
- (M) - это MenuView, подзаголовок EditView
- (B) - ButtonView, подзаголовок MenuView
Диаграмма:
+------------------------------+
|E |
| |
| |
| |
| |
|+-----+ |
||B | |
|+-----+ |
|+----------------------------+|
||M ||
|| ||
|+----------------------------+|
+------------------------------+
Поскольку (B) находится снаружи (M), кратковременное нажатие в области (B) никогда не будет отправлено на (M) - на самом деле, (M) никогда не анализирует прикосновение в этом случае, и прикосновение посылается к следующему объекту в иерархии.
Однако, если вы реализуете hitTest:withEvent:
в (M), метки в любом месте приложения будут отправлены в (M) (или, по крайней мере, он знает о них). Вы можете написать код для обработки касания в этом случае и вернуть объект, который должен получить прикосновение.
В частности: цель hitTest:withEvent:
- вернуть объект, который должен получить удар. Итак, в (M) вы можете написать код следующим образом:
// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *subview in self.subviews) {
if (CGRectContainsPoint(subview.frame, point)) {
return subview;
}
}
// use this to pass the 'touch' onward in case no subviews trigger the touch
return [super hitTest:point withEvent:event];
}
Я все еще очень новичок в этом методе и этой проблеме, поэтому, если есть более эффективные или правильные способы написания кода, прокомментируйте.
Я надеюсь, что это поможет кому-то еще, кто обратится к этому вопросу позже.:)
Ответ 3
В Swift 4
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return nil
}
Ответ 4
Что бы я сделал, так это то, что ButtonView и MenuView существуют на одном уровне в иерархии представлений, помещая их как в контейнер, чей фрейм полностью соответствует их обоим. Таким образом, интерактивная область обрезанного элемента не будет игнорироваться из-за границ надзора.
Ответ 5
Если кому-то это нужно, вот быстрая альтернатива
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
for subview in self.subviews.reverse() {
let subPoint = subview.convertPoint(point, fromView:self);
if let result = subview.hitTest(subPoint, withEvent:event) {
return result;
}
}
}
return nil
}
Ответ 6
Если у вас есть много других объектов в представлении родителя, то, вероятно, большинство других интерактивных представлений не будут работать, если вы используете вышеуказанные решения, в этом случае вы можете использовать что-то вроде этого (в Swift 3.2):
class BoundingSubviewsViewExtension: UIView {
@IBOutlet var targetView: UIView!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Convert the point to the target view coordinate system.
// The target view isn't necessarily the immediate subview
let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
if (targetView?.bounds.contains(pointForTargetView!))! {
// The target view may have its view hierarchy,
// so call its hitTest method to return the right hit-test view
return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
}
return super.hitTest(point, with: event)
}
}
Ответ 7
Поместите ниже строки кода в иерархию вашего представления:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
[self.superview bringSubviewToFront:self];
}
return hitView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
CGRect rect = self.bounds;
BOOL isInside = CGRectContainsPoint(rect, point);
if(!isInside)
{
for (UIView *view in self.subviews)
{
isInside = CGRectContainsPoint(view.frame, point);
if(isInside)
break;
}
}
return isInside;
}
Для более подробного объяснения в моем блоге было объяснено: "goaheadwithiphonetech" в отношении "Пользовательская выноска: кнопка не является проблемой для кликов".
Я надеюсь, что это поможет вам...!!!