Захоплення дотиків до підвиду поза рамкою його перегляду за допомогою hitTest: withEvent:


94

Моя проблема: у мене є огляд, EditViewякий займає в основному весь додаток, і підвід, MenuViewякий займає лише нижню частину ~ 20%, а потім MenuViewмістить свій власний підвид, ButtonViewякий фактично знаходиться за межами MenuViewкордонів (приблизно так :) ButtonView.frame.origin.y = -100.

(примітка: EditViewє інші підпрограми, які не є частиною MenuViewієрархії подання, але можуть вплинути на відповідь.)

Ви, мабуть, вже знаєте проблему: коли ButtonViewце в межах MenuView(або, точніше, коли мої дотики знаходяться в MenuViewмежах), ButtonViewреагує на дотикові події. Коли мої дотики виходять за MenuViewмежі (але все ще в ButtonViewмежах), жодна сенсорна подія не отримується ButtonView.

Приклад:

  • (Е) є EditViewбатьком усіх поглядів
  • (M) - MenuViewце підпрогляд EditView
  • (B) - ButtonViewце підпрогляд MenuView

Діаграма:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Оскільки (B) знаходиться поза рамкою (M), дотик в області (B) ніколи не надсилатиметься до (M) - насправді, (M) в цьому випадку ніколи не аналізує дотик, а дотик надсилається до наступний об’єкт в ієрархії.

Мета: Я розумію, що перевизначення hitTest:withEvent:може вирішити цю проблему, але я не розумію, як саме. У моєму випадку, повинні hitTest:withEvent:бути перевизначені в EditView(мій «майстер» надтабліци)? Або це слід MenuViewзамінити, безпосередній перегляд кнопки, яка не отримує дотиків? Або я думаю про це неправильно?

Якщо для цього потрібні довгі пояснення, корисний хороший Інтернет-ресурс був би корисним - за винятком документів UIView від Apple, які мені не дали зрозуміти.

Дякую!

Відповіді:


145

Я змінив код прийнятої відповіді на більш загальний - він обробляє випадки, коли подання виконує відсікання підпоглядів до своїх меж, може бути прихованим, і що ще важливіше: якщо підпосилання являють собою складні ієрархії подання, буде повернуто правильний підгляд.

- (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
}

Сподіваюся, це допоможе тим, хто намагається використовувати це рішення для більш складних випадків використання.


Це виглядає добре і дякую, що зробили це. Однак було одне питання: чому ти робиш повернення [super hitTest: point withEvent: event]; ? Хіба ви не повернетеся до нуля, якщо не існує підвід, який запускає дотик? Apple каже, що HitTest повертає нуль, якщо жоден підвід не містить дотику.
Ser Pounce

Хм ... Так, це звучить майже правильно. Крім того, для коректної поведінки об’єкти потрібно повторювати у зворотному порядку (оскільки останній візуально є самим верхнім). Відредагований код, який відповідає.
Ноам

3
Я щойно використав ваше рішення, щоб зробити UIButton захоплення дотику, і це всередині UIView, що знаходиться всередині UICollectionViewCell всередині (очевидно) UICollectionView. Мені довелося підкласи UICollectionView і UICollectionViewCell, щоб перекрити hitTest: withEvent: на цих трьох класах. І це працює як шарм !! Дякую !!
Даніель Гарсія

3
Залежно від бажаного використання, він повинен або повернути [super hitTest: point withEvent: event], або нульовий. Повернення себе призвело б до отримання всього.
Ноам

1
Ось технічний документ Q&A від Apple щодо цієї ж методики: developer.apple.com/library/ios/qa/qa2013/qa1812.html
Джеймс Куанг

33

Гаразд, я кілька копав і тестував, ось як це 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];
}

Я все ще дуже новачок у цьому методі та цій проблемі, тому, якщо є більш ефективні чи правильні способи написання коду, будь ласка, прокоментуйте.

Я сподіваюся, що це допоможе тому, хто пізніше потрапить на це питання. :)


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

Коли фрейм перегляду був кадром батьківського подання, подія натискання не відповідала! Ваше рішення вирішило цю проблему. Велике спасибі :-)
відJeevan

@toblerpwn (смішний псевдонім :)), вам слід відредагувати цю чудову давнішу відповідь, щоб було дуже зрозуміло (ВИКОРИСТОВУЙТЕ ВЕЛИКІ ПІСЛІ ШАРКИ), до якого класу потрібно це додати. Ура!
Fattie

26

У Swift 5

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
}

Це спрацює лише в тому випадку, якщо ви застосуєте переоцінку прямого огляду виду "недоброзичливців"
Худі Ільфельд

2

Я б хотів зробити, щоб і ButtonView, і MenuView існували на одному рівні в ієрархії перегляду, розміщуючи їх обох у контейнері, кадр якого повністю відповідає обом. Таким чином, інтерактивна область відрізаного елемента не буде ігнорована через межі його огляду.


Я також замислювався над цим вирішенням - це означає, що мені доведеться дублювати певну логіку розміщення (або рефакторировать якийсь серйозний код!), але, зрештою, це мій найкращий вибір в кінцевому підсумку ..
toblerpwn

1

Якщо у вашому батьківському представництві є багато інших підпереглядів, напевно, більшість інших інтерактивних представлень не працюватимуть, якщо ви використовуєте вищезазначені рішення, у такому випадку ви можете використовувати щось подібне (у 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)
    }
}

0

Якщо комусь це потрібно, ось швидка альтернатива

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
}

0

Розмістіть нижче рядків коду у вашій ієрархії подання:

- (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" щодо "Спеціальної виноски: кнопка не викликає кліків".

Сподіваюся, це вам допоможе ... !!!


Ваш блог видалено, тож де ми можемо знайти пояснення, будь ласка?
ishahak
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.