Custom shape button implementation

For most iOS development, this is very rare, after all, an irregular button is not common on mobile, but it is inevitable to encounter some special requirements, such as a pie chart, such as a pie chart, such as a map of each province in China….. It’s very likely that at some point in development we’ll need to implement irregularly shaped buttons, so how do we do that? In fact, this is only a very small knowledge point, but a lot of people will not consider, so encountered such a situation

One day, my friend gave me a set of product prototypes, and he asked me to help him write some buttons, which were very simple. I felt confused at that time, but I didn’t think too much about it. I thought I had a little spare time, so I could help, so he sent me the prototype drawing

Oh, it’s a simple 4+1 button. First write 4 square buttons in the format of nine, and then add a circular button in the center. And then he sent us the blueprints

Sure enough, after having a design, instantly look much better, but imagination is not complicated. First use a view as the base, and then 5 buttons on the view, and then the view to a radius, isn’t the end of it? But is it really that simple?

Obviously not. One obvious result is that you will find that you use the unshown part of the circle radius and it will still respond to button clicks. What is this? The simple principle is that although we set the radius so that the layer outside the circle is not displayed, it does not mean that it does not exist, it is still clickable, which in some cases does not meet the design standards of buttons, because what we want is wySIWYG

Then what YOU see is what you get becomes my goal. Do not think so simple. Although this case is a simple button with a relatively normal shape, I need to imagine it as a very complex irregular figure to deal with

1. Since it is a custom shape button, it is still a button. There is no need to customize complex controls, just inherit UIButton

2. Design how to achieve a special shape in fact, this is easy to think of, to achieve a circular button, is to modify the layer shape

If you’ve read advanced iOS Core Animation Tips, you can easily see what we’re looking for

Yes, here we can see several key points, an original background + a mask = a custom shape of the graph

Reference to the button we want, we will find the same applies, so do we have to make the mask in the applicable image? Obviously, we can’t do that in development, so how do we customize a shape?

So we can easily use UIBezierPath to draw the shape we want, and then use CAShapeLayer to create the path of the layer based on the shape and once we’re done, we can use this layer as the mask for the button

So one of the things that’s useful for us is a Bessel curve customization that’s very simple to implement with an irregular button class

#import <UIKit/UIKit.h>

@interface IrregularButton : UIButton

@property(nonatomic, strong)UIBezierPath *maskPath;

@end


#import "IrregularButton.h"

@implementation IrregularButton

-(void)setMaskPath:(UIBezierPath *)maskPath{
    _maskPath = maskPath;
    CAShapeLayer *layer = [[CAShapeLayer alloc]init];
    layer.path = maskPath.CGPath;
    self.layer.mask = layer;
}

@end

Copy the code

When we need to customize a special button, we simply inherit the class, or use the class directly, and give the maskPath a desired path

So the above four semicircular buttons or even the full circle button we can easily achieve

#import "IrregularButton.h"

typedef NS_ENUM(NSUInteger, QuarterCircleType) {
    QuarterCircleTypeTopNone,
    QuarterCircleTypeTopLeft,
    QuarterCircleTypeTopRight,
    QuarterCircleTypeBottomLeft,
    QuarterCircleTypeBottomRight,
    QuarterCircleTypeAllCircle,
};

@interface QuarterCircleButton : IrregularButton

@property(nonatomic, assign)QuarterCircleType circleType;

@end

#import "QuarterCircleButton.h"

@implementation QuarterCircleButton

-(void)setFrame:(CGRect)frame{
    [super setFrame:frame];
    
    self.circleType = self.circleType;
}

-(void)setCircleType:(QuarterCircleType)circleType{
    _circleType = circleType;
    
    CGFloat width = self.frame.size.width;
    CGFloat height = self.frame.size.height;
    switch (circleType) {
        case QuarterCircleTypeTopLeft:{
            CGPoint bottomRightPoint = CGPointMake(width, height);
            CGPoint bottomLeftPoint = CGPointMake(0, height);
            
            UIBezierPath *path = [[UIBezierPath alloc]init];
            [path moveToPoint:bottomRightPoint];
            [path addLineToPoint:bottomLeftPoint];
            [path addArcWithCenter:bottomRightPoint radius:MAX(width, height) startAngle:M_PI endAngle:-M_PI_2 clockwise:YES];
            [path addLineToPoint:bottomRightPoint];
            [path closePath];
            self.maskPath = path;
        }
            break;
        case QuarterCircleTypeTopRight:{
            CGPoint bottomLeftPoint = CGPointMake(0, height);
            CGPoint topLeftPoint = CGPointMake(0, 0);
            
            UIBezierPath *path = [[UIBezierPath alloc]init];
            [path moveToPoint:bottomLeftPoint];
            [path addLineToPoint:topLeftPoint];
            [path addArcWithCenter:bottomLeftPoint radius:MAX(width, height) startAngle:-M_PI endAngle:0 clockwise:YES];
            [path addLineToPoint:bottomLeftPoint];
            [path closePath];
            self.maskPath = path;
        }
            break;
        case QuarterCircleTypeBottomLeft:{
            CGPoint topRightPoint = CGPointMake(width, 0);
            CGPoint bottomRightPoint = CGPointMake(width, height);
            
            UIBezierPath *path = [[UIBezierPath alloc]init];
            [path moveToPoint:topRightPoint];
            [path addLineToPoint:bottomRightPoint];
            [path addArcWithCenter:topRightPoint radius:MAX(width, height) startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
            [path addLineToPoint:topRightPoint];
            [path closePath];
            self.maskPath = path;
        }
            break;
        case QuarterCircleTypeBottomRight:{
            CGPoint topLeftPoint = CGPointMake(0, 0);
            CGPoint topRightPoint = CGPointMake(width, 0);
            
            UIBezierPath *path = [[UIBezierPath alloc]init];
            [path moveToPoint:topLeftPoint];
            [path addLineToPoint:topRightPoint];
            [path addArcWithCenter:topLeftPoint radius:MAX(width, height) startAngle:0 endAngle:M_PI_2 clockwise:YES];
            [path addLineToPoint:topLeftPoint];
            [path closePath];
            self.maskPath = path;
        }
            break;
        case QuarterCircleTypeAllCircle:{
            CGPoint centerPoint = CGPointMake(width/2, height/2);
            
            UIBezierPath *path = [[UIBezierPath alloc]init];
            [path addArcWithCenter:centerPoint radius:MAX(width/2, height/2) startAngle:0 endAngle:M_PI * 2 clockwise:YES];
            [path closePath];
            self.maskPath = path;
        }
            break;
            
        default:
            break; }}Copy the code

So, it was very easy for me to create something like this

The four areas outside the semicircle can still be clicked accordingly. To explain, the area outside the mask can not be seen, but it still belongs to the area of the button and can still respond to clicking.

So we need to circumvent it so how do we circumvent it? It’s still an irregular shape, isn’t it? In fact, we have prepared for the early….

3. To avoid the click outside the irregular area, we just need to simply implement the method of whether the click responds, so the complete implementation of the irregular button is like this

#import <UIKit/UIKit.h>

@interface IrregularButton : UIButton

@property(nonatomic, strong)UIBezierPath *maskPath;

@end


#import "IrregularButton.h"

@implementation IrregularButton

-(void)setMaskPath:(UIBezierPath *)maskPath{
    _maskPath = maskPath;
    CAShapeLayer *layer = [[CAShapeLayer alloc]init];
    layer.path = maskPath.CGPath;
    self.layer.mask = layer;
}

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    BOOL res = [super pointInside:point withEvent:event];
    if (res) {
        if(! self.maskPath || [self.maskPath containsPoint:point]) {return YES;
        }
        return NO;
    }
    return res;
}

@end
Copy the code

We just need to make sure that the point we click is within the area of our mask (here we need to make sure that our mask is a closed area). Clicking is allowed within the area of mask, but not outside the area. So a maskPath was used twice, the first time to set the mask to show the irregular button, and the second time to determine the response area when clicked

At this point, the base class button can adapt to various custom shapes. When we want to implement this, we just draw the shape we want from the irregular shape in the prototype to create a Bezier curve. (We have to draw this part ourselves because the shape is different.)

We also have third-party tools

All right, here’s the demo… Github.com/spicyShrimp…