【问题标题】:UIButton in cell in collection view not receiving touch up inside event集合视图中单元格中的 UIButton 未收到事件内部的修饰
【发布时间】:2012-10-24 11:14:29
【问题描述】:

以下代码表达了我的问题: (它是自包含的,您可以使用空模板创建一个 Xcode 项目,替换 main.m 文件的内容,删除 AppDelegate.h/.m 文件并构建它)

//
//  main.m
//  CollectionViewProblem
//


#import <UIKit/UIKit.h>

@interface Cell : UICollectionViewCell

@property (nonatomic, strong) UIButton *button;
@property (nonatomic, strong) UILabel *label;

@end

@implementation Cell
 - (id)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        self.label = [[UILabel alloc] initWithFrame:self.bounds];
        self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        self.label.backgroundColor = [UIColor greenColor];
        self.label.textAlignment = NSTextAlignmentCenter;

        self.button = [UIButton buttonWithType:UIButtonTypeInfoLight]; 
        self.button.frame = CGRectMake(-frame.size.width/4, -frame.size.width/4, frame.size.width/2, frame.size.width/2);
        self.button.backgroundColor = [UIColor redColor];
        [self.button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
        [self.contentView addSubview:self.label];
        [self.contentView addSubview:self.button];
    }
    return self;
}


// Overriding this because the button's rect is partially outside the parent-view's bounds:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([super pointInside:point withEvent:event])
    {
        NSLog(@"inside cell");
        return YES;
    }
    if ([self.button
         pointInside:[self convertPoint:point
                                 toView:self.button] withEvent:nil])
    {
        NSLog(@"inside button");
        return YES;
    }

    return NO;
}


- (void)buttonClicked:(UIButton *)sender
{
    NSLog(@"button clicked!");
}
@end

@interface ViewController : UICollectionViewController

@end

@implementation ViewController

// (1a) viewdidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.collectionView registerClass:[Cell class] forCellWithReuseIdentifier:@"ID"];
}

// collection view data source methods ////////////////////////////////////

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 100;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    Cell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ID" forIndexPath:indexPath];
    cell.label.text = [NSString stringWithFormat:@"%d", indexPath.row];
    return cell;
}
///////////////////////////////////////////////////////////////////////////

// collection view delegate methods ////////////////////////////////////////

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"cell #%d was selected", indexPath.row);
}
////////////////////////////////////////////////////////////////////////////
@end


@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    ViewController *vc = [[ViewController alloc] initWithCollectionViewLayout:layout];


    layout.itemSize = CGSizeMake(128, 128);
    layout.minimumInteritemSpacing = 64;
    layout.minimumLineSpacing = 64;
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    layout.sectionInset = UIEdgeInsetsMake(32, 32, 32, 32);


    self.window.rootViewController = vc;

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

@end


int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

基本上,我正在使用集合视图创建一个 Springboard 类型的 UI。我的 UICollectionViewCell 子类 (Cell) 有一个按钮,该按钮部分位于单元格的 contentView(即其父视图)边界之外。

问题在于,单击 contentView 边界之外的按钮的任何部分(基本上是按钮的 3/4)都不会调用按钮操作。仅当单击与 contentView 重叠的按钮部分时,才会调用该按钮的操作方法。

我什至在 Cell 中重写了-pointInside:withEvent: 方法,以便确认按钮中的触摸。但这对按钮点击问题没有帮助。

我猜这可能与 collectionView 处理触摸的方式有关,但我不知道是什么。我知道 UICollectionView 是一个 UIScrollView 子类,我实际上已经测试了在包含部分重叠按钮的视图(将子视图制作为滚动视图)上覆盖 -pointInside:withEvent: 可以解决按钮点击问题,但在这里没有用。

有什么帮助吗?

** 添加: 作为记录,我目前对该问题的解决方案包括在 contentView 中插入一个较小的子视图,从而使单元格具有其外观。删除按钮被添加到 contentView 中,因此它的矩形实际上位于 contentView 的范围内,但仅部分重叠单元格的可见部分(即插入子视图)。所以我得到了我想要的效果,并且按钮工作正常。但是我还是很好奇上面原来实现的问题。

【问题讨论】:

    标签: ios uicollectionview uiresponder


    【解决方案1】:

    问题似乎出在 hitTest/pointInside 上。我猜如果触摸是在单元格外部的按钮部分上,则单元格会从 pointInside 返回 NO,因此该按钮未经过命中测试。要解决此问题,您必须在 UICollectionViewCell 子类上覆盖 pointInside 以将按钮考虑在内。如果触摸在按钮内部,您还需要覆盖 hitTest 以返回按钮。下面是示例实现,假设您的按钮位于 UICollectionViewCell 子类中名为 deleteButton 的属性中。

    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        UIView *view = [self.deleteButton hitTest:[self.deleteButton convertPoint:point fromView:self] withEvent:event];
        if (view == nil) {
            view = [super hitTest:point withEvent:event];
        }
        return view;
    }
    
    -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        if ([super pointInside:point withEvent:event]) {
            return YES;
        }
        //Check to see if it is within the delete button
        return !self.deleteButton.hidden && [self.deleteButton pointInside:[self.deleteButton convertPoint:point fromView:self] withEvent:event];
    }
    

    请注意,由于 hitTest 和 pointInside 期望点位于接收器的坐标空间中,因此您必须记住在调用按钮上的这些方法之前转换点。

    【讨论】:

    • 这是一个完美的答案!你怎么能添加更多的按钮呢?
    • 这个解决方案很好地帮助了我。我设法用几个按钮来使用它。但我认为我的解决方案不是很优雅。我刚刚用我的所有按钮创建了一个数组,并在返回的视图为零时运行它。如果在运行结束时它为零,我就做view = [super hitTest:point withEvent:event];
    • 在将点传递给deleteButton的hitTest之前,我调用convertPoint将其转换为deleteButton的坐标空间。
    • 当单元格靠近屏幕右侧时,帮助我在单元格中添加了一个按钮。我不知道为什么,但未检测到靠近屏幕右侧的触摸。
    【解决方案2】:

    在 Interface Builder 中,您是否将对象设置为 UICollectionViewCell?因为有一次我错误地设置了一个 UIView 并在为它分配了正确的 UICollectionViewCell 类之后......但是做这些事情(按钮,标签,ecc。)没有添加到 contentView 所以他们不会像他们那样响应......

    所以,在 IB 中提醒在绘制界面时带 UICollectionViewCell 对象 :)

    【讨论】:

    • 我的问题与 IB 无关。一切都是用代码创建的。无论如何,出于所有实际目的,我在问题底部概述的解决方案效果很好。我只是好奇这个问题背后的确切原因。
    • 尽管其他一切看起来都符合预期,但按钮是不可触摸的。这个答案是我的问题的解决方案。
    • 这解决了我在 UICollectionViewCell 中的子视图没有收到触摸的问题。
    • 这是一个很棒的答案! :D
    【解决方案3】:

    Swift 版本:

    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    
        //From higher z- order to lower except base view;
    
        for (var i = subviews.count-2; i >= 0 ; i--){
            let newPoint = subviews[i].convertPoint(point, fromView: self)
            let view = subviews[i].hitTest(newPoint, withEvent: event)
            if view != nil{
                return view
            }
        }
    
        return super.hitTest(point, withEvent: event)
    
    }
    

    就是这样......对于所有子视图

    【讨论】:

    • 这就是我在代码中调用 convertPoint: 的原因。您应该使用 convertPoint 和相关方法,而不是自己做数学。
    【解决方案4】:

    我成功接收到在子类 UICollectionViewCell.m 文件中创建的按钮的触摸;

    - (id)initWithCoder:(NSCoder *)aDecoder
        {
        self = [super initWithCoder:aDecoder];
        if (self)
        {
    
        // Create button
    
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        button.frame = CGRectMake(0, 0, 100, 100); // position in the parent view and set the size of the button
        [button setTitle:@"Title" forState:UIControlStateNormal];
        [button setImage:[UIImage imageNamed:@"animage.png"] forState:UIControlStateNormal];
        [button addTarget:self action:@selector(button:) forControlEvents:UIControlEventTouchUpInside];
    
        // add to contentView
        [self.contentView addSubview:button];
        }
        return self;
    }
    

    在意识到 Storyboard 中添加的按钮不起作用后,我在代码中添加了按钮,不确定这是否在最新的 Xcode 中得到修复。

    希望对您有所帮助。

    【讨论】:

    • 感谢您的尝试,但我认为您没有理解我的问题。
    • 同意,深夜盯着屏幕的注意力太分散了。道歉。
    【解决方案5】:

    我看到原始答案的两个快速转换并不完全是快速转换。所以我只想给出原始答案的Swift 4 转换,以便每个想要使用它的人都可以使用它。您只需将代码粘贴到您的subclassedUICollectionViewCell。只需确保您使用自己的按钮更改 closeButton

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        var view = closeButton.hitTest(closeButton.convert(point, from: self), with: event)
        if view == nil {
            view = super.hitTest(point, with: event)
        }
    
        return view
    }
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if super.point(inside: point, with: event) {
            return true
        }
    
        return !closeButton.isHidden && closeButton.point(inside: closeButton.convert(point, from: self), with: event)
    }
    

    【讨论】:

      【解决方案6】:

      按照接受的答案要求,我们应该创建一个hitTest 以便在单元格内接收触摸。这是命中测试的 Swift 4 代码:

      override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        for i in (0..<subviews.count-1).reversed() {
          let newPoint = subviews[i].convert(point, from: self)
          if let view = subviews[i].hitTest(newPoint, with: event) {
              return view
          }
        }
        return super.hitTest(point, with: event)
      }
      

      【讨论】:

        【解决方案7】:

        我在尝试将删除按钮放在 uicollectionview 单元格的边界之外时遇到了类似的问题,但它没有接缝以响应点击事件。

        我解决它的方法是在集合上放置一个 UITapGestureRecognizer,当点击发生时执行以下代码

        //this works also on taps outside the cell bouns, im guessing by getting the closest cell to the point of click.
        
        NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:[tapRecognizer locationInView:self.collectionView]]; 
        
        if(tappedCellPath) {
            UICollectionViewCell *tappedCell = [self.collectionView cellForItemAtIndexPath:tappedCellPath];
            CGPoint tapInCellPoint = [tapRecognizer locationInView:tappedCell];
            //if the tap was outside of the cell bounds then its in negative values and it means the delete button was tapped
            if (tapInCellPoint.x < 0) [self deleteCell:tappedCell]; 
        }
        

        【讨论】:

          【解决方案8】:

          在我看来,Honus 在这里给出了最好的答案。事实上,只有一个对我有用,所以我一直在回答其他类似的问题并以这种方式发送它们:

          我花了好几个小时在网上搜索我的 UIButton 在 UICollectionView 中不起作用的解决方案。让我发疯,直到我终于找到适合我的解决方案。而且我相信这也是正确的方法:破解命中测试。这是一个比修复 UICollectionView 按钮问题更深入(双关语)的解决方案,因为它可以帮助您将点击事件发送到隐藏在阻止您的事件通过的其他视图下的任何按钮:

          UIButton in cell in collection view not receiving touch up inside event

          由于 SO 答案是在 Objective C 中,我从那里找到了一个快速解决方案的线索:

          http://khanlou.com/2018/09/hacking-hit-tests/

          --

          当我禁用单元格上的用户交互或我尝试的任何其他各种答案时,没有任何效果。

          我在上面发布的解决方案的美妙之处在于,您可以保留 addTarget 和选择器函数的习惯,因为它们很可能永远不会成为问题。您只需要重写一个函数来帮助触摸事件到达目的地。

          解决方案为何有效:

          在最初的几个小时里,我发现我的 addTarget 调用没有正确注册手势。事实证明,目标注册良好。触摸事件根本就没有到达我的按钮。

          现实似乎来自我阅读的任何数量的 SO 帖子和文章,即 UICollectionView 单元旨在容纳一个动作,而不是出于各种原因的多个动作。所以你只是应该使用内置的选择动作。考虑到这一点,我相信绕过此限制的正确方法不是破解 UICollectionView 以禁用滚动或用户交互的某些方面。 UICollectionView 只是在做它的工作。 正确的方法是破解命中测试以在点击到达 UICollectionView 之前拦截点击并找出他们正在点击的项目。然后,您只需向他们正在点击的按钮发送一个触摸事件,然后让您的正常工作完成。


          我的最终解决方案(来自 khanlou.com 文章)是将我的 addTarget 声明和我的选择器函数放在我喜欢的任何位置(在单元格类或 cellForItemAt 覆盖中),并在覆盖 hitTest 函数的单元格类中。

          在我的单元格中,我有:

          @objc func didTapMyButton(sender:UIButton!) {
              print("Tapped it!")
          }
          

          override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
          
              guard isUserInteractionEnabled else { return nil }
          
              guard !isHidden else { return nil }
          
              guard alpha >= 0.01 else { return nil }
          
              guard self.point(inside: point, with: event) else { return nil }
          
          
              // add one of these blocks for each button in our collection view cell we want to actually work
              if self.myButton.point(inside: convert(point, to: myButton), with: event) {
                  return self.myButton
              }
          
              return super.hitTest(point, with: event)
          }
          

          在我的单元类初始化中,我有:

          self.myButton.addTarget(self, action: #selector(didTapMyButton), for: .touchUpInside)
          

          【讨论】:

          • 感谢您的描述和链接。
          【解决方案9】:

          我从这里找到了这个py4u.net

          尝试了最底层的解决方案。 (据我所知,所有的东西都是从这个页面收集的)

          在我的情况下,colution 也有效。然后我刚刚检查了 User Interaction Enabled 复选标记是否在 xib 的 Collection 视图中被选中,它是 contentView。你猜怎么了。 contentView 的 UserInteraction 被禁用。

          启用它解决了按钮的 touchUpInside 事件的问题,并且无需覆盖 hitTest 方法。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-01-31
            • 2023-03-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多