Взаимодействие за пределами UIView

Возможно ли, чтобы UIButton (или любой другой элемент управления в этом отношении) получал события касания, когда фрейм UIButton находится за пределами его родительского фрейма? Потому что, когда я пытаюсь это сделать, мой UIButton, похоже, не может получать какие-либо события. Как мне обойти это?


person ThomasM    schedule 25.03.2011    source источник
comment
Это отлично работает для меня. developer.apple.com/library/ios/qa/qa2013/qa1812. html Лучший   -  person Frank Li    schedule 05.02.2015
comment
Это также хорошо и актуально (или, возможно, обман): stackoverflow.com/a/31420820/8047   -  person Dan Rosenstark    schedule 09.01.2017


Ответы (9)


да. Вы можете переопределить метод hitTest:withEvent:, чтобы вернуть представление для большего набора точек, чем содержит это представление. См. справочник по классам UIView.

Изменить: Пример:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(-radius, -radius,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}

Редактировать 2: (После уточнения:) Чтобы кнопка считалась находящейся в границах родителя, вам необходимо переопределить pointInside:withEvent: в родительском элементе, чтобы включить рамку кнопки.

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(self.view.bounds, point) ||
        CGRectContainsPoint(button.view.frame, point))
    {
        return YES;
    }
    return NO;
}

Обратите внимание, приведенный здесь код для переопределения pointInside не совсем корректен. Как Summon поясняет ниже, сделайте следующее:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
    if ( CGRectContainsPoint(self.oversizeButton.frame, point) )
        return YES;

    return [super pointInside:point withEvent:event];
    }

Обратите внимание, что вы, скорее всего, сделаете это с self.oversizeButton в качестве IBOutlet в этом подклассе UIView; затем вы можете просто перетащить рассматриваемую «кнопку большого размера» в соответствующий специальный вид. (Или, если по какой-то причине вы часто делали это в проекте, у вас был бы специальный подкласс UIButton, и вы могли бы просмотреть список подпредставлений для этих классов.) Надеюсь, это поможет.

person jnic    schedule 25.03.2011
comment
Можете ли вы помочь мне правильно реализовать это с помощью примера кода? Также я прочитал ссылку и из-за следующей строки я запутался: точки, которые лежат за пределами границ приемника, никогда не сообщаются как попадания, даже если они фактически находятся в одном из подпредставлений приемника. Подпредставления могут визуально выходить за границы своего родителя, если для свойства clipsToBounds родительского представления установлено значение NO. Однако проверка попадания всегда игнорирует точки за пределами родительского представления. - person ThomasM; 25.03.2011
comment
Именно эта часть создает впечатление, что это не то решение, которое мне нужно: однако тестирование на попадание всегда игнорирует точки за пределами родительского представления. - person ThomasM; 25.03.2011
comment
А, теперь я понимаю. Вам нужно переопределить pointInside:withEvent: родителя, чтобы включить рамку кнопки. Я добавлю пример кода к моему ответу выше. - person jnic; 25.03.2011
comment
Есть ли способ сделать это без переопределения методов UIView? Например, если ваши представления полностью универсальны для UIKit и инициализированы с помощью Nib, можете ли вы переопределить эти методы из своего UIViewController? - person RLH; 04.04.2012
comment
Вы не можете переопределить методы напрямую; однако вы можете переопределить touchesBegan и т. д., поскольку UIViewController является подклассом UIResponder. Представления, инициализированные непосредственно из пера, все еще могут быть пользовательскими подклассами. Просто установите поле «Класс» вашего представления в «Инспекторе удостоверений» в качестве своего собственного подкласса и, после загрузки, отобразите свой наконечник соответствующим образом. - person jnic; 05.04.2012
comment
Еще одно замечание об этом решении. В hitTest:withEvent: вы должны вернуть самого младшего потомка, который соответствует тесту на попадание. Это означает, что если у вас есть кнопка за пределами родительского представления, вы сначала нажмете эту кнопку и вернете ее, а затем просто вызовете «return [super hitTest: point withEvent: event];» - person Jeremy Wiebe; 01.11.2013
comment
Разве кадр, тестируемый в первом блоке, не должен быть CGRectMake(-radius, -radius, self.frame.size.width + 2 * радиус, self.frame.size.height + 2 * радиус)? В противном случае переполнение по правому и нижнему краю будет на одном уровне с исходным правым и нижним. - person rob5408; 01.07.2014
comment
Метод не вызывается. - person Iulian Onofrei; 27.02.2015
comment
Спасибо, это поставило меня на правильный путь. я переопределяю pointInside моего родительского представления кнопки, чтобы вернуть true (для пользовательского размера кадра), и это сделало всю работу. - person Jorge Wander Santana Ureña; 05.07.2016
comment
hitTest:withEvent: и pointInside:withEvent: области не вызываются для меня, когда я нажимаю кнопку, которая находится за пределами родительских границ. Что может быть не так? - person iosdude; 13.06.2017

@jnic, я работаю над iOS SDK 5.0, и чтобы ваш код работал правильно, мне пришлось сделать следующее:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(button.frame, point)) {
    return YES;
}
return [super pointInside:point withEvent:event]; }

Представление контейнера в моем случае представляет собой UIButton, и все дочерние элементы также являются UIButton, которые могут перемещаться за пределы родительского UIButton.

Лучший

person Summon    schedule 02.02.2012

В родительском представлении вы можете переопределить метод проверки попадания:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint translatedPoint = [_myButton convertPoint:point fromView:self];

    if (CGRectContainsPoint(_myButton.bounds, translatedPoint)) {
        return [_myButton hitTest:translatedPoint withEvent:event];
    }
    return [super hitTest:point withEvent:event];

}

В этом случае, если точка попадает в пределы вашей кнопки, вы перенаправляете звонок туда; если нет, вернитесь к исходной реализации.

person Ja͢ck    schedule 25.05.2015

В моем случае у меня был подкласс UICollectionViewCell, который содержал класс UIButton. Я отключил clipsToBounds в ячейке, и кнопка была видна за пределами ячейки. Однако кнопка не получала событий касания. Мне удалось обнаружить сенсорные события на кнопке, используя ответ Джека: https://stackoverflow.com/a/30431157/3344977< /а>

Вот версия Swift:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    let translatedPoint = button.convertPoint(point, fromView: self)

    if (CGRectContainsPoint(button.bounds, translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, withEvent: event)
    }
    return super.hitTest(point, withEvent: event)
}

Свифт 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    let translatedPoint = button.convert(point, from: self)

    if (button.bounds.contains(translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, with: event)
    }
    return super.hitTest(point, with: event)
}
person user3344977    schedule 30.11.2015
comment
Это был именно мой сценарий. Во-вторых, вы должны поместить этот метод в класс CollectionViewCell. - person Hamid Reza Ansari; 30.08.2019
comment
Но зачем вводить кнопку в переопределенный метод??? - person rommex; 17.11.2019

Почему это происходит?

Это связано с тем, что когда ваше подпредставление находится за пределами границ вашего суперпредставления, события касания, которые фактически происходят в этом подпредставлении, не будут доставлены в это подпредставление. Однако он БУДЕТ доставлен в супервизор.

Независимо от того, обрезаются ли подпредставления визуально, события касания всегда учитывают прямоугольник границ суперпредставления целевого представления. Другими словами, сенсорные события, происходящие в части представления, которая находится за пределами прямоугольника границ его суперпредставления, не доставляются в это представление. Ссылка

Что вам нужно сделать?

Когда ваше суперпредставление получает событие касания, упомянутое выше, вам нужно явно указать UIKit, что мое подпредставление должно получать это событие касания.

Что насчет кода?

В своем супервизоре реализуйте func hitTest(_ point: CGPoint, with event: UIEvent?)

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if isHidden || alpha == 0 || clipsToBounds { return super.hitTest(point, with: event) }
        // convert the point into subview's coordinate system
        let subviewPoint = self.convert(point, to: subview)
        // if the converted point lies in subview's bound, tell UIKit that subview should be the one that receives this event
        if !subview.isHidden && subview.bounds.contains(subviewPoint) { return subview }
        return super.hitTest(point, with: event)
    }

Увлекательная ловушка: вы должны перейти к "самому высокому слишком маленькому супервизору"

Вы должны подняться «вверх» к «самому высокому» виду, за пределами которого находится проблемный вид.

Типичный пример:

Скажем, у вас есть экран S с представлением контейнера C. Представление контроллера представления контейнера — это V. (Напомним, что V будет находиться внутри C и иметь такой же размер.) V имеет подпредставление (возможно, кнопку) B. B — это проблема вид, который на самом деле находится за пределами V.

Но обратите внимание, что B также находится за пределами C.

В этом примере вам нужно применить решение override hitTest на самом деле к C, а не к V. Если вы примените его к V - он ничего не сделает.

person Scott Zhu    schedule 10.01.2018
comment
Спасибо тебе за это! Я потратил часы и часы на проблему, вытекающую из этого. Я очень ценю, что вы нашли время, чтобы опубликовать это. :) - person ClayJ; 24.02.2018
comment
@ScottZhu: я добавил к твоему ответу важную ошибку. Конечно, вы можете свободно удалять или изменять мое дополнение! Еще раз спасибо! - person Fattie; 01.09.2019

Быстрый:

Где targetView — это представление, в котором вы хотите получить событие (которое может быть полностью или частично расположено за пределами фрейма своего родительского представления).

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        let pointForTargetView: CGPoint = targetView.convertPoint(point, fromView: self)
        if CGRectContainsPoint(targetView.bounds, pointForTargetView) {
            return closeButton
        }
        return super.hitTest(point, withEvent: event)
}
person Luke Bartolomeo    schedule 25.04.2015

Для меня (как и для других) ответом было не переопределить метод hitTest, а скорее метод pointInside. Мне пришлось сделать это только в двух местах.

Суперпредставление Это представление, согласно которому, если бы clipsToBounds было установлено значение true, вся эта проблема исчезла бы. Итак, вот мой простой UIView в Swift 3:

class PromiscuousView : UIView {
    override func point(inside point: CGPoint,
               with event: UIEvent?) -> Bool {
        return true
    }
} 

Вспомогательное представление Ваш пробег может отличаться, но в моем случае мне также пришлось перезаписать метод pointInside для подпредставления, которое решило, что касание выходит за пределы допустимого. Мой находится в Objective-C, поэтому:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    WEPopoverController *controller = (id) self.delegate;
    if (self.takeHitsOutside) {
        return YES;
    } else {
        return [super pointInside:point withEvent:event];
    }
}

Этот ответ (и несколько других на тот же вопрос) может помочь прояснить ваше понимание.

person Dan Rosenstark    schedule 09.01.2017

свифт 4.1 обновлен

Чтобы получить сенсорные события в том же UIView, вам просто нужно добавить следующий метод переопределения, он будет идентифицировать конкретное представление, нажатое в UIView, которое находится за пределами границ.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let pointforTargetview:CGPoint = self.convert(point, to: btnAngle)
    if  btnAngle.bounds.contains(pointforTargetview) {
        return  self
    }
    return super.hitTest(point, with: event)
}
person Ankit Kushwah    schedule 26.09.2018

Я знаю, что это очень старый вопрос, но все же нам нужны эти решения в виде обновленной версии, поэтому я поделюсь новой версией решения этой проблемы.

Предположим, что мой mainView кадр равен CGRect(0.0, 0.0, 250, 250.0), и я хочу добавить кнопку размером ширины и высоты 50 в качестве subView моего mainView (в верхний левый угол), его кадр будет выглядеть как CGRect(-50, -50, 50, 50) в mainView, в этом случае мы не можем взаимодействовать с этой кнопкой, и вот пример решения этой проблемы

В вашем MainView (представление контейнера кнопок) переопределите hitTestfunction, как указано в других ответах, но здесь измените порядок подпредставлений и преобразуйте точку попадания в эту точку подпредставления, затем проверьте результат только с помощью hitTest еще раз, иначе проверьте, содержит ли ваше представление это point верните mainView, иначе верните nil, чтобы не влиять на другие виды

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return self.superview }
    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 self.bounds.contains(point) ? self : nil
}

Я протестировал и использовал этот код в своем собственном приложении, и он работает хорошо.

person Coder ACJHP    schedule 15.01.2021