Захоплення дотиків до підвиду поза рамкою його перегляду за допомогою 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.