简体   繁体   English

集合视图中的单元格中的 UIButton 未接收内部事件的修饰

[英]UIButton in cell in collection view not receiving touch up inside event

The following code expresses my problem: (It's self-contained in that you could create a Xcode project with an empty template, replace the contents of the main.m file, delete the AppDelegate.h/.m files and build it)以下代码表达了我的问题:(它是独立的,因为您可以使用空模板创建一个 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]));
    }
}

Basically I'm creating a Springboard-type UI using collection views.基本上,我正在使用集合视图创建一个 Springboard 类型的 UI。 My UICollectionViewCell subclass ( Cell ) has a button which lies partially outside the cell's contentView (ie its superview's) bounds.我的UICollectionViewCell子类 ( Cell ) 有一个按钮,它部分位于单元格的contentView (即它的超级视图)边界之外。

The problem is that clicking on any part of the button outside of the contentView bounds (basically 3/4th of the button) doesn't invoke the button action.问题是单击contentView边界之外的按钮的任何部分(基本上是按钮的 3/4)不会调用按钮操作。 Only when clicking on the portion of the button that overlaps the contentView is the button's action method called.仅当单击与contentView重叠的按钮部分时,才会调用按钮的操作方法。

I've even overridden -pointInside:withEvent: method in Cell so that touches in the button will be acknowledged.我什至覆盖了Cell中的-pointInside:withEvent:方法,以便确认按钮中的触摸。 But that hasn't helped with the button clicking problem.但这对解决按钮点击问题没有帮助。

I'm guessing it might be something to do with how collectionView handles touches, but I don't know what.我猜这可能与collectionView处理触摸的方式有关,但我不知道是什么。 I know that UICollectionView is a UIScrollView subclass and I've actually tested that overriding -pointInside:withEvent: on a view (made subview to a scroll view) containing a partially overlapping button solves the button clicking problem, but it hasn't worked here.我知道UICollectionView是一个UIScrollView子类,我实际上已经测试了覆盖-pointInside:withEvent:在包含部分重叠按钮的视图(使子视图成为滚动视图)上解决了按钮点击问题,但它在这里不起作用.

Any help?有什么帮助吗?

** Added: For the record, my current solution to the problem involves insetting a smaller subview to contentView which gives the cell its appearance. ** 添加:为了记录,我当前的问题解决方案涉及将较小的子视图插入到contentView中,从而为单元格提供外观。 The delete button is added to the contentView such that its rect actually lies within the bounds of contentView but only partially overlaps the visible part of the cell (ie the inset subview).删除按钮被添加到contentView ,这样它的 rect 实际上位于 contentView 的边界内,但仅部分重叠单元格的可见部分(即插入子视图)。 So I've got the effect I wanted, and the button is working properly.所以我得到了我想要的效果,并且按钮工作正常。 But I'm still curious about the problem with the original implementation above.但是我仍然对上面原始实现的问题感到好奇。

The problem appears to be with hitTest/pointInside. 问题似乎是hitTest / pointInside。 I'm guessing the cell is returning NO from pointInside if the touch is on the part of the button that is outside the cell and thus the button doesn't get hit tested. 我猜测单元格是从pointInside返回NO,如果触摸位于单元格外部的按钮部分,因此按钮不会受到测试。 To fix this you have to override pointInside on your UICollectionViewCell subclass to take the button into account. 要解决此问题,您必须覆盖UICollectionViewCell子类上的pointInside以将该按钮考虑在内。 You also need to override hitTest to return the button if the touch is inside the button. 如果触摸在按钮内,您还需要覆盖hitTest以返回按钮。 Here are example implementations assuming your button is in a property in the UICollectionViewCell subclass called deleteButton. 下面是示例实现,假设您的按钮位于名为deleteButton的UICollectionViewCell子类中的属性中。

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

Note that because hitTest and pointInside expect the point to be in the coordinate space of the receiver you have to remember to convert the point before calling those methods on the button. 请注意,因为hitTest和pointInside期望该点位于接收器的坐标空间中,所以您必须记住在按钮上调用这些方法之前转换该点。

In Interface Builder do you have set the object as UICollectionViewCell? 在Interface Builder中,您是否将对象设置为UICollectionViewCell? Because erroneously one time I set a UIView and after assign to it the correct UICollectionViewCell class...but doing this things (buttons, labels, ecc.) are not added tor the contentView so they don't respond as they would... 因为错误的一次我设置了一个UIView并在为它分配了正确的UICollectionViewCell类之后......但是这些东西(按钮,标签,ecc。)没有被添加到contentView中,因此它们不会像它们那样响应......

So, remind in IB to take the UICollectionViewCell Object when drawing the interface :) 所以,在IB中提醒在绘制界面时采用UICollectionViewCell对象:)

Swift version: 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)

}

that's it ... for all subViews 这就是...对于所有子视图

I am successfully receiving touches to a button created as follows in the subclassed UICollectionViewCell.m file; 我成功接收到子类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;
}

I added the button in code after realising that buttons added in Storyboard did not work, not sure if this is fixed in latest Xcode. 我意识到在Storyboard中添加的按钮不起作用,我在代码中添加了按钮,不确定这是否在最新的Xcode中修复。

Hope that helps. 希望有所帮助。

As accepted answer requested, we should make a hitTest in order to recieve touches inside the cell. 根据要求接受的答案,我们应该进行一次hitTest以接收细胞内的触摸。 Here is the Swift 4 code for hit test: 这是命中测试的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)
}

I had a similar problem trying to place a deletion button outside the bounds of a uicollectionview cell and it didn't seam to respond to tap events. 我有一个类似的问题,试图在一个uicollectionview单元格的边界之外放置一个删除按钮,它没有接缝来响应点击事件。

the way i solved it was to place a UITapGestureRecognizer on the collection and when a tap happend preform the following code 我解决它的方法是在集合上放置一个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]; 
}

I see two swift conversions of the original answer that aren't exactly swift conversions. 我看到两个原始答案的快速转换并不是完全快速的转换。 So I just want to give the Swift 4 conversion of the original answer so everyone who wants to can use it. 所以我只想给原始答案的Swift 4转换,以便每个想要使用它的人。 You can just paste the code into your subclassed UICollectionViewCell . 您只需将代码粘贴到subclassed UICollectionViewCell Just make sure that you change closeButton with your own button. 只需确保使用您自己的按钮更改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)
}

Honus has the best answer here in my opinion. 在我看来,Honus有最好的答案。 In fact the only one that worked for me so I've been answering other similar questions and sending them this way: 事实上,唯一一个适合我的人,所以我一直在回答其他类似的问题并以这种方式发送:

I spent hours scouring the web for a solution to my UIButton's inside UICollectionView's not working. 我花了几个小时在网上搜索我的UIButton内部UICollectionView的解决方案不起作用。 Driving me nuts until I finally found a solution that works for me. 让我疯了,直到我终于找到适合我的解决方案。 And I believe it's also the proper way to go: hacking the hit tests. 我相信这也是正确的方法:黑客攻击测试。 It's a solution that can go a lot deeper (pun intended) than fixing the UICollectionView Button issues as well, as it can help you get the click event to any button buried under other views that are blocking your events from getting through: 这个解决方案可以比修复UICollectionView Button问题更深入(双关语),因为它可以帮助您将click事件发送到隐藏在阻止事件通过的其他视图下的任何按钮:

UIButton in cell in collection view not receiving touch up inside event 集合视图中的单元格中的UIButton未在内部事件中接收修饰

Since that SO answer was in Objective C, I followed the clues from there to find a swift solution: 由于SO的答案是在Objective C中,我从那里听到了线索,找到了一个快捷的解决方案:

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

-- -

When I would disable user interaction on the cell, or any other variety of answers I tried, nothing worked. 当我在单元格上禁用用户交互,或者我试过的任何其他各种答案时,没有任何效果。

The beauty of the solution I posted above is that you can leave your addTarget's and selector functions how you are used to doing them since they were most likey never the problem. 我上面发布的解决方案的优点在于,您可以将addTarget和selector功能留给您如何使用它们,因为它们从来都不是问题。 You need only override one function to help the touch event make it to its destination. 您只需覆盖一个功能即可帮助触摸事件将其发送到目的地。

Why the solution works: 解决方案的工作原理:

For the first few hours I figured the gesture wasn't being registered properly with my addTarget calls. 在最初的几个小时里,我发现我的addTarget调用没有正确注册手势。 It turns out the targets were registering fine. 事实证明目标是正常的。 The touch events were simply never reaching my buttons. 触摸事件根本就没有到达我的按钮。

The reality seems to be from any number of SO posts and articles I read, that UICollectionView Cells were meant to house one action, not multiple for a variety of reasons. 现实似乎来自我读过的任何数量的SO帖子和文章,UICollectionView Cells旨在容纳一个动作,而不是多个因各种原因。 So you were only supposed to be using the built in selection actions. 所以你应该只使用内置的选择动作。 With that in mind, I believe the proper way around this limitation is not to hack UICollectionView to disable certain aspects of scrolling or user interaction. 考虑到这一点,我认为绕过此限制的正确方法不是破解UICollectionView来禁用滚动或用户交互的某些方面。 UICollectionView is only doing its job. UICollectionView只是在做它的工作。 The proper way is to hack the hit tests to intercept the tap before it gets to UICollectionView and figure out which items they were tapping on. 正确的方法是破解命中测试,以便在它到达UICollectionView之前拦截它,并找出他们正在点击的项目。 Then you simply send a touch event to the button they were tapping on, and let your normal stuff do the work. 然后,您只需将触摸事件发送到他们正在点击的按钮,然后让您的正常工作完成工作。


My final solution (from the khanlou.com article) is to put my addTarget declaration and my selector function wherever I like (in the cell class or the cellForItemAt override), and in the cell class overriding the hitTest function. 我的最终解决方案(来自khanlou.com文章)是将我的addTarget声明和我的选择器函数放在我喜欢的任何地方(在单元格类或cellForItemAt覆盖中),以及在覆盖hitTest函数的单元格类中。

In my cell class I have: 在我的细胞课上,我有:

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

and

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

And in my cell class init I have: 在我的单元类初始化我有:

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

I found this from here py4u.net我从这里找到了这个py4u.net

tried the solution that was at the very bottom.尝试了最底层的解决方案。 (the whole stuff was collected from this page, as I understand) (据我所知,所有内容都是从这个页面收集的)

In my case the colution also worked.在我的情况下,colution 也有效。 and then I just checked wether the User Interaction Enabled checkmark is checked on Collection view in xib and in it's contentView .然后我只是检查了是否在 xib 和它的contentView集合视图上检查了User Interaction Enabled标记。 Guess what.你猜怎么着。 the contentView`s UserInteraction was disabled. contentView 的 UserInteraction 被禁用。

Enabling it fixed the issue with button's touchUpInside event and there was no need to override hitTest method.启用它修复了按钮 touchUpInside 事件的问题,并且不需要覆盖hitTest方法。

You might not have to override hit test or do any of the complicated solutions above.您可能不必覆盖命中测试或执行上述任何复杂的解决方案。

This issue can also be caused if you are using a custom button with lots of subviews.如果您使用带有大量子视图的自定义按钮,也可能导致此问题。

What happens is when the button is hit tested, one of its subviews is being returned.发生的事情是当按钮被点击测试时,它的一个子视图被返回。

A cleaner solution here is just to set userInteractionEnabled = false on all of your button's subviews.这里一个更简洁的解决方案是在所有按钮的子视图上设置userInteractionEnabled = false

That way, hit testing your button will only ever return the button itself and none of the views on it.这样,点击测试你的按钮只会返回按钮本身,而不返回按钮上的任何视图。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM