Захват касается подпредставления вне кадра его суперпредставления, используя hitTest:withEvent:
Моя проблема: у меня есть супервизия EditView
которая занимает в основном весь фрейм приложения и подпредставление MenuView
который занимает только дно ~20%, а затем MenuView
содержит свое собственное подпредставление ButtonView
который на самом деле проживает за пределами MenuView
границы (что-то вроде этого: ButtonView.frame.origin.y = -100
).
(нота: EditView
имеет другие подпредставления, которые не являются частью MenuView
Просмотр иерархии, но может повлиять на ответ.)
Вы, наверное, уже знаете проблему: когда ButtonView
находится в пределах MenuView
(или, более конкретно, когда мои прикосновения находятся в пределах MenuView
границы), ButtonView
реагирует на сенсорные события. Когда мои прикосновения находятся за пределами MenuView
границы (но все еще в пределах ButtonView
s), событие касания не получено ButtonView
,
Пример:
- (E)
EditView
Родитель всех взглядов - (М)
MenuView
, подпредставление EditView - (Б) является
ButtonView
, подпредставление MenuView
Диаграмма:
+------------------------------+
|E |
| |
| |
| |
| |
|+-----+ |
||B | |
|+-----+ |
|+----------------------------+|
||M ||
|| ||
|+----------------------------+|
+------------------------------+
Поскольку (B) находится за пределами кадра (M), касание в области (B) никогда не будет отправлено (M) - фактически, (M) никогда не анализирует касание в этом случае, и касание отправляется следующий объект в иерархии.
Цель: я понимаю, что это главное hitTest:withEvent:
может решить эту проблему, но я не понимаю, как именно. В моем случае следует hitTest:withEvent:
быть переопределенным в EditView
(мой "главный" супервизор)? Или это должно быть переопределено в MenuView
, прямое наблюдение за кнопкой, которая не получает прикосновения? Или я думаю об этом неправильно?
Если это требует длинного объяснения, хороший интернет-ресурс был бы полезен - за исключением документов Apple UIView, которые не дали мне понять.
Спасибо!
8 ответов
Я изменил принятый код ответа, чтобы он был более универсальным - он обрабатывает случаи, когда представление обрезает подпредставления до его границ, может быть скрытым, и, что более важно: если подпредставления представляют собой сложные иерархии представлений, будет возвращено правильное подпредставление.
- (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
}
Я надеюсь, что это поможет любому, кто пытается использовать это решение для более сложных случаев использования.
Хорошо, я немного покопался и протестировал, вот как 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];
}
Я все еще очень плохо знаком с этим методом и этой проблемой, поэтому, если есть более эффективные или правильные способы написания кода, пожалуйста, прокомментируйте.
Я надеюсь, что это поможет любому, кто ответит на этот вопрос позже.:)
В 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
}
Я хотел бы, чтобы и ButtonView, и MenuView существовали на одном уровне в иерархии представлений, поместив их оба в контейнер, фрейм которого полностью соответствует им обоим. Таким образом, интерактивная область вырезанного элемента не будет игнорироваться из-за границ его суперпредставления.
Если у вас есть много других подпредставлений внутри родительского представления, то, вероятно, большинство других интерактивных представлений не будет работать, если вы используете вышеуказанные решения, в этом случае вы можете использовать что-то вроде этого (в 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's 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)
}
}
Мне потребовалось время, чтобы понять, как это работает, потому что приведенные выше решения не сработали для меня, поскольку у меня много вложенных подпредставлений. Я попытаюсь объяснить просто hitTest, чтобы каждый мог адаптировать свой код в зависимости от ситуации.
Представьте себе такую ситуацию: представление с именем GrandParent имеет полностью в своих границах подпредставление с именем Parent. У этого Родителя есть подпредставление под названием Дочерний элемент, границы которого выходят за границы Родителя:
-------------------------------------- -> Grand parent
| ------- -> Child |
| | | |
| ----|-----|--- -> Parent |
| | | ^ | | |
| | | | | |
| ----|-----|--- |
| | + | |
| * |-----| |
| |
--------------------------------------
и 3 различных пользовательских прикосновения. touch не распознается в дочернем элементе Всякий раз, когда вы касаетесь прародителя в любом месте его границы, прародитель будет вызывать все его прямые подвиды.
.hitTest
, независимо от того, где вы коснулись прародителя . Так вот, за 3 касания позвонит дедушка
parent.hitTest()
.
Предполагается, что hitTest возвращает самого дальнего потомка, содержащего заданную точку (точка задается относительно собственных границ, в нашем примере Grand Parent вызывает Parent.hitTest() с точкой относительно родительских границ).
За
*
touch, parent.hitTest возвращает ноль, и это прекрасно. За
^
touch, parent.hitTest возвращает Child, потому что он находится в его границах (реализация hitTest по умолчанию).
Но для
+
касание, parent.hitTest по умолчанию возвращает nil, так как касание не находится в границах родителя. Таким образом, нам нужна наша собственная реализация hitTest, чтобы в основном преобразовать точку относительно границы Родителя в точку относительно границ Дочернего элемента. Затем вызовите hitTest для дочернего элемента, чтобы увидеть, находится ли касание в границах дочернего элемента (предполагается, что дочерний элемент имеет реализацию hitTest по умолчанию, возвращающую самого дальнего потомка от него в пределах его границ, то есть самого себя).
Если у вас есть сложные вложенные подпредставления и если приведенное выше решение не очень хорошо работает для вас, это объяснение может быть полезно для создания собственной реализации, соответствующей вашей иерархии представлений.
Если кому-то это нужно, вот быстрая альтернатива
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
}
Поместите ниже строки кода в вашу иерархию представлений:
- (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" относительно "Пользовательский вынос: кнопка не реагирует на нажатия".
Я надеюсь, что это поможет вам...!!!