簡體   English   中英

集合視圖中的單元格中的 UIButton 未接收內部事件的修飾

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

以下代碼表達了我的問題:(它是獨立的,因為您可以使用空模板創建一個 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 ,這樣它的 rect 實際上位於 contentView 的邊界內,但僅部分重疊單元格的可見部分(即插入子視圖)。 所以我得到了我想要的效果,並且按鈕工作正常。 但是我仍然對上面原始實現的問題感到好奇。

問題似乎是hitTest / pointInside。 我猜測單元格是從pointInside返回NO,如果觸摸位於單元格外部的按鈕部分,因此按鈕不會受到測試。 要解決此問題,您必須覆蓋UICollectionViewCell子類上的pointInside以將該按鈕考慮在內。 如果觸摸在按鈕內,您還需要覆蓋hitTest以返回按鈕。 下面是示例實現,假設您的按鈕位於名為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];
}

請注意,因為hitTest和pointInside期望該點位於接收器的坐標空間中,所以您必須記住在按鈕上調用這些方法之前轉換該點。

在Interface Builder中,您是否將對象設置為UICollectionViewCell? 因為錯誤的一次我設置了一個UIView並在為它分配了正確的UICollectionViewCell類之后......但是這些東西(按鈕,標簽,ecc。)沒有被添加到contentView中,因此它們不會像它們那樣響應......

所以,在IB中提醒在繪制界面時采用UICollectionViewCell對象:)

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)

}

這就是...對於所有子視圖

我成功接收到子類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中修復。

希望有所幫助。

根據要求接受的答案,我們應該進行一次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)
}

我有一個類似的問題,試圖在一個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]; 
}

我看到兩個原始答案的快速轉換並不是完全快速的轉換。 所以我只想給原始答案的Swift 4轉換,以便每個想要使用它的人。 您只需將代碼粘貼到subclassed UICollectionViewCell 只需確保使用您自己的按鈕更改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有最好的答案。 事實上,唯一一個適合我的人,所以我一直在回答其他類似的問題並以這種方式發送:

我花了幾個小時在網上搜索我的UIButton內部UICollectionView的解決方案不起作用。 讓我瘋了,直到我終於找到適合我的解決方案。 我相信這也是正確的方法:黑客攻擊測試。 這個解決方案可以比修復UICollectionView Button問題更深入(雙關語),因為它可以幫助您將click事件發送到隱藏在阻止事件通過的其他視圖下的任何按鈕:

集合視圖中的單元格中的UIButton未在內部事件中接收修飾

由於SO的答案是在Objective C中,我從那里聽到了線索,找到了一個快捷的解決方案:

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

-

當我在單元格上禁用用戶交互,或者我試過的任何其他各種答案時,沒有任何效果。

我上面發布的解決方案的優點在於,您可以將addTarget和selector功能留給您如何使用它們,因為它們從來都不是問題。 您只需覆蓋一個功能即可幫助觸摸事件將其發送到目的地。

解決方案的工作原理:

在最初的幾個小時里,我發現我的addTarget調用沒有正確注冊手勢。 事實證明目標是正常的。 觸摸事件根本就沒有到達我的按鈕。

現實似乎來自我讀過的任何數量的SO帖子和文章,UICollectionView Cells旨在容納一個動作,而不是多個因各種原因。 所以你應該只使用內置的選擇動作。 考慮到這一點,我認為繞過此限制的正確方法不是破解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)

我從這里找到了這個py4u.net

嘗試了最底層的解決方案。 (據我所知,所有內容都是從這個頁面收集的)

在我的情況下,colution 也有效。 然后我只是檢查了是否在 xib 和它的contentView集合視圖上檢查了User Interaction Enabled標記。 你猜怎么着。 contentView 的 UserInteraction 被禁用。

啟用它修復了按鈕 touchUpInside 事件的問題,並且不需要覆蓋hitTest方法。

您可能不必覆蓋命中測試或執行上述任何復雜的解決方案。

如果您使用帶有大量子視圖的自定義按鈕,也可能導致此問題。

發生的事情是當按鈕被點擊測試時,它的一個子視圖被返回。

這里一個更簡潔的解決方案是在所有按鈕的子視圖上設置userInteractionEnabled = false

這樣,點擊測試你的按鈕只會返回按鈕本身,而不返回按鈕上的任何視圖。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM