【问题标题】:Why is the top portion of my UISegmentedControl not tappable?为什么我的 UISegmentedControl 的顶部不可点击?
【发布时间】:2013-07-15 01:44:15
【问题描述】:

当我在手机上玩游戏时,我注意到我的 UISegmentedControl 响应不快。让我的水龙头注册需要 2 次或更多次尝试。所以我决定在模拟器中运行我的应用程序,以更准确地探测问题所在。通过用鼠标单击数十次,我确定 UISegmentedControl 的前 25% 没有响应(在下面的屏幕截图中,该部分用 Photoshop 以红色突出显示)。我不知道有任何不可见的 UIView 可能会阻止它。你知道如何让整个控件可以点击吗?

self.segmentedControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Uno", @"Dos", nil]];
self.segmentedControl.selectedSegmentIndex = 0;
[self.segmentedControl addTarget:self action:@selector(segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
self.segmentedControl.height = 32.0;
self.segmentedControl.width = 310.0;
self.segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
self.segmentedControl.tintColor = [UIColor colorWithWhite:0.9 alpha:1.0];
self.segmentedControl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;

UIView* toolbar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, HEADER_HEIGHT)];
toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
CAGradientLayer *gradient = [CAGradientLayer layer];
    gradient.frame = CGRectMake(
        toolbar.bounds.origin.x,
        toolbar.bounds.origin.y,
        // * 2 for enough slack when iPad rotates
        toolbar.bounds.size.width * 2,
        toolbar.bounds.size.height
    );
    gradient.colors = [NSArray arrayWithObjects:
        (id)[[UIColor whiteColor] CGColor],
        (id)[[UIColor 
            colorWithWhite:0.8
            alpha:1.0
            ] CGColor
        ],
        nil
];
[toolbar.layer insertSublayer:gradient atIndex:0];
toolbar.backgroundColor = [UIColor navigationBarShadowColor];
[toolbar addSubview:self.segmentedControl];

UIView* border = [[UIView alloc] initWithFrame:CGRectMake(0, HEADER_HEIGHT - 1, toolbar.width, 1)];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
border.backgroundColor = [UIColor colorWithWhite:0.7 alpha:1.0];
border.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[toolbar addSubview:border];

[self.segmentedControl centerInParent];

self.tableView.tableHeaderView = toolbar;

http://scs.veetle.com/soget/session-thumbnails/5363e222d2e10/86a8dd984fcaddee339dd881544ecac7/5363e222d2e10_86a8dd984fcaddee339dd881544ecac7_20140509171623_536d6fd78f503_68_896x672.jpg

【问题讨论】:

  • HEADER_HEIGHT 是什么值(假设它是宏或常量)?这段代码是从哪里调用的 - viewDidLoad?
  • HEADER_HEIGHT = 42.0; 代码在viewDidLoad[super viewDidLoad]之后调用。我使用UIView+Position 类别,它具有方便的属性,例如 x、y、宽度、高度和 centerInParent。
  • 这可能有助于stackoverflow.com/a/9719364/488611。导航栏触摸区域在下方延伸,以便于点击。
  • 你试过打开模拟器选项Debug -> Color blended layers吗?我也许可以给你看一些你不知道的覆盖物,它们会阻止你触摸
  • 如果有任何安慰:您在 Apple 自己的应用程序中遇到相同的行为,当他们使用导航栏下方的分段控件时。

标签: ios cocoa-touch uisegmentedcontrol


【解决方案1】:

正如其他答案中已经写的那样,UINavigationBar 会抓住导航栏本身附近的触摸,但不是因为它有一些子视图延伸到边缘:这不是原因。

如果您记录整个视图层次结构,您会看到 UINavigationBar 没有延伸到定义的边缘。

它收到触摸的原因是另一个:

在 UIKit 中,有很多“特殊情况”,这就是其中之一。

当您点击屏幕时,会启动一个称为“命中测试”的过程。从第一个 UIWindow 开始,所有视图都被要求回答两个“问题”:点是否在您的边界内被点击?必须接收触摸事件的子视图是什么?

这个问题是通过这两种方法来回答的:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

好的,现在我们可以继续了。

点击后,UIApplicationMain 开始点击测试过程。命中测试从主 UIWindow 开始(例如,甚至在状态栏窗口和警报视图窗口上执行),并遍历所有子视图。

这个过程执行了3次:

  • 从 UIWindow 开始两次
  • 从_UIApplicationHandleEvent开始一次

如果您点击导航栏,您将看到 UIWindow 上的hitTest 将返回 UINavigationBar(全部 3 次)

如果你点击导航栏下方的区域,你会看到一些奇怪的东西:

  • 前两个 hitTest 将返回您的 UISegmentedControl
  • 最后的 hitTest 将返回 UINavigationBar

为什么会这样? 如果你 swizzle 和子类 UIView,覆盖 hitTest,你会看到前两次点击点是正确的。第三次,一些事情改变了点,比如point - 15(或类似的数字)

经过大量搜索,我找到了发生这种情况的地方:

UIWindow 有一个名为的(私有)方法

-(CGPoint)warpPoint:(CGPoint)point;

调试它,我看到如果点击点位于状态栏的正下方,则此方法会更改点击点。 调试更多,我看到使这成为可能的堆栈调用只有 3 个:

[UINavigationBar, _isChargeEnabled]
[UINavigationBar, isEnabled]
[UINavigationBar, _isAlphaHittableAndHasAlphaHittableAncestors]

因此,最后,此warpPoint 方法检查 UINavigationBar 是否已启用且可点击,如果是,则它“扭曲”了这一点。该点扭曲了 0 到 15 之间的多个像素,当您靠近导航栏时,这种“扭曲”会增加。

既然您知道幕后发生了什么,您就必须知道如何避免它(如果您愿意的话)。

如果应用程序必须在 AppStore 上运行,您不能简单地覆盖 warpPoint::这是一个私有方法,您的应用程序将被拒绝。

你必须找到另一个系统(就像建议的那样,覆盖 sendEvent,但我不确定它是否会工作)

因为这个问题很有趣,我明天会考虑一个合法的解决方案并更新这个答案(一个好的起点可以是子类化 UINavigationBar,覆盖 hitTest 和 pointInside,如果给定多次调用的相同事件,则返回 nil/false,重点改变了。但我必须明天测试它是否有效)

编辑

好的,我尝试了很多解决方案,但要找到一个合法且稳定的解决方案并不容易。 我已经描述了系统的实际行为,这可能会因不同版本而异(hitTest 调用多于或少于 3 次,warpPoint 会扭曲大约 15px 的点,可以改变 ecc ecc)。

最稳定的显然是UIWindow子类中warpPoint:的非法覆盖:

-(CGPoint)warpPoint:(CGPoint)point;
{
    return point;
}

但是,我发现像这样的方法(在 UIWindow 子类中)足够稳定并且可以解决问题:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // this method is not safe if you tap the screen two times at the same x position and y position different for 16px, because it moves the point
    if (self.lastPoint.x == point.x)
    {
        // the points are on the same vertical line
        if ((0 < (self.lastPoint.y - point.y)) && ((self.lastPoint.y - point.y) < 16) )
        {
            // there is a differenc of ~15px in the y position?
            // if so, the point has been changed
            point.y = self.lastPoint.y;
        }
    }

    self.lastPoint = point;

    return [super hitTest:point withEvent:event];
}

此方法记录最后点击的点,如果后续点击在相同的 x 处,并且 y 不同,最大 16px,则使用前一个点。 我已经测试了很多,它看起来很稳定。 如果需要,您可以添加更多控件以仅在特定控制器中启用此行为,或者仅在窗口的已定义部分 ecc ecc 上启用此行为。 如果我找到其他解决方案,我会更新帖子

【讨论】:

  • 混搭warpPoint: 有用吗?我不确定这是否非法,因为您实际上并没有调用该方法。
  • 这是非法的,因为你在调配私有方法。似乎 Apple 使用静态分析器在可执行文件中查找私有符号,因此如果您尝试在运行时调整从字符串构造选择器的方法(例如:@"war" + @"P" + @"oint") ,Apple 可能很难找到您使用私有方法(但这仍然是非法的)。找到合法的解决方案并不简单,现在我发布我找到的更简单的方法
【解决方案2】:

我认为问题在于 UINavigationBar 中的按钮具有比正常触摸区域更大的触摸区域。请参阅此 SOpost。您还可以通过“UINavigationBar touch area”谷歌搜索找到大量关于此的讨论。

作为一种可能的解决方案,您可以将分段控件放在导航栏中,但您会比我更清楚这是否适合您的用例。

【讨论】:

  • 我需要把分段控件放在导航栏下面,因为导航栏没有空格。
【解决方案3】:

我想出了一个替代解决方案,在我看来它比 LombaX 的更安全。它利用两个事件都带有相同时间戳的事实来拒绝后续事件。

@interface RFNavigationBar ()

@property (nonatomic, assign) NSTimeInterval lastOutOfBoundsEventTimestamp;

@end

@implementation RFNavigationBar

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // [rfillion 2014-03-28]
    // UIApplication/UIWindow/UINavigationBar conspire against us. There's a band under the UINavigationBar for which the bar will return
    // subviews instead of nil (to make those tap targets larger, one would assume). We don't want that. To do this, it seems to end up
    // calling -hitTest twice. Once with a value out of bounds which is easy to check for. But then it calls it again with an altered point
    // value that is actually within bounds. The UIEvent it passes to both seem to be the same. However, we can't just compare UIEvent pointers
    // because it looks like these get reused and you end up rejecting valid touches if you just keep around the last bad touch UIEvent. So
    // instead we keep around the timestamp of the last bad event, and try to avoid processing any events whose timestamp isn't larger.
    if (point.y > self.bounds.size.height)
    {
        self.lastOutOfBoundsEventTimestamp = event.timestamp;
        return nil;
    }
    if (event.timestamp <= self.lastOutOfBoundsEventTimestamp + 0.001)
    {
        return nil;
    }
    return [super hitTest:point withEvent:event];
}

@end

【讨论】:

  • 很棒的解决方案 - 唯一在 iOS 9 上对我有用的解决方案。
【解决方案4】:

您可能想要检查哪个视图正在记录触摸。试试这个方法-

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    [touch locationInView:self.view];
    if([touch.view isKindOfClass:[UISegmentedControl class]])
    {
      NSLog(@"This is UISegment");
    }
    else if([touch.view isKindOfClass:[UITabBar class]]) 
    {
      NSLog(@"This is UITabBar");
    } else if(...other views...) {
        ...
    }
 }

一旦你弄清楚了,你也许可以缩小你的问题范围。

【讨论】:

  • 这个函数应该在哪个视图中定义?
  • 处理“Supernetting & CIDR”的视图控制器
  • 我将该功能添加到我的视图控制器中。我在第一行放了一个断点,但它永远不会在那里中断。
【解决方案5】:

看起来好像您正在使用类别扩展来设置视图的宽度/高度,并将它们置于父视图的中心。也许这里有一个隐藏的问题——你能在没有这个类别的情况下重构你的布局吗?

我将您的代码复制到一个干净的项目中并在 UITableViewController 的 viewDidLoad 方法中运行它 - 它工作正常,并且我没有像您报告的死点。我不得不稍微更改您的代码,因为我没有您使用的相同类别扩展。

另外,如果您在 viewDidLoad 中运行此代码,您应该验证您的视图是否具有定义的大小(您访问您的 view.width)。如果您以编程方式创建 UITableViewController(相对于 nib/storyboard),那么框架可能是 CGRectZero。我的是从笔尖加载的,所以框架是预设的。

我也会尝试暂时删除您的边框视图,看看它是否是罪魁祸首。

【讨论】:

  • 我的 UIView 类别很简单。 - (CGFloat) width { return self.frame.size.width; }- (void) setWidth:(CGFloat)newWidth { self.frame = CGRectMake(self.x, self.y, newWidth, self.height); }。其余功能非常相似。在过去的 2 年中,这些都经过了良好的单元测试和使用。我确实把我的代码放在了viewDidLoadself.view 没有问题。它的框架如下:x: 0.000000, y: 20.000000, w: 320.000000, h: 460.000000。移除边框并没有解决问题。
  • 你能把你的代码放到一个干净的 UITableViewController 中,看看它是否在那里复制吗?我没有。
【解决方案6】:

我建议您避免在靠近导航栏或工具栏的位置使用触敏 UI。这些区域通常被称为“倾斜因素”,使用户更容易在按钮上执行触摸事件,而不会遇到执行精确触摸的困难。例如,UIButtons 也是如此。

但如果你想在导航栏或工具栏接收到触摸事件之前捕获它,你可以继承 UIWindow 并覆盖:-(void)sendEvent:(UIEvent *)event;

【讨论】:

    【解决方案7】:

    一个简单的调试方法是尝试在您的项目中使用DCIntrospect。这是一个非常易于使用/实现的库,可以在模拟器中轻松找出哪些视图在哪里。

    1. 安装库并配置它
    2. 在模拟器中运行应用程序并导航到出现问题的屏幕
    3. 按键盘上的空格键(计算机键盘,而不是模拟器的 键盘)
    4. 点击 25% 的区域,看看突出显示的内容。

    如果突出显示的不是分段视图控制器,则该视图可能是覆盖触摸事件的内容。

    【讨论】:

    • 由于@LombaX 描述的扭曲机制,这并不能解决问题。
    【解决方案8】:

    为 UINavigationBar 创建一个协议:(添加新文件并粘贴以下代码)

    /******** file: UINavigationBar+BelowSpace.h*******/
    
    "UINavigationBar+BelowSpace.h"
    
        #import <Foundation/Foundation.h>
    
    @interface UINavigationBar (BelowSpace)
    
    @end
    
    /*******- file: UINavigationBar+BelowSpace.m*******/
    
    #import "UINavigationBar+BelowSpace.h"
    
    @implementation UINavigationBar (BelowSpace)
    
    
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        int errorMargin = 5;// space left to decrease the click event area
        CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height);
        BOOL isTouchAllowed =  (CGRectContainsPoint(smallerFrame, point) == 1);
    
        if (isTouchAllowed) {
            self.userInteractionEnabled = YES;
        } else {
            self.userInteractionEnabled = NO;
        }
        return [super hitTest:point withEvent:event];
    }
    @end
    

    希望对你有所帮助^^

    【讨论】:

    • 这不是协议。它是一个类别。
    【解决方案9】:

    试试这个

    self.navigationController!.navigationBar.userInteractionEnabled = false;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-18
      • 2017-10-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-31
      相关资源
      最近更新 更多