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

94

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

ThomasM
источник
1
У меня это отлично работает. developer.apple.com/library/ios/qa/qa2013/qa1812.html Лучший
Фрэнк Ли
2
Это тоже хорошо и актуально (или, возможно, обман): stackoverflow.com/a/31420820/8047
Дэн Розенстарк,

Ответы:

94

Да. Вы можете переопределить 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, и вы могли бы просмотреть свой список подпредставлений для этих классов.) Надеюсь, это поможет.

Джник
источник
Можете ли вы помочь мне в дальнейшем правильно реализовать это с помощью примера кода? Также я прочитал ссылку и из-за следующей строки меня смущает: «Точки, которые лежат за пределами границ получателя, никогда не сообщаются как попадания, даже если они на самом деле лежат в одном из подчиненных представлений получателя. Подпредставления могут визуально выходить за границы своего родителя, если для свойства clipsToBounds родительского представления установлено значение NO. Однако проверка нажатия всегда игнорирует точки за пределами границ родительского представления ".
ThomasM
Особенно эта часть создает впечатление, что это не то решение, которое мне нужно: «Тем не менее, проверка попадания всегда игнорирует точки за пределами родительского представления» .. Но я могу ошибаться, я нахожу ссылки на яблоки иногда действительно сбивающими с толку ..
ThomasM
Ах, теперь я понимаю. Вам нужно переопределить родительский элемент, pointInside:withEvent:чтобы включить рамку кнопки. Я добавлю пример кода к своему ответу выше.
jnic
Есть ли способ сделать это без переопределения методов UIView? Например, если ваши представления полностью универсальны для UIKit и инициализируются с помощью Nib, можете ли вы переопределить эти методы из своего UIViewController?
RLH
1
hitTest:withEvent:и pointInside:withEvent:область, которая не вызывается для меня, когда я нажимаю кнопку, которая находится за пределами родительских границ. Что может быть не так?
iosdude
24

@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.

Лучший

Вызвать
источник
20

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

- (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];

}

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

Джек
источник
18

В моем случае у меня был 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)
}

Swift 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)
}
user3344977
источник
Это был именно мой сценарий. Другой момент: вы должны поместить этот метод в класс CollectionViewCell.
Хамид Реза Ансари
Но зачем вам вставлять кнопку в замещаемый метод ???
rommex
10

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

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

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

Что тебе необходимо сделать?

Когда ваш супервизор получит упомянутое выше событие касания, вам нужно будет явно указать 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 - проблема вид, который фактически находится за пределами В.

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

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

Скотт Чжу
источник
4
Спасибо тебе за это! Я потратил часы и часы на решение проблемы, связанной с этим. Я очень ценю, что вы нашли время опубликовать это. :)
ClayJ
@ScottZhu: Я добавил к вашему ответу важный нюанс. Конечно, вы можете удалить или изменить мое дополнение! Еще раз спасибо!
Fattie 01
9

Swift:

Где 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)
}
Люк Бартоломео
источник
5

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

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

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

Subview Ваш опыт может отличаться, но в моем случае мне также пришлось перезаписать pointInsideметод для subview, который решил, что прикосновение было вне пределов. Мой находится в 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];
    }
}

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

Дэн Розенстарк
источник
2

swift 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)
}
Анкит Кушва
источник