The Shapes library offers a way to customize the shape of existing components, or expand your own view to have a custom shape.
Currently we offer built-in functionality for shapes within our Buttons and Cards components.
Our shapes library consists of different APIs and classes you can use to build a shape.
As you will see throughout the doc, we will be talking plentifully about the shapeGenerator
object as part of our view/component. This object holds the generated shape and must conform to the MDCShapeGenerating
protocol. MDCShapeGenerating
consists of one method that returns the shape’s CGPath
for the expected size.
(CGPath *)pathForSize:(CGSize)size
For our convenience we have a few classes under the hood that help us generate a shape. At the core we have MDCPathGenerator
that has a few helper methods to build your own custom CGPath
that will serve as the path for your shape.
We also have an MDCShapedShadowLayer
class that has to be used as the base layer of your view instead of MDCShadowLayer
to allow shapes to work well with shadows, borders, and also background color. This is needed because such attributes must follow the shape's path rather than the normal bounds of the view.
MDCShapedView
is a base UIView
that already incorporates MDCShapedShadowLayer
, a shapeGenerator
object, and elevation support to provide a minimal view that has full shape support. This can be used as a basic building block to build on top of when building new components that need shape support from the get go.
MDCCornerTreatment
and MDCEdgeTreatment
are both classes that provide a more modular approach for defining specifically the CGPath
for a specific edge or corner.
The last (but not least) class is MDCRectangleShapeGenerator
. It acts as a shapeGenerator
on its own (meaning it implements MDCShapeGenerating
), and generates a CGPath
but allows good customization as part of its implementation. It allows us to set each of its corners and edges by using its MDCCornerTreatments
and MDCEdgeTreatments
. With this class we can theoretically build any shape we want.
We also have at our disposal convenience classes under the ShapeLibrary/ folder that generate specific shapes:
- MDCCurvedCornerTreatment Generates an
MDCCornerTreatment
that is curved and receives a size to define the size of the curve. - MDCCutCornerTreatment Generates an
MDCCornerTreatment
that is a cut corner and receives a cut to define by how much to cut. - MDCRoundedCornerTreatment Generates an
MDCCornerTreatment
that is rounded and receives a radius to define the radius of the rounding. - MDCTriangleEdgeTreatment Generates an
MDCEdgeTreatment
that consists of a triangle of a settable size and style.
- MDCCurvedRectShapeGenerator This generates a shape using
MDCRectangleShapeGenerator
withMDCCurvedCornerTreatment
for its corners.
- MDCPillShapeGenerator This generates a shape using
MDCRectangleShapeGenerator
withMDCRoundedCornerTreatment
for its corners.
- MDCSlantedRectShapeGenerator This generates a shape using
MDCRectangleShapeGenerator
and adds a slant to its corners using a simple offset to its corners.
You will need to expose the shape generator in your component’s API to be able to set the shape. You will need to add a property in your .h file that is of the type id<MDCShapeGenerating>
. A client can use any of the convenience pre-made shape generators noted above, including the base MDCRectangleShapeGenerator
. Alternatively the developer can build his own shape by creating an object that implements MDCShapeGenerating
and creating a path with the help of MDCPathGenerator
.
@property(nullable, nonatomic, strong) id<MDCShapeGenerating> shapeGenerator;
If your component needs support either for shadow, border width/color customization, or even background color, then your component will need to use MDCShapedShadowLayer
as its CALayer
. This is a must as a shape is different than the frame/bounds of your view, and hence these properties need to abide by the shape's path.
You will need to implement the layerClass
in your view as follows:
+ (Class)layerClass {
return [MDCShapedShadowLayer class];
}
You then must also make sure that when setting your layer’s properties you must call the corresponding shapedBorderColor
, shapedBorderWidth
, and shapedBackgroundColor
rather than the default CALayer
properties.
Because shapes work with a different path than the view’s default layer, you need to be conscious of places in your code where these layer's properties are set, as that might cause unexpected behaviors or override existing behaviors to support shapes. When setting such properties, it is advised to check if (shapeGenerator != nil)
before applying them.
Shapes change our view’s default layer and therefore the layer no longer imitates the bounds. Due to that, we will now have cases where there are places in the bounds where the layer isn’t drawn, or places outside the bounds where the layer is drawn. Therefore we will need a way to support proper touch that would correspond to where the shape is and isn’t and act accordingly. To achieve this we can override the hitTest method of our UIView so we can check if the hitTest should return the view if the touch is part of our layer’s shape or not.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.layer.shapeGenerator) {
if (CGPathContainsPoint(self.layer.shapeLayer.path, nil, point, true)) {
return self;
} else {
return nil;
}
}
return [super hitTest:point withEvent:event];
}
If the component uses ink ripples, we will need to add shapes support for it. For ink ripples there are two things we must update, firstly is the maxRippleRadius
and secondly is the masking to bounds. The maxRippleRadius
must be updated in cases where the shape is either smaller or bigger than the bounds. In these cases we can’t rely on the bounds because for smaller shapes the ink will ripple too fast, and for bigger shapes the ripple won’t cover the entire shape. The ink layer’s maskToBounds
needs to also be set to NO so we can allow the ink to spread outside of the bounds when the shape goes outside of the bounds.
- (void)updateInkForShape {
CGRect boundingBox = CGPathGetBoundingBox(self.layer.shapeLayer.path);
self.inkView.maxRippleRadius =
(CGFloat)(MDCHypot(CGRectGetHeight(boundingBox), CGRectGetWidth(boundingBox)) / 2 + 10.f);
self.inkView.layer.masksToBounds = NO;
}
Now that our component can come in all different sizes and shapes, you will need to set a margin for the content so that the shape won't truncate the images/text etc. For that using the built-in UIView’s layoutMargins
is recommended.
If the component already supports shapes, it then already has an accessible id shapeGenerator property. In that case you only need to set the shapeGenerator to a shape of your choice and the component will be contained in that shape. There are available examples here:
let floatingButton = MDCFloatingButton()
floatingButton.setImage(plusImage for:.normal)
floatingButton.sizeToFit()
let floatingShapeGenerator = MDCRectangleShapeGenerator()
floatingShapeGenerator.setCorners(MDCCutCornerTreatment(cut: floatingButton.bounds.width / 2))
floatingButton.shapeGenerator = floatingShapeGenerator
self.view.addSubview(floatingButton)
self.floatingButton = [[MDCFloatingButton alloc] init];
[self.floatingButton setImage:plusImage forState:UIControlStateNormal];
[self.floatingButton sizeToFit];
MDCRectangleShapeGenerator *floatingShapeGenerator = [[MDCRectangleShapeGenerator alloc] init];
[floatingShapeGenerator setCorners:
[[MDCCutCornerTreatment alloc] initWithCut:CGRectGetWidth(self.floatingButton.bounds) / 2.f]];
self.floatingButton.shapeGenerator = floatingShapeGenerator;
[self.view addSubview:self.floatingButton];
let containedButton = MDCButton()
containedButton.setTitle("Add To Cart" for:.normal)
containedButton.setImage(plusImage for:.normal)
let raisedShapeGenerator = MDCRectangleShapeGenerator()
raisedShapeGenerator.setCorners(MDCCutCornerTreatment(cut: 8))
containedButton.shapeGenerator = raisedShapeGenerator
containedButton.sizeToFit()
self.view.addSubview(containedButton)
MDCButton *containedButton = [[MDCButton alloc] init];
[containedButton setTitle:@"Add To Cart" forState:UIControlStateNormal];
[containedButton setImage:plusImage forState:UIControlStateNormal];
MDCRectangleShapeGenerator *raisedShapeGenerator =
[[MDCRectangleShapeGenerator alloc] init];
[raisedShapeGenerator setCorners:[[MDCCutCornerTreatment alloc] initWithCut:8.f]];
containedButton.shapeGenerator = raisedShapeGenerator;
[containedButton sizeToFit];
[self.view addSubview:containedButton];
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kReusableIdentifierItem,
for: indexPath) as! MDCCardCollectionCell
let shapeGenerator = MDCRectangleShapeGenerator()
shapeGenerator.topLeftCorner = MDCCutCornerTreatment(cut: 20)
cell.shapeGenerator = shapeGenerator
return cell
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MDCCardCollectionCell *cell =
[collectionView dequeueReusableCellWithReuseIdentifier:kReusableIdentifierItem
forIndexPath:indexPath];
MDCRectangleShapeGenerator *shapeGenerator =
[[MDCRectangleShapeGenerator alloc] init];
shapeGenerator.topLeftCorner = [[MDCCutCornerTreatment alloc] initWithCut:20];
cell.shapeGenerator = shapeGenerator
return cell;
}
let card = MDCCard()
let shapeGenerator = MDCRectangleShapeGenerator()
let cutCorner = MDCCutCornerTreatment(cut: 20)
let roundedCorner = MDCRoundedCornerTreatment(radius: 20)
let curvedCorner = MDCCurvedCornerTreatment(size: CGSize(width: 20, height: 60))
shapeGenerator.topLeftCorner = cutCorner
shapeGenerator.topRightCorner = roundedCorner
shapeGenerator.bottomLeftCorner = roundedCorner
shapeGenerator.bottomRightCorner = curvedCorner
card.shapeGenerator = shapeGenerator
MDCCard *card = [[MDCCard alloc] init];
MDCRectangleShapeGenerator *shapeGenerator =
[[MDCRectangleShapeGenerator alloc] init];
MDCCutCornerTreatment *curCorner = [[MDCCutCornerTreatment alloc] initWithCut:20];
MDCRoundedCornerTreatment *roundedCorner =
[[MDCRoundedCornerTreatment alloc] initWithRadius:20];
MDCCurvedCornerTreatment *curvedCorner =
[[MDCCurvedCornerTreatment alloc] initWithSize:CGSizeMake(20, 60)];
shapeGenerator.topLeftCorner = cutCorner;
shapeGenerator.topRightCorner = roundedCorner;
shapeGenerator.bottomLeftCorner = roundedCorner;
shapeGenerator.bottomRightCorner = curvedCorner;
card.shapeGenerator = shapeGenerator;