【问题标题】:How can I increase the Tap Area for UIButton?如何增加 UIButton 的点击区域?
【发布时间】:2015-06-25 17:16:06
【问题描述】:

我使用带有自动布局的UIButton。当图像很小时,点击区域也很小。我可以想象几种方法来解决这个问题:

  1. 增加图像大小,即在图像周围放置一个透明区域。这不好,因为当您定位图像时,您必须牢记额外的透明边框。
  2. 使用 CGRectInset 并增加大小。这不适用于自动布局,因为使用自动布局它会回退到原始图像大小。

除了以上两种方法,有没有更好的办法来增加 UIButton 的点击面积?

【问题讨论】:

    标签: ios objective-c swift uibutton


    【解决方案1】:

    您可以简单地调整按钮的内容插图以获得所需的大小。在代码中,它将如下所示:

    button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
    //Or if you specifically want to adjust around the image, instead use button.imageEdgeInsets
    

    在界面生成器中,它将如下所示:

    【讨论】:

    • 应该给负值还是请解释
    • 正值 - 几乎就像 CSS 中的“边距”。
    • 这不起作用。 AutoLayout 将尊重插图,移动按钮的位置,这不是我们想要的。相反,我们只希望影响命中检测,而不是布局。
    • 我很惊讶这是公认的答案。它不起作用,框架在扭曲图像时保持不变。
    • 我昨天创建了一个示例项目,看看它是否仍然有效并且确实有效。如果它扭曲了图像,那是因为图像视图的 contentMode 设置不正确。
    【解决方案2】:

    很容易。创建一个自定义 UIButton 类。然后重写 pointInside... 方法并根据需要更改值。

    #import "CustomButton.h"
    
    @implementation CustomButton
    
    -(BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
        CGRect newArea = CGRectMake(self.bounds.origin.x - 10, self.bounds.origin.y - 10, self.bounds.size.width + 20, self.bounds.size.height + 20);
        
        return CGRectContainsPoint(newArea, point);
    }
    @end
    

    每边将需要更多的 10 点触摸区域。

    还有 Swift 5 版本:

    class CustomButton: UIButton {
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            return bounds.insetBy(dx: -10, dy: -10).contains(point)
        }
    }
    

    【讨论】:

    • 同样的问题 - 如果按钮被放置在小视图中并且您需要在该视图上扩展可触摸区域怎么办?
    • @VyachaslavGerchicov ,使用大的透明视图作为按钮的父级以便于解决。
    • 请务必使用有意义的名称,例如 LargeTapAreaButton
    【解决方案3】:

    我确认 Syed 的解决方案即使使用自动布局也能正常工作。这是 Swift 4.x 版本:

    import UIKit
    
    class BeepSmallButton: UIButton {
    
        // MARK: - Functions
    
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
            let newArea = CGRect(
                x: self.bounds.origin.x - 5.0,
                y: self.bounds.origin.y - 5.0,
                width: self.bounds.size.width + 10.0,
                height: self.bounds.size.height + 20.0
            )
            return newArea.contains(point)
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    

    【讨论】:

    • 这应该是公认的答案。效果很棒,只需处理按钮周围更大的区域并允许它接收事件
    • 我发现这对我的情况没有任何影响,我认为这是因为按钮的 UIImageView 正在抓住触摸。我怎样才能防止这种情况?这让我完全疯了......
    • @mrwheet button.imageView?.isUserInteractionEnabled = false
    【解决方案4】:

    您可以在情节提要中或通过代码设置按钮 EdgeInsets。按钮的大小在高度和宽度上应大于设置为按钮的图像。

    注意:Xcode8之后,可以在size inspecor中设置content inset

    或者您也可以在点击图像视图时使用带有点击手势的图像视图来执行操作。确保为情节提要上的图像视图勾选用户交互启用,以便手势工作。 使图像视图大于图像以在其上设置并在其上设置图像。 现在将图像视图图像的模式设置为以故事板/界面构建器为中心。

    您可以点击图片进行操作。

    希望对您有所帮助。

    【讨论】:

      【解决方案5】:

      这应该可以工作

      import UIKit
      
      @IBDesignable
      class GRCustomButton: UIButton {
      
          @IBInspectable var margin:CGFloat = 20.0
          override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
              //increase touch area for control in all directions by 20
      
              let area = self.bounds.insetBy(dx: -margin, dy: -margin)
              return area.contains(point)
          }
      
      }
      

      【讨论】:

      • 谢谢。这似乎是没有副作用的正确答案。
      【解决方案6】:

      基于 Syed 回答的 Swift 5 版本(更大区域的负值):

      override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
          return bounds.insetBy(dx: -10, dy: -10).contains(point)
      }
      

      或者:

      override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
          return bounds.inset(by: UIEdgeInsets(top: -5, left: -5, bottom: -5, right: -5)).contains(point)
      }
      

      【讨论】:

      • 如果按钮被插入到另一个尺寸小于可触摸区域的视图中怎么办?
      【解决方案7】:

      关于边缘插入答案的一些上下文。

      当使用自动布局结合内容边缘插入时,您可能需要更改约束。

      假设您有一个 10x10 的图像,并且您希望将其设置为 30x30 以获得更大的命中区域:

      1. 将您的自动布局约束设置为所需的更大区域。如果你 立即构建这将拉伸图像。

      2. 使用内容边缘插入来缩小可用空间 图像,所以它匹配正确的大小。在这个例子中,10 10 10 10. 为图像留出 10x10 的空间来绘制自己。

      3. 赢了。

      【讨论】:

      • 超级有帮助。感谢您“修复”上述解决方案。这对我来说是一个优越得多的解决方案,因为它不涉及自定义类。用一个例子再次强调第一点:如果您之前将此按钮固定在距左上角 32 个像素的位置,您还应该从顶部和左侧约束中减去 10,将这两个常量都减小到 22
      【解决方案8】:

      子类UIButton并添加这个函数

      override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
          let verticalInset = CGFloat(10)
          let horizontalInset = CGFloat(10)
      
          let largerArea = CGRect(
              x: self.bounds.origin.x - horizontalInset,
              y: self.bounds.origin.y - verticalInset,
              width: self.bounds.size.width + horizontalInset*2,
              height: self.bounds.size.height + verticalInset*2
          )
      
          return largerArea.contains(point)
      }
      

      【讨论】:

        【解决方案9】:

        Swift 4 • Xcode 9

        您可以通过编程方式选择 -

        图片 -

        button.imageEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
        

        对于标题 -

        button.titleEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
        

        【讨论】:

        • 这会放大按钮上的任何图像,这不是我们想要的。
        • @Whomble。如果您不想要更大的背景区域。只需在 UIImage 上添加一个有用的 Ui 按钮
        【解决方案10】:

        我解决这个问题的方法是使用contentEdgeInsets(它就像按钮内容之外的边距)在小图像周围为按钮提供一些额外空间,但也使用相同的插图覆盖alignmentRect 属性,这会将自动布局使用的矩形带回 in 到图像中。这可确保自动布局使用较小的图像而不是按钮的完整可点击范围来计算其约束。

        class HIGTargetButton: UIButton {
          override init(frame: CGRect) {
            super.init(frame: frame)
          }
          
          required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
          }
          
          override func setImage(_ image: UIImage?, for state: UIControl.State) {
            super.setImage(image, for: state)
            guard let image = image else { return }
            let verticalMarginToAdd = max(0, (targetSize.height - image.size.height) / 2)
            let horizontalMarginToAdd = max(0, (targetSize.width - image.size.width) / 2)
            let insets = UIEdgeInsets(top: verticalMarginToAdd,
                                             left: horizontalMarginToAdd,
                                             bottom: verticalMarginToAdd,
                                             right: horizontalMarginToAdd)
            contentEdgeInsets = insets
          }
          
          override var alignmentRectInsets: UIEdgeInsets {
            contentEdgeInsets
          }
          
          private let targetSize = CGSize(width: 44.0, height: 44.0)
        }
        

        粉红色按钮具有更大的可点击目标(此处显示为粉红色,但可能是.clear)和较小的图像 - 它的前沿与基于图标的绿色视图的前沿对齐,而不是整个按钮。

        【讨论】:

          【解决方案11】:

          这里介绍的两种解决方案都可以工作......在适当的情况下。但这里有一些你可能会遇到的问题。 首先是不完全明显的:

          • 点击必须在按钮内,稍微触摸按钮边界不起作用。如果按钮非常小,您的大部分手指很可能会在按钮之外,因此无法点击。

          具体到上面的解决方案:

          解决方案 1 @Travis:

          使用 contentEdgeInsets 在不增加图标/文本大小的情况下增加按钮大小,类似于添加填充

          button.contentEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
          

          这个很简单,增加按钮大小会增加点击区域。

          • 如果您设置了高度/宽度框架或约束,显然这不会起到太大作用,只会扭曲或移动您的图标/文本。
          • 按钮尺寸会更大。在布置其他视图时必须考虑这一点。 (根据需要偏移其他视图)

          解决方案 2 @Syed Sadrul Ullah Sahad:

          继承 UIButton 并覆盖 point(inside point: CGPoint, with event: UIEvent?) -> Bool

          class BigAreaButton: UIButton {
              
              override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
                  return bounds.insetBy(dx: -20, dy: -20).contains(point)
              }
          }
          

          这个解决方案很棒,因为它可以让您在不改变布局的情况下将点击区域扩展到视图边界之外,但这里有一些问题:

          • 父视图需要有背景,将按钮放入没有背景的空 ViewController 中是行不通的。
          • 如果按钮是嵌套的,则视图层次结构中的所有视图都需要提供足够的“空间”或覆盖指向。 例如
          
          ---------
          |       |
          |oooo   |
          |oXXo   |
          |oXXo   |
          |oooo   | Button-X nested in View-o will NOT extend beyond View-o
          --------- 
          

          【讨论】:

            【解决方案12】:

            子类化的替代方法是扩展 UIControl,向其添加 touchAreaInsets 属性 - 通过利用 objC 运行时 - 并调整 pointInside:withEvent

            #import <objc/runtime.h>
            #import <UIKit/UIKit.h>
            
            #import "NSObject+Swizzling.h" // This is where the magic happens :)
            
            
            @implementation UIControl (Extensions)
            
            @dynamic touchAreaInsets;
            static void * CHFLExtendedTouchAreaControlKey;
            
            + (void)load
            {
                static dispatch_once_t onceToken;
                dispatch_once(&onceToken, ^{
                    [self swizzleSelector:@selector(pointInside:withEvent:) withSelector:@selector(chfl_pointInside:event:) classMethod:NO];
                });
            }
            
            - (BOOL)chfl_pointInside:(CGPoint)point event:(UIEvent *)event
            {
                if(UIEdgeInsetsEqualToEdgeInsets(self.touchAreaInsets, UIEdgeInsetsZero)) {
                    return [self chfl_pointInside:point event:event];
                }
                
                CGRect relativeFrame = self.bounds;
                CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.touchAreaInsets);
                
                return CGRectContainsPoint(hitFrame, point);
            }
            
            - (UIEdgeInsets)touchAreaInsets
            {
                NSValue *value = objc_getAssociatedObject(self, &CHFLExtendedTouchAreaControlKey);
                if (value) {
                    UIEdgeInsets touchAreaInsets; [value getValue:&touchAreaInsets]; return touchAreaInsets;
                }
                else {
                    return UIEdgeInsetsZero;
                }
            }
            
            - (void)setTouchAreaInsets:(UIEdgeInsets)touchAreaInsets
            {
                NSValue *value = [NSValue value:&touchAreaInsets withObjCType:@encode(UIEdgeInsets)];
                objc_setAssociatedObject(self, &CHFLExtendedTouchAreaControlKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            }
            
            @end
            

            这里是NSObject+Swizzling.h https://gist.github.com/epacces/fb9b8e996115b3bfa735707810f41ec8

            这是一个非常通用的界面,可以让您减少/增加UIControls 的触摸区域。

            #import <UIKit/UIKit.h>
            
            /**
             *  Extends or reduce the touch area of any UIControls
             *
             *  Example (extends the button's touch area by 20 pt):
             *
             *  UIButton *button = [[UIButton alloc] initWithFrame:CGRectFrame(0, 0, 20, 20)]
             *  button.touchAreaInsets = UIEdgeInsetsMake(-10.0f, -10.0f, -10.0f, -10.0f);
             */
            
            @interface UIControl (Extensions)
            @property (nonatomic, assign) UIEdgeInsets touchAreaInsets;
            @end
            

            【讨论】:

              【解决方案13】:

              如果您为按钮使用 Material 的 iOS 库,您可以使用 hitAreaInsets 来增加按钮的触摸目标大小。

              来自https://material.io/components/buttons/ios#using-buttons的示例代码

              let buttonVerticalInset =
                min(0, -(kMinimumAccessibleButtonSize.height - button.bounds.height) / 2);
              let buttonHorizontalInset =
                min(0, -(kMinimumAccessibleButtonSize.width - button.bounds.width) / 2);
              button.hitAreaInsets =
                UIEdgeInsetsMake(buttonVerticalInset, buttonHorizontalInset,
              buttonVerticalInset, buttonHorizontalInset);
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2010-10-22
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-07-31
                • 2012-10-26
                • 2013-10-15
                相关资源
                最近更新 更多