|
| 1 | +<!--docs: |
| 2 | +title: "Shapes" |
| 3 | +layout: landing |
| 4 | +section: docs |
| 5 | +path: /docs/supporting-shapes/ |
| 6 | +--> |
| 7 | + |
| 8 | +# Shapes |
| 9 | + |
| 10 | +The Shapes library offers a way to customize the shape of existing components, or expand your own view to have a custom shape. |
| 11 | + |
| 12 | +Currently we offer built-in functionality for shapes within our <a href="../../components/Buttons">Buttons</a> and <a href="../../components/Cards">Cards</a> components. |
| 13 | + |
| 14 | +### Shapes Library Preview |
| 15 | + |
| 16 | +Our shapes library consists of different APIs and classes you can use to build a shape. |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +```objc |
| 21 | +(CGPath *)pathForSize:(CGSize)size |
| 22 | +``` |
| 23 | + |
| 24 | +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. |
| 25 | +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. |
| 26 | + |
| 27 | +`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. |
| 28 | + |
| 29 | +`MDCCornerTreatment` and `MDCEdgeTreatment` are both classes that provide a more modular approach for defining specifically the `CGPath` for a specific edge or corner. |
| 30 | + |
| 31 | +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. |
| 32 | + |
| 33 | +### Shapes Convenience Generators |
| 34 | + |
| 35 | +We also have at our disposal convenience classes under the ShapeLibrary/ folder that generate specific shapes: |
| 36 | + |
| 37 | +#### Corner and Edge generators |
| 38 | + |
| 39 | +- **MDCCurvedCornerTreatment** Generates an `MDCCornerTreatment` that is curved and receives a size to define the size of the curve. |
| 40 | +- **MDCCutCornerTreatment** Generates an `MDCCornerTreatment` that is a cut corner and receives a cut to define by how much to cut. |
| 41 | +- **MDCRoundedCornerTreatment** Generates an `MDCCornerTreatment` that is rounded and receives a radius to define the radius of the rounding. |
| 42 | +- **MDCTriangleEdgeTreatment** Generates an `MDCEdgeTreatment` that consists of a triangle of a settable size and style. |
| 43 | + |
| 44 | +#### Pre-made shape generators |
| 45 | + |
| 46 | +- **MDCCurvedRectShapeGenerator** This generates a shape using `MDCRectangleShapeGenerator` with `MDCCurvedCornerTreatment` for its corners. |
| 47 | + |
| 48 | +<img src="assets/MDCCurvedRectShapeGenerator.png" alt="MDCCurvedRectShapeGenerator" width="115"> |
| 49 | + |
| 50 | +- **MDCPillShapeGenerator** This generates a shape using `MDCRectangleShapeGenerator` with `MDCRoundedCornerTreatment` for its corners. |
| 51 | + |
| 52 | +<img src="assets/MDCPillShapeGenerator.png" alt="MDCPillShapeGenerator" width="115"> |
| 53 | + |
| 54 | +- **MDCSlantedRectShapeGenerator** This generates a shape using `MDCRectangleShapeGenerator` and adds a slant to its corners using a simple offset to its corners. |
| 55 | + |
| 56 | +<img src="assets/MDCSlantedRectShapeGenerator.png" alt="MDCSlantedRectShapeGenerator" width="115"> |
| 57 | + |
| 58 | +### Adding Shapes to your components |
| 59 | + |
| 60 | +#### Exposing the shape generator |
| 61 | + |
| 62 | +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`. |
| 63 | + |
| 64 | +```objc |
| 65 | +@property(nullable, nonatomic, strong) id<MDCShapeGenerating> shapeGenerator; |
| 66 | +``` |
| 67 | + |
| 68 | +#### Adding proper shadow, border, and background color support |
| 69 | + |
| 70 | +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. |
| 71 | +You will need to implement the `layerClass` in your view as follows: |
| 72 | + |
| 73 | +```objc |
| 74 | ++ (Class)layerClass { |
| 75 | + return [MDCShapedShadowLayer class]; |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +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. |
| 80 | + |
| 81 | +#### Being conscious of setting layer’s properties such as shadowPath and cornerRadius |
| 82 | + |
| 83 | +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. |
| 84 | + |
| 85 | +#### Supporting touch events |
| 86 | + |
| 87 | +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. |
| 88 | +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. |
| 89 | + |
| 90 | +```objc |
| 91 | +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { |
| 92 | + if (self.layer.shapeGenerator) { |
| 93 | + if (CGPathContainsPoint(self.layer.shapeLayer.path, nil, point, true)) { |
| 94 | + return self; |
| 95 | + } else { |
| 96 | + return nil; |
| 97 | + } |
| 98 | + } |
| 99 | + return [super hitTest:point withEvent:event]; |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +#### Ink Ripple Support |
| 104 | + |
| 105 | +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. |
| 106 | + |
| 107 | +```objc |
| 108 | +- (void)updateInkForShape { |
| 109 | + CGRect boundingBox = CGPathGetBoundingBox(self.layer.shapeLayer.path); |
| 110 | + self.inkView.maxRippleRadius = |
| 111 | + (CGFloat)(MDCHypot(CGRectGetHeight(boundingBox), CGRectGetWidth(boundingBox)) / 2 + 10.f); |
| 112 | + self.inkView.layer.masksToBounds = NO; |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +#### Content Margins |
| 117 | + |
| 118 | +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. |
| 119 | + |
| 120 | +### Using Shapes in the already shape-supported components (Buttons / Cards) |
| 121 | + |
| 122 | +If the component already supports shapes, it then already has an accessible id<MDCShapeGenerating> shapeGenerator property. |
| 123 | +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: |
| 124 | + * <a href="../../components/Buttons/examples/ButtonsShapesExampleViewController.m">Shaped Buttons</a> |
| 125 | + * <a href="../../components/Cards/examples/ShapedCardViewController.swift ">Shaped Cards</a> |
| 126 | + |
| 127 | +### Examples |
| 128 | + |
| 129 | +#### Diamond FAB |
| 130 | + |
| 131 | +<img src="assets/diamondfab.gif" alt="Diamond FAB."> |
| 132 | + |
| 133 | +<!--<div class="material-code-render" markdown="1">--> |
| 134 | +##### Swift |
| 135 | +```swift |
| 136 | +let floatingButton = MDCFloatingButton() |
| 137 | +floatingButton.setImage(plusImage for:.normal) |
| 138 | +floatingButton.sizeToFit() |
| 139 | + |
| 140 | +let floatingShapeGenerator = MDCRectangleShapeGenerator() |
| 141 | +floatingShapeGenerator.setCorners(MDCCutCornerTreatment(cut: floatingButton.bounds.width / 2)) |
| 142 | +floatingButton.shapeGenerator = floatingShapeGenerator |
| 143 | +self.view.addSubview(floatingButton) |
| 144 | +``` |
| 145 | + |
| 146 | +##### Objective-C |
| 147 | +```objc |
| 148 | +self.floatingButton = [[MDCFloatingButton alloc] init]; |
| 149 | +[self.floatingButton setImage:plusImage forState:UIControlStateNormal]; |
| 150 | +[self.floatingButton sizeToFit]; |
| 151 | + |
| 152 | +MDCRectangleShapeGenerator *floatingShapeGenerator = [[MDCRectangleShapeGenerator alloc] init]; |
| 153 | +[floatingShapeGenerator setCorners: |
| 154 | + [[MDCCutCornerTreatment alloc] initWithCut:CGRectGetWidth(self.floatingButton.bounds) / 2.f]]; |
| 155 | +self.floatingButton.shapeGenerator = floatingShapeGenerator; |
| 156 | +[self.view addSubview:self.floatingButton]; |
| 157 | +``` |
| 158 | +<!--</div>--> |
| 159 | + |
| 160 | +#### Cut Corners Contained Button |
| 161 | + |
| 162 | +<img src="assets/cutcornersbutton.gif" alt="Cut Corners Button."> |
| 163 | + |
| 164 | +<!--<div class="material-code-render" markdown="1">--> |
| 165 | +##### Swift |
| 166 | +```swift |
| 167 | +let containedButton = MDCButton() |
| 168 | +containedButton.setTitle("Add To Cart" for:.normal) |
| 169 | +containedButton.setImage(plusImage for:.normal) |
| 170 | + |
| 171 | +let raisedShapeGenerator = MDCRectangleShapeGenerator() |
| 172 | +raisedShapeGenerator.setCorners(MDCCutCornerTreatment(cut: 8)) |
| 173 | +containedButton.shapeGenerator = raisedShapeGenerator |
| 174 | + |
| 175 | +containedButton.sizeToFit() |
| 176 | +self.view.addSubview(containedButton) |
| 177 | +``` |
| 178 | + |
| 179 | +##### Objective-C |
| 180 | +```objc |
| 181 | +MDCButton *containedButton = [[MDCButton alloc] init]; |
| 182 | +[containedButton setTitle:@"Add To Cart" forState:UIControlStateNormal]; |
| 183 | +[containedButton setImage:plusImage forState:UIControlStateNormal]; |
| 184 | + |
| 185 | +MDCRectangleShapeGenerator *raisedShapeGenerator = |
| 186 | + [[MDCRectangleShapeGenerator alloc] init]; |
| 187 | +[raisedShapeGenerator setCorners:[[MDCCutCornerTreatment alloc] initWithCut:8.f]]; |
| 188 | +containedButton.shapeGenerator = raisedShapeGenerator; |
| 189 | + |
| 190 | +[containedButton sizeToFit]; |
| 191 | +[self.view addSubview:containedButton]; |
| 192 | +``` |
| 193 | +<!--</div>--> |
| 194 | + |
| 195 | +#### Top Left Cut Corner Card Cell |
| 196 | + |
| 197 | +<img src="assets/cardcellcutcorner.gif" alt="Card Cell Cut Corner."> |
| 198 | + |
| 199 | +<!--<div class="material-code-render" markdown="1">--> |
| 200 | +##### Swift |
| 201 | +```swift |
| 202 | +func collectionView(_ collectionView: UICollectionView, |
| 203 | + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
| 204 | + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kReusableIdentifierItem, |
| 205 | + for: indexPath) as! MDCCardCollectionCell |
| 206 | + let shapeGenerator = MDCRectangleShapeGenerator() |
| 207 | + shapeGenerator.topLeftCorner = MDCCutCornerTreatment(cut: 20) |
| 208 | + cell.shapeGenerator = shapeGenerator |
| 209 | + return cell |
| 210 | +``` |
| 211 | + |
| 212 | +##### Objective-C |
| 213 | +```objc |
| 214 | +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView |
| 215 | + cellForItemAtIndexPath:(NSIndexPath *)indexPath { |
| 216 | + MDCCardCollectionCell *cell = |
| 217 | + [collectionView dequeueReusableCellWithReuseIdentifier:kReusableIdentifierItem |
| 218 | + forIndexPath:indexPath]; |
| 219 | + MDCRectangleShapeGenerator *shapeGenerator = |
| 220 | + [[MDCRectangleShapeGenerator alloc] init]; |
| 221 | + shapeGenerator.topLeftCorner = [[MDCCutCornerTreatment alloc] initWithCut:20]; |
| 222 | + cell.shapeGenerator = shapeGenerator |
| 223 | + return cell; |
| 224 | +} |
| 225 | +``` |
| 226 | +<!--</div>--> |
| 227 | + |
| 228 | +#### Card with different corners |
| 229 | + |
| 230 | +<img src="assets/cardwithdiffcorners.gif" alt="Card With Different Corners."> |
| 231 | + |
| 232 | +<!--<div class="material-code-render" markdown="1">--> |
| 233 | +##### Swift |
| 234 | +```swift |
| 235 | +let card = MDCCard() |
| 236 | +let shapeGenerator = MDCRectangleShapeGenerator() |
| 237 | +let cutCorner = MDCCutCornerTreatment(cut: 20) |
| 238 | +let roundedCorner = MDCRoundedCornerTreatment(radius: 20) |
| 239 | +let curvedCorner = MDCCurvedCornerTreatment(size: CGSize(width: 20, height: 60)) |
| 240 | +shapeGenerator.topLeftCorner = cutCorner |
| 241 | +shapeGenerator.topRightCorner = roundedCorner |
| 242 | +shapeGenerator.bottomLeftCorner = roundedCorner |
| 243 | +shapeGenerator.bottomRightCorner = curvedCorner |
| 244 | +card.shapeGenerator = shapeGenerator |
| 245 | +``` |
| 246 | + |
| 247 | +##### Objective-C |
| 248 | +```objc |
| 249 | +MDCCard *card = [[MDCCard alloc] init]; |
| 250 | +MDCRectangleShapeGenerator *shapeGenerator = |
| 251 | + [[MDCRectangleShapeGenerator alloc] init]; |
| 252 | +MDCCutCornerTreatment *curCorner = [[MDCCutCornerTreatment alloc] initWithCut:20]; |
| 253 | +MDCRoundedCornerTreatment *roundedCorner = |
| 254 | + [[MDCRoundedCornerTreatment alloc] initWithRadius:20]; |
| 255 | +MDCCurvedCornerTreatment *curvedCorner = |
| 256 | + [[MDCCurvedCornerTreatment alloc] initWithSize:CGSizeMake(20, 60)]; |
| 257 | +shapeGenerator.topLeftCorner = cutCorner; |
| 258 | +shapeGenerator.topRightCorner = roundedCorner; |
| 259 | +shapeGenerator.bottomLeftCorner = roundedCorner; |
| 260 | +shapeGenerator.bottomRightCorner = curvedCorner; |
| 261 | +card.shapeGenerator = shapeGenerator; |
| 262 | +``` |
| 263 | +<!--</div>--> |
| 264 | + |
0 commit comments