Allow customization of margins in primaryLayout

We are providing predefined set of margins that are recommended to be
used and helper method to create custom once as percentages. They will
automatically sort out which values from the given bucket
(min/mid/max/default) to use, based on the presence of slots, that is
passed around via MaterialScope.

This also update values per updated spec.

Fixes: 373578620
Test: Locally verified and updated screenshot test. Added unit for customized
Relnote: "We are now allowing margins (side and in some cases bottom) to be customized in Material3 primaryLayout."
Change-Id: Ib22f60aa2e9517fc9d0b29611124870fd3346b6d
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index eae2d58..5b94c7b 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -248,7 +248,24 @@
   }
 
   public final class PrimaryLayoutKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement primaryLayout(androidx.wear.protolayout.material3.MaterialScope, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> mainSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? titleSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? bottomSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? labelForBottomSlot, optional androidx.wear.protolayout.ModifiersBuilders.Clickable? onClick);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement primaryLayout(androidx.wear.protolayout.material3.MaterialScope, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> mainSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? titleSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? bottomSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? labelForBottomSlot, optional androidx.wear.protolayout.ModifiersBuilders.Clickable? onClick, optional androidx.wear.protolayout.material3.PrimaryLayoutMargins margins);
+  }
+
+  public abstract class PrimaryLayoutMargins {
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion Companion;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins DEFAULT_PRIMARY_LAYOUT_MARGIN;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins MAX_PRIMARY_LAYOUT_MARGIN;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins MID_PRIMARY_LAYOUT_MARGIN;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins MIN_PRIMARY_LAYOUT_MARGIN;
+  }
+
+  public static final class PrimaryLayoutMargins.Companion {
+    method public androidx.wear.protolayout.material3.PrimaryLayoutMargins customizedPrimaryLayoutMargin(@FloatRange(from=0.0, to=1.0) float start, @FloatRange(from=0.0, to=1.0) float end);
+    method public androidx.wear.protolayout.material3.PrimaryLayoutMargins customizedPrimaryLayoutMargin(@FloatRange(from=0.0, to=1.0) float start, @FloatRange(from=0.0, to=1.0) float end, @FloatRange(from=0.0, to=1.0) float bottom);
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins DEFAULT_PRIMARY_LAYOUT_MARGIN;
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins MAX_PRIMARY_LAYOUT_MARGIN;
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins MID_PRIMARY_LAYOUT_MARGIN;
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins MIN_PRIMARY_LAYOUT_MARGIN;
   }
 
   public final class ProgressIndicatorColors {
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index eae2d58..5b94c7b 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -248,7 +248,24 @@
   }
 
   public final class PrimaryLayoutKt {
-    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement primaryLayout(androidx.wear.protolayout.material3.MaterialScope, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> mainSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? titleSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? bottomSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? labelForBottomSlot, optional androidx.wear.protolayout.ModifiersBuilders.Clickable? onClick);
+    method public static androidx.wear.protolayout.LayoutElementBuilders.LayoutElement primaryLayout(androidx.wear.protolayout.material3.MaterialScope, kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement> mainSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? titleSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? bottomSlot, optional kotlin.jvm.functions.Function1<? super androidx.wear.protolayout.material3.MaterialScope,? extends androidx.wear.protolayout.LayoutElementBuilders.LayoutElement>? labelForBottomSlot, optional androidx.wear.protolayout.ModifiersBuilders.Clickable? onClick, optional androidx.wear.protolayout.material3.PrimaryLayoutMargins margins);
+  }
+
+  public abstract class PrimaryLayoutMargins {
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion Companion;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins DEFAULT_PRIMARY_LAYOUT_MARGIN;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins MAX_PRIMARY_LAYOUT_MARGIN;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins MID_PRIMARY_LAYOUT_MARGIN;
+    field public static final androidx.wear.protolayout.material3.PrimaryLayoutMargins MIN_PRIMARY_LAYOUT_MARGIN;
+  }
+
+  public static final class PrimaryLayoutMargins.Companion {
+    method public androidx.wear.protolayout.material3.PrimaryLayoutMargins customizedPrimaryLayoutMargin(@FloatRange(from=0.0, to=1.0) float start, @FloatRange(from=0.0, to=1.0) float end);
+    method public androidx.wear.protolayout.material3.PrimaryLayoutMargins customizedPrimaryLayoutMargin(@FloatRange(from=0.0, to=1.0) float start, @FloatRange(from=0.0, to=1.0) float end, @FloatRange(from=0.0, to=1.0) float bottom);
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins DEFAULT_PRIMARY_LAYOUT_MARGIN;
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins MAX_PRIMARY_LAYOUT_MARGIN;
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins MID_PRIMARY_LAYOUT_MARGIN;
+    property public final androidx.wear.protolayout.material3.PrimaryLayoutMargins MIN_PRIMARY_LAYOUT_MARGIN;
   }
 
   public final class ProgressIndicatorColors {
diff --git a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
index e5d1f6a..619cff2 100644
--- a/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
+++ b/wear/protolayout/protolayout-material3/samples/src/main/java/androidx/wear/protolayout/material3/samples/Material3ComponentsSample.kt
@@ -38,6 +38,7 @@
 import androidx.wear.protolayout.material3.DataCardStyle.Companion.extraLargeDataCardStyle
 import androidx.wear.protolayout.material3.DataCardStyle.Companion.largeCompactDataCardStyle
 import androidx.wear.protolayout.material3.GraphicDataCardStyle.Companion.largeGraphicDataCardStyle
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MAX_PRIMARY_LAYOUT_MARGIN
 import androidx.wear.protolayout.material3.TitleCardStyle.Companion.largeTitleCardStyle
 import androidx.wear.protolayout.material3.Typography
 import androidx.wear.protolayout.material3.appCard
@@ -145,7 +146,7 @@
                                 ModifiersBuilders.Modifiers.Builder()
                                     .setBackground(
                                         ModifiersBuilders.Background.Builder()
-                                            .setCorner(shapes.full)
+                                            .setCorner(shapes.small)
                                             .build()
                                     )
                                     .build()
@@ -154,6 +155,8 @@
                     }
                 }
             },
+            // Adjust margins as the corner of the inner content is on the square side.
+            margins = MAX_PRIMARY_LAYOUT_MARGIN,
             bottomSlot = {
                 iconEdgeButton(
                     onClick = clickable,
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index 4537676..7e34220 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -36,10 +36,11 @@
 import androidx.wear.protolayout.material3.DataCardStyle.Companion.smallCompactDataCardStyle
 import androidx.wear.protolayout.material3.IconButtonStyle.Companion.largeIconButtonStyle
 import androidx.wear.protolayout.material3.MaterialGoldenTest.Companion.pxToDp
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MAX_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MIN_PRIMARY_LAYOUT_MARGIN
 import androidx.wear.protolayout.material3.TextButtonStyle.Companion.extraLargeTextButtonStyle
 import androidx.wear.protolayout.material3.TextButtonStyle.Companion.largeTextButtonStyle
 import androidx.wear.protolayout.material3.TextButtonStyle.Companion.smallTextButtonStyle
-import androidx.wear.protolayout.material3.TitleContentPlacementInDataCard.Companion.Bottom
 import androidx.wear.protolayout.modifiers.LayoutModifier
 import androidx.wear.protolayout.modifiers.clickable
 import androidx.wear.protolayout.modifiers.clip
@@ -135,6 +136,7 @@
                             graphic = { circularProgressIndicator(staticProgress = 0.5F) }
                         )
                     },
+                    margins = MIN_PRIMARY_LAYOUT_MARGIN
                 )
             }
         testCases["primarylayout_edgebuttonfilledvariant_iconoverride_golden$NORMAL_SCALE_SUFFIX"] =
@@ -153,17 +155,9 @@
                                     title = { text("MM".layoutString) },
                                     content = { text("Min".layoutString) },
                                     secondaryIcon = { icon(ICON_ID) },
-                                    shape = shapes.full
-                                )
-                            }
-                            buttonGroupItem {
-                                iconDataCard(
-                                    onClick = clickable,
-                                    modifier = LayoutModifier.contentDescription("Data Card"),
-                                    title = { text("MM".layoutString) },
-                                    content = { text("Min".layoutString) },
-                                    secondaryIcon = { icon(ICON_ID) },
-                                    titleContentPlacement = Bottom
+                                    shape = shapes.none,
+                                    width = expand(),
+                                    height = expand()
                                 )
                             }
                             buttonGroupItem {
@@ -178,11 +172,13 @@
                                             backgroundColor = colorScheme.onSecondary,
                                             titleColor = colorScheme.secondary,
                                             contentColor = colorScheme.secondaryDim
-                                        )
+                                        ),
+                                    shape = shapes.full
                                 )
                             }
                         }
                     },
+                    margins = MAX_PRIMARY_LAYOUT_MARGIN,
                     bottomSlot = {
                         textEdgeButton(
                             onClick = clickable,
@@ -473,7 +469,10 @@
                 deviceParameters,
                 allowDynamicTheme = false
             ) {
-                primaryLayout(mainSlot = { progressIndicatorGroup() })
+                primaryLayout(
+                    mainSlot = { progressIndicatorGroup() },
+                    margins = MIN_PRIMARY_LAYOUT_MARGIN
+                )
             }
 
         testCases["primarylayout_circularprogressindicators_fallback__golden$NORMAL_SCALE_SUFFIX"] =
@@ -486,6 +485,7 @@
             ) {
                 primaryLayout(
                     mainSlot = { progressIndicatorGroup() },
+                    margins = MIN_PRIMARY_LAYOUT_MARGIN,
                     bottomSlot = {
                         iconEdgeButton(
                             onClick = clickable,
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt
index c6d834e..939df6f 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Button.kt
@@ -42,6 +42,8 @@
 import androidx.wear.protolayout.material3.CompactButtonStyle.COMPACT_BUTTON_ICON_SIZE_SMALL_DP
 import androidx.wear.protolayout.material3.CompactButtonStyle.COMPACT_BUTTON_LABEL_TYPOGRAPHY
 import androidx.wear.protolayout.material3.IconButtonStyle.Companion.defaultIconButtonStyle
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.maxPrimaryLayoutMargins
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.minPrimaryLayoutMargins
 import androidx.wear.protolayout.material3.TextButtonStyle.Companion.defaultTextButtonStyle
 import androidx.wear.protolayout.modifiers.LayoutModifier
 import androidx.wear.protolayout.modifiers.background
@@ -62,7 +64,11 @@
  *   the associated action.
  * @param modifier Modifiers to set to this element. It's highly recommended to set a content
  *   description using [contentDescription].
- * @param shape Defines the button's shape, in other words the corner radius for this button.
+ * @param shape Defines the button's shape, in other words the corner radius for this button. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param width The width of this button. It's highly recommended to set this to [expand] or
  *   [weight]
  * @param height The height of this button. It's highly recommended to set this to [expand] or
@@ -82,7 +88,6 @@
  * @sample androidx.wear.protolayout.material3.samples.oneSlotButtonsSample
  */
 // TODO: b/346958146 - Link Button visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.iconButton(
     onClick: Clickable,
     iconContent: (MaterialScope.() -> LayoutElement),
@@ -121,7 +126,11 @@
  *   the associated action.
  * @param modifier Modifiers to set to this element. It's highly recommended to set a content
  *   description using [contentDescription].
- * @param shape Defines the button's shape, in other words the corner radius for this button.
+ * @param shape Defines the button's shape, in other words the corner radius for this button. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param width The width of this button. It's highly recommended to set this to [expand] or
  *   [weight]
  * @param height The height of this button. It's highly recommended to set this to [expand] or
@@ -142,7 +151,6 @@
  * @sample androidx.wear.protolayout.material3.samples.oneSlotButtonsSample
  */
 // TODO: b/346958146 - Link Button visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.textButton(
     onClick: Clickable,
     labelContent: (MaterialScope.() -> LayoutElement),
@@ -186,7 +194,11 @@
  *   recommended to use default styling that is automatically provided by only calling [text].
  * @param iconContent The icon slot for content displayed in this button. It is recommended to use
  *   default styling that is automatically provided by only calling [icon] with the resource ID.
- * @param shape Defines the button's shape, in other words the corner radius for this button.
+ * @param shape Defines the button's shape, in other words the corner radius for this button. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param width The width of this button. It's highly recommended to set this to [expand] or
  *   [weight]
  * @param height The height of this button. It's highly recommended to set this to [expand] or
@@ -331,7 +343,11 @@
  *   parameter.
  * @param iconContent The icon slot for content displayed in this button. It is recommended to use
  *   default styling that is automatically provided by only calling [icon] with the resource ID.
- * @param shape Defines the button's shape, in other words the corner radius for this button.
+ * @param shape Defines the button's shape, in other words the corner radius for this button. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param width The width of this button. It's highly recommended to set this to [expand] or
  *   [weight]
  * @param colors The colors used for this button. If not set, [ButtonDefaults.filledButtonColors]
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonGroup.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonGroup.kt
index eb1ec09..f38013d 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonGroup.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/ButtonGroup.kt
@@ -64,6 +64,8 @@
  * @param spacing The amount of spacing between buttons
  * @param content The content for each child. The UX guidance is to use no more than 3 elements
  *   within a this button group.
+ * @sample androidx.wear.protolayout.material3.samples.dataCardSample
+ * @sample androidx.wear.protolayout.material3.samples.oneSlotButtonsSample
  */
 // TODO: b/346958146 - Link visuals once they are available.
 public fun MaterialScope.buttonGroup(
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
index 951a192..ef9b1cc 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
@@ -38,6 +38,8 @@
 import androidx.wear.protolayout.material3.DataCardStyle.Companion.defaultDataCardStyle
 import androidx.wear.protolayout.material3.GraphicDataCardDefaults.buildContentForGraphicDataCard
 import androidx.wear.protolayout.material3.GraphicDataCardStyle.Companion.defaultGraphicDataCardStyle
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.maxPrimaryLayoutMargins
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.minPrimaryLayoutMargins
 import androidx.wear.protolayout.material3.TitleCardDefaults.buildContentForTitleCard
 import androidx.wear.protolayout.material3.TitleCardStyle.Companion.defaultTitleCardStyle
 import androidx.wear.protolayout.modifiers.LayoutModifier
@@ -63,7 +65,11 @@
  *   expected to be a short piece of text. Uses [CardColors.timeColor] color by default.
  * @param height The height of this card. It's highly recommended to set this to [expand] or
  *   [weight].
- * @param shape Defines the card's shape, in other words the corner radius for this card.
+ * @param shape Defines the card's shape, in other words the corner radius for this card. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param colors The colors to be used for a background and inner content of this card. If the
  *   background image is also specified, the image will be laid out on top of the background color.
  *   In case of the fully opaque background image, then the background color will not be shown.
@@ -87,7 +93,6 @@
  * @sample androidx.wear.protolayout.material3.samples.titleCardSample
  */
 // TODO: b/346958146 - link Card visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.titleCard(
     onClick: Clickable,
     title: (MaterialScope.() -> LayoutElement),
@@ -186,7 +191,11 @@
  * @param height The height of this card. It's highly recommended to leave this with default value
  *   as `wrap` if there's only 1 card on the screen. If there are two cards, it is highly
  *   recommended to set this to [expand] and use the smaller styles.
- * @param shape Defines the card's shape, in other words the corner radius for this card.
+ * @param shape Defines the card's shape, in other words the corner radius for this card. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param colors The colors to be used for a background and inner content of this card. If the
  *   background image is also specified, the image will be laid out on top of the background color.
  *   In case of the fully opaque background image, then the background color will not be shown.
@@ -207,7 +216,6 @@
  * @sample androidx.wear.protolayout.material3.samples.appCardSample
  */
 // TODO: b/346958146 - link Card visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.appCard(
     onClick: Clickable,
     title: (MaterialScope.() -> LayoutElement),
@@ -313,7 +321,11 @@
  *   for the most optimal experience across different screen sizes.
  * @param height The height of this card. It's highly recommended to set this to [expand] for the
  *   most optimal experience across different screen sizes.
- * @param shape Defines the card's shape, in other words the corner radius for this card.
+ * @param shape Defines the card's shape, in other words the corner radius for this card. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param colors The colors to be used for a background and inner content of this card. If the
  *   background image is also specified, the image will be laid out on top of the background color.
  *   In case of the fully opaque background image, then the background color will not be shown.
@@ -339,7 +351,6 @@
  * @sample androidx.wear.protolayout.material3.samples.dataCardSample
  */
 // TODO: b/346958146 - link Card visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.textDataCard(
     onClick: Clickable,
     title: (MaterialScope.() -> LayoutElement),
@@ -426,7 +437,11 @@
  *   for the most optimal experience across different screen sizes.
  * @param height The height of this card. It's highly recommended to set this to [expand] for the
  *   most optimal experience across different screen sizes.
- * @param shape Defines the card's shape, in other words the corner radius for this card.
+ * @param shape Defines the card's shape, in other words the corner radius for this card. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param colors The colors to be used for a background and inner content of this card. If the
  *   background image is also specified, the image will be laid out on top of the background color.
  *   In case of the fully opaque background image, then the background color will not be shown.
@@ -454,7 +469,6 @@
  * @sample androidx.wear.protolayout.material3.samples.dataCardSample
  */
 // TODO: b/346958146 - link Card visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.iconDataCard(
     onClick: Clickable,
     title: (MaterialScope.() -> LayoutElement),
@@ -532,7 +546,11 @@
  * @param graphic A slot for displaying graphic data, such as progress indicator.
  * @param height The width of this card. It's highly recommended to set this to [expand] for the
  *   most optimal experience across different screen sizes.
- * @param shape Defines the card's shape, in other words the corner radius for this card.
+ * @param shape Defines the card's shape, in other words the corner radius for this card. If
+ *   changing these to radius smaller than [Shapes.medium], it is important to adjusts the margins
+ *   of [primaryLayout] used to accommodate for more space, for example by using
+ *   [maxPrimaryLayoutMargins]. Or, if the [shape] is set to [Shapes.full], using
+ *   [minPrimaryLayoutMargins] can be considered.
  * @param colors The colors to be used for a background and inner content of this card. Specified
  *   colors can be [CardDefaults.filledCardColors] for high emphasis card,
  *   [CardDefaults.filledVariantCardColors] for high/medium emphasis card,
@@ -552,7 +570,6 @@
  * @sample androidx.wear.protolayout.material3.samples.graphicDataCardSample
  */
 // TODO: b/346958146 - link Card visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.graphicDataCard(
     onClick: Clickable,
     // TODO: b/368272767 - Potentially add helper for CPI and icon and link in KDocs.
@@ -627,7 +644,11 @@
  * @param modifier Modifiers to set to this element. It's highly recommended to set a content
  *   description using [contentDescription]. If [LayoutModifier.background] modifier is used and the
  *   the background image is also specified, the image will be laid out on top of this color. In
- *   case of the fully opaque background image, then the background color will not be shown.
+ *   case of the fully opaque background image, then the background color will not be shown. If
+ *   [LayoutModifier.clip] modifier is used to change the shape of the card to radius smaller than
+ *   [Shapes.medium], it is important to adjusts the margins of [primaryLayout] used to accommodate
+ *   for more space, for example by using [maxPrimaryLayoutMargins]. Or, if changing to
+ *   [Shapes.full], using [minPrimaryLayoutMargins] can be considered.
  * @param backgroundContent The background object to be used behind the content in the card. It is
  *   recommended to use the default styling that is automatically provided by only calling
  *   [backgroundImage] with the content. It can be combined with the specified
@@ -641,7 +662,6 @@
  * @sample androidx.wear.protolayout.material3.samples.cardSample
  */
 // TODO: b/346958146 - link Card visuals in DAC
-// TODO: b/373578620 - Add how corners affects margins in the layout.
 public fun MaterialScope.card(
     onClick: Clickable,
     modifier: LayoutModifier = LayoutModifier,
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index 8f080b0..f29af19 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -26,7 +26,6 @@
 import androidx.wear.protolayout.DimensionBuilders.ContainerDimension
 import androidx.wear.protolayout.DimensionBuilders.DpProp
 import androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp
-import androidx.wear.protolayout.DimensionBuilders.dp
 import androidx.wear.protolayout.DimensionBuilders.expand
 import androidx.wear.protolayout.DimensionBuilders.wrap
 import androidx.wear.protolayout.LayoutElementBuilders
@@ -44,6 +43,8 @@
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.Padding
 import androidx.wear.protolayout.ModifiersBuilders.SEMANTICS_ROLE_BUTTON
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.percentageHeightToDp
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.percentageWidthToDp
 import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory
 import androidx.wear.protolayout.modifiers.LayoutModifier
 import androidx.wear.protolayout.modifiers.clickable
@@ -53,6 +54,7 @@
 import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
 import androidx.wear.protolayout.types.LayoutColor
 import androidx.wear.protolayout.types.argb
+import androidx.wear.protolayout.types.dp
 import java.nio.charset.StandardCharsets
 
 /**
@@ -62,7 +64,7 @@
 internal const val SCREEN_SIZE_BREAKPOINT_DP = 225
 
 /** Minimum tap target for any clickable element. */
-internal val MINIMUM_TAP_TARGET_SIZE: DpProp = dp(48f)
+internal val MINIMUM_TAP_TARGET_SIZE: DpProp = 48f.dp
 
 /** Returns byte array representation of tag from String. */
 internal fun String.toTagBytes(): ByteArray = toByteArray(StandardCharsets.UTF_8)
@@ -89,7 +91,7 @@
 
 @Dimension(unit = SP) private fun Float.dpToSpLinear(fontScale: Float): Float = this / fontScale
 
-internal fun Int.toDp() = dp(this.toFloat())
+internal fun Int.toDp() = this.toFloat().dp
 
 /** Builds a horizontal Spacer, with width set to expand and height set to the given value. */
 internal fun horizontalSpacer(@Dimension(unit = DP) heightDp: Int): Spacer =
@@ -206,3 +208,33 @@
         )
         .build()
 }
+
+/**
+ * Returns [Padding] objects with values represented as percentages from the screen size.
+ *
+ * @param start The ratio percentage of the screen width that should be use as start padding
+ * @param end The ratio percentage of the screen width that should be use as end padding
+ * @param bottom The ratio percentage of the screen width that should be use as bottom padding
+ */
+internal fun MaterialScope.percentagePadding(
+    @FloatRange(from = 0.0, to = 1.0) start: Float,
+    @FloatRange(from = 0.0, to = 1.0) end: Float,
+    @FloatRange(from = 0.0, to = 1.0) bottom: Float
+): Padding =
+    padding(
+        start = percentageWidthToDp(start),
+        end = percentageWidthToDp(end),
+        bottom = percentageHeightToDp(bottom)
+    )
+
+/**
+ * Returns [Padding] objects with values represented as percentages from the screen size, using only
+ * horizontal padding.
+ *
+ * @param start The ratio percentage of the screen width that should be use as start padding
+ * @param end The ratio percentage of the screen width that should be use as end padding
+ */
+internal fun MaterialScope.percentagePadding(
+    @FloatRange(from = 0.0, to = 1.0) start: Float,
+    @FloatRange(from = 0.0, to = 1.0) end: Float
+): Padding = padding(start = percentageWidthToDp(start), end = percentageWidthToDp(end))
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/MaterialScope.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/MaterialScope.kt
index 6b9a14b..d547b3f 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/MaterialScope.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/MaterialScope.kt
@@ -73,6 +73,7 @@
     internal val defaultIconStyle: IconStyle,
     internal val defaultBackgroundImageStyle: BackgroundImageStyle,
     internal val defaultAvatarImageStyle: AvatarImageStyle,
+    internal val layoutSlotsPresence: LayoutSlotsPresence
 ) {
     /** Color Scheme used within this scope and its components. */
     public val colorScheme: ColorScheme = theme.colorScheme
@@ -84,7 +85,8 @@
         defaultTextElementStyle: TextElementStyle = this.defaultTextElementStyle,
         defaultIconStyle: IconStyle = this.defaultIconStyle,
         defaultBackgroundImageStyle: BackgroundImageStyle = this.defaultBackgroundImageStyle,
-        defaultAvatarImageStyle: AvatarImageStyle = this.defaultAvatarImageStyle
+        defaultAvatarImageStyle: AvatarImageStyle = this.defaultAvatarImageStyle,
+        layoutSlotsPresence: LayoutSlotsPresence = this.layoutSlotsPresence
     ): MaterialScope =
         MaterialScope(
             context = context,
@@ -94,7 +96,8 @@
             defaultTextElementStyle = defaultTextElementStyle,
             defaultIconStyle = defaultIconStyle,
             defaultBackgroundImageStyle = defaultBackgroundImageStyle,
-            defaultAvatarImageStyle = defaultAvatarImageStyle
+            defaultAvatarImageStyle = defaultAvatarImageStyle,
+            layoutSlotsPresence = layoutSlotsPresence
         )
 }
 
@@ -140,7 +143,8 @@
             defaultTextElementStyle = TextElementStyle(),
             defaultIconStyle = IconStyle(),
             defaultBackgroundImageStyle = BackgroundImageStyle(),
-            defaultAvatarImageStyle = AvatarImageStyle()
+            defaultAvatarImageStyle = AvatarImageStyle(),
+            layoutSlotsPresence = LayoutSlotsPresence()
         )
         .layout()
 
@@ -180,3 +184,9 @@
     @ContentScaleMode
     val contentScaleMode: Int = LayoutElementBuilders.CONTENT_SCALE_MODE_FILL_BOUNDS
 )
+
+internal class LayoutSlotsPresence(
+    val isTitleSlotPresent: Boolean = false,
+    val isBottomSlotEdgeButton: Boolean = false,
+    val isBottomSlotPresent: Boolean = isBottomSlotEdgeButton
+)
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
index 0bd6491..39019fa 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayout.kt
@@ -18,11 +18,11 @@
 
 import androidx.annotation.Dimension
 import androidx.annotation.Dimension.Companion.DP
+import androidx.annotation.FloatRange
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.wear.protolayout.DimensionBuilders
 import androidx.wear.protolayout.DimensionBuilders.DpProp
-import androidx.wear.protolayout.DimensionBuilders.dp
 import androidx.wear.protolayout.DimensionBuilders.expand
 import androidx.wear.protolayout.DimensionBuilders.wrap
 import androidx.wear.protolayout.LayoutElementBuilders.Box
@@ -33,25 +33,39 @@
 import androidx.wear.protolayout.ModifiersBuilders.ElementMetadata
 import androidx.wear.protolayout.ModifiersBuilders.Modifiers
 import androidx.wear.protolayout.ModifiersBuilders.Padding
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.defaultPrimaryLayoutMargins
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.maxBottomMargin
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.maxPrimaryLayoutMargins
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.maxSideMargin
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.midPrimaryLayoutMargins
+import androidx.wear.protolayout.material3.PredefinedPrimaryLayoutMargins.minPrimaryLayoutMargins
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_EMPTY_MARGIN_BOTTOM_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_MARGIN_BOTTOM_PERCENTAGE
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_BOTTOM_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_TOP_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_BOTTOM_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_TOP_PERCENTAGE
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.BOTTOM_SLOT_OTHER_MARGIN_TOP_DP
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_ICON_SIZE_DP
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_ICON_TITLE_SPACER_HEIGHT_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_ICON_TITLE_SPACER_HEIGHT_LARGE_DP
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_ICON_TITLE_SPACER_HEIGHT_SMALL_DP
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_MARGIN_BOTTOM_DP
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_MARGIN_SIDE_PERCENTAGE
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.HEADER_MARGIN_TOP_DP
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITH_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE
-import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.MAIN_SLOT_WITH_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
 import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.METADATA_TAG
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.percentageHeightToDp
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.DEFAULT_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MAX_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MID_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MIN_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.customizedPrimaryLayoutMargin
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.DEFAULT
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.MAX
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.MID
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.MIN
+import androidx.wear.protolayout.modifiers.LayoutModifier
+import androidx.wear.protolayout.modifiers.padding
+import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.dp
 
 /**
  * ProtoLayout Material3 full screen layout that represents a suggested Material3 layout style that
@@ -99,19 +113,31 @@
  *   it an edge button, the given label will be ignored.
  * @param onClick The clickable action for whole layout. If any area (outside of other added
  *   tappable components) is clicked, it will fire the associated action.
+ * @param margins The customized outer margin that will be applied as following:
+ *     * `start` and `end` would be applied as a side margins on [mainSlot]
+ *     * `bottom` would be applied as a bottom margin when [bottomSlot] is not present.
+ *
+ *   It is highly recommended to use provided constants for these
+ *   margins - [DEFAULT_PRIMARY_LAYOUT_MARGIN], [MIN_PRIMARY_LAYOUT_MARGIN],
+ *   [MID_PRIMARY_LAYOUT_MARGIN] or [MAX_PRIMARY_LAYOUT_MARGIN], depending on inner content and its
+ *   corners shape. If providing custom numbers by [customizedPrimaryLayoutMargin], it is a
+ *   requirement for those to be percentages of the screen width and height.
+ *
  * @sample androidx.wear.protolayout.material3.samples.topLevelLayout
+ * @sample androidx.wear.protolayout.material3.samples.cardSample
+ * @sample androidx.wear.protolayout.material3.samples.oneSlotButtonsSample
+ * @sample androidx.wear.protolayout.material3.samples.graphicDataCardSample
  */
-// TODO: b/356568440 - Add sample above and put it in a proper samples file and link with @sample
 // TODO: b/346958146 - Link visuals once they are available.
 // TODO: b/353247528 - Handle the icon.
-// TODO: b/369162409 -Allow side and bottom margins in PrimaryLayout to be customizable.
 // TODO: b/370976767 - Specify that this should be used with MaterialTileService.
 public fun MaterialScope.primaryLayout(
     mainSlot: (MaterialScope.() -> LayoutElement),
     titleSlot: (MaterialScope.() -> LayoutElement)? = null,
     bottomSlot: (MaterialScope.() -> LayoutElement)? = null,
     labelForBottomSlot: (MaterialScope.() -> LayoutElement)? = null,
-    onClick: Clickable? = null
+    onClick: Clickable? = null,
+    margins: PrimaryLayoutMargins = DEFAULT_PRIMARY_LAYOUT_MARGIN
 ): LayoutElement =
     primaryLayoutWithOverrideIcon(
         overrideIcon = false,
@@ -119,7 +145,8 @@
         mainSlot = mainSlot,
         bottomSlot = bottomSlot,
         labelForBottomSlot = labelForBottomSlot,
-        onClick = onClick
+        onClick = onClick,
+        margins = margins
     )
 
 /**
@@ -135,6 +162,7 @@
     bottomSlot: (MaterialScope.() -> LayoutElement)? = null,
     labelForBottomSlot: (MaterialScope.() -> LayoutElement)? = null,
     onClick: Clickable? = null,
+    margins: PrimaryLayoutMargins = DEFAULT_PRIMARY_LAYOUT_MARGIN
 ): LayoutElement {
     val screenWidth = deviceConfiguration.screenWidthDp
     val screenHeight = deviceConfiguration.screenHeightDp
@@ -156,19 +184,6 @@
 
     onClick?.apply { modifiers.setClickable(this) }
 
-    val mainSlotSideMargin: DpProp =
-        dp(
-            screenWidth *
-                if (bottomSlot != null)
-                    (if (titleSlot != null)
-                        MAIN_SLOT_WITH_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
-                    else MAIN_SLOT_WITH_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE)
-                else
-                    (if (titleSlot != null)
-                        MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE
-                    else MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE)
-        )
-
     val mainLayout =
         Column.Builder()
             .setModifiers(modifiers.build())
@@ -191,19 +206,60 @@
                 )
             )
 
+    val bottomSlotValue = bottomSlot?.let { bottomSlot() }
+
+    val marginsValues: Padding =
+        withStyle(
+                layoutSlotsPresence =
+                    LayoutSlotsPresence(
+                        isTitleSlotPresent = titleSlot != null,
+                        isBottomSlotPresent = bottomSlot != null,
+                        isBottomSlotEdgeButton = bottomSlotValue?.isSlotEdgeButton() == true
+                    )
+            )
+            .let { scope ->
+                if (margins is PrimaryLayoutMarginsImpl) {
+                    when (margins.size) {
+                        MIN -> scope.minPrimaryLayoutMargins()
+                        MID -> scope.midPrimaryLayoutMargins()
+                        MAX -> scope.maxPrimaryLayoutMargins()
+                        DEFAULT -> scope.defaultPrimaryLayoutMargins()
+                        else -> scope.defaultPrimaryLayoutMargins()
+                    }
+                } else if (margins is CustomPrimaryLayoutMargins) {
+                    margins.toPadding(scope)
+                } else {
+                    // Fallback to default
+                    scope.defaultPrimaryLayoutMargins()
+                }
+            }
+
     // Contains main content. This Box is needed to set to expand, even if empty so it
     // fills the empty space until bottom content.
-    mainSlot?.let { mainLayout.addContent(mainSlot().getMainContentBox(mainSlotSideMargin)) }
+    mainSlot?.let {
+        mainLayout.addContent(
+            mainSlot()
+                .getMainContentBox(
+                    sideMargins = marginsValues,
+                    maxSideMarginFallbackDp = maxSideMargin()
+                )
+        )
+    }
 
     // Contains bottom slot, optional label or needed padding if empty.
-    mainLayout.addContent(getFooterContent(bottomSlot?.let { bottomSlot() }, labelSlot))
+    mainLayout.addContent(
+        getFooterContent(
+            bottomSlot = bottomSlotValue,
+            labelSlot = labelSlot,
+            bottomMarginForNoContentDp = marginsValues.bottom?.value ?: maxBottomMargin()
+        )
+    )
 
     return mainLayout.build()
 }
 
 private fun MaterialScope.getIconPlaceholder(overrideIcon: Boolean): LayoutElement {
-    val iconSlot =
-        Box.Builder().setWidth(HEADER_ICON_SIZE_DP.toDp()).setHeight(HEADER_ICON_SIZE_DP.toDp())
+    val iconSlot = Box.Builder().setWidth(HEADER_ICON_SIZE_DP.dp).setHeight(HEADER_ICON_SIZE_DP.dp)
     if (overrideIcon) {
         iconSlot.setModifiers(
             Modifiers.Builder()
@@ -233,7 +289,13 @@
 
     titleSlot?.apply {
         headerBuilder
-            .addContent(horizontalSpacer(HEADER_ICON_TITLE_SPACER_HEIGHT_DP))
+            .addContent(
+                horizontalSpacer(
+                    if (deviceConfiguration.screenHeightDp.isBreakpoint())
+                        HEADER_ICON_TITLE_SPACER_HEIGHT_LARGE_DP
+                    else HEADER_ICON_TITLE_SPACER_HEIGHT_SMALL_DP
+                )
+            )
             .addContent(titleSlot)
     }
 
@@ -241,22 +303,25 @@
 }
 
 /** Returns central slot with the optional main content. It expands to fill the available space. */
-private fun LayoutElement.getMainContentBox(sideMargin: DpProp): Box =
-    Box.Builder()
+private fun LayoutElement.getMainContentBox(
+    sideMargins: Padding,
+    maxSideMarginFallbackDp: Float,
+): Box {
+    // Start and end Padding shouldn't be null if these are predefined margins, but if developers
+    // sets some other object, we will fallback to the max margin.
+    val sideMarginStart = sideMargins.start?.value ?: maxSideMarginFallbackDp
+    val sideMarginEnd = sideMargins.end?.value ?: maxSideMarginFallbackDp
+    return Box.Builder()
         .setWidth(expand())
         .setHeight(expand())
         .setModifiers(
-            Modifiers.Builder()
-                .setPadding(
-                    Padding.Builder() // Top and bottom space has been added to other elements.
-                        .setStart(sideMargin)
-                        .setEnd(sideMargin)
-                        .build()
-                )
-                .build()
+            // Top and bottom space has been added to other elements.
+            LayoutModifier.padding(start = sideMarginStart, end = sideMarginEnd)
+                .toProtoLayoutModifiers()
         )
         .addContent(this)
         .build()
+}
 
 /**
  * Returns the footer content, containing bottom slot and optional label with the corresponding
@@ -265,22 +330,19 @@
  */
 private fun MaterialScope.getFooterContent(
     bottomSlot: LayoutElement?,
-    labelSlot: LayoutElement?
+    labelSlot: LayoutElement?,
+    bottomMarginForNoContentDp: Float
 ): LayoutElement {
     val footer = Box.Builder().setWidth(wrap()).setHeight(wrap())
 
     if (bottomSlot == null) {
         footer.setWidth(expand())
-        footer.setHeight(
-            dp(BOTTOM_SLOT_EMPTY_MARGIN_BOTTOM_PERCENTAGE * deviceConfiguration.screenHeightDp)
-        )
+        footer.setHeight(bottomMarginForNoContentDp.dp)
     } else if (bottomSlot.isSlotEdgeButton()) {
         // Label shouldn't be used with EdgeButton.
         footer.setModifiers(
             Modifiers.Builder()
-                .setPadding(
-                    Padding.Builder().setTop(BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP.toDp()).build()
-                )
+                .setPadding(Padding.Builder().setTop(BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP.dp).build())
                 .build()
         )
 
@@ -292,21 +354,10 @@
             Modifiers.Builder()
                 .setPadding(
                     Padding.Builder()
-                        .setTop(
-                            dp(
-                                (if (labelSlot == null)
-                                    BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_TOP_PERCENTAGE
-                                else BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_TOP_PERCENTAGE) *
-                                    deviceConfiguration.screenHeightDp
-                            )
-                        )
+                        .setTop(BOTTOM_SLOT_OTHER_MARGIN_TOP_DP.dp)
                         .setBottom(
-                            dp(
-                                (if (labelSlot == null)
-                                    BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_BOTTOM_PERCENTAGE
-                                else BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_BOTTOM_PERCENTAGE) *
-                                    deviceConfiguration.screenHeightDp
-                            )
+                            percentageHeightToDp(BOTTOM_SLOT_OTHER_MARGIN_BOTTOM_PERCENTAGE / 100)
+                                .dp
                         )
                         .build()
                 )
@@ -317,10 +368,9 @@
             otherBottomSlot
                 .addContent(
                     generateLabelContent(
-                        dp(
-                            FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE *
-                                deviceConfiguration.screenWidthDp
-                        )
+                        (FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE *
+                                deviceConfiguration.screenWidthDp)
+                            .dp
                     )
                 )
                 .addContent(horizontalSpacer(FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP))
@@ -330,10 +380,9 @@
             otherBottomSlot
                 .addContent(
                     bottomSlot.generateBottomSlotContent(
-                        dp(
-                            BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE *
-                                deviceConfiguration.screenWidthDp
-                        )
+                        (BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE *
+                                deviceConfiguration.screenWidthDp)
+                            .dp
                     )
                 )
                 .build()
@@ -363,47 +412,47 @@
         .addContent(this)
         .build()
 
-private fun MaterialScope.getMarginForHeader(): Padding {
-    return Padding.Builder()
-        .setTop(HEADER_MARGIN_TOP_DP.toDp())
-        .setBottom(HEADER_MARGIN_BOTTOM_DP.toDp())
-        .setStart(dp(HEADER_MARGIN_SIDE_PERCENTAGE * deviceConfiguration.screenWidthDp))
-        .setEnd(dp(HEADER_MARGIN_SIDE_PERCENTAGE * deviceConfiguration.screenWidthDp))
-        .build()
-}
+private fun MaterialScope.getMarginForHeader() =
+    padding(
+        top = HEADER_MARGIN_TOP_DP,
+        bottom = HEADER_MARGIN_BOTTOM_DP,
+        start = HEADER_MARGIN_SIDE_PERCENTAGE * deviceConfiguration.screenWidthDp,
+        end = HEADER_MARGIN_SIDE_PERCENTAGE * deviceConfiguration.screenWidthDp
+    )
 
 /** Contains the default values used by Material layout. */
 internal object PrimaryLayoutDefaults {
+    internal fun MaterialScope.percentageWidthToDp(
+        @FloatRange(from = 0.0, to = 1.0) percentage: Float
+    ): Float = percentage * deviceConfiguration.screenWidthDp
+
+    internal fun MaterialScope.percentageHeightToDp(
+        @FloatRange(from = 0.0, to = 1.0) percentage: Float
+    ): Float = percentage * deviceConfiguration.screenHeightDp
+
     /** Tool tag for Metadata in Modifiers, so we know that Row is actually a PrimaryLayout. */
-    @VisibleForTesting const val METADATA_TAG: String = "M3_PL"
+    @VisibleForTesting internal const val METADATA_TAG: String = "M3_PL"
 
-    @Dimension(unit = DP) const val HEADER_MARGIN_TOP_DP: Int = 3
+    @Dimension(DP) internal const val HEADER_MARGIN_TOP_DP = 3f
 
-    @Dimension(unit = DP) const val HEADER_MARGIN_BOTTOM_DP: Int = 6
+    @Dimension(DP) internal const val HEADER_MARGIN_BOTTOM_DP = 6f
 
-    const val HEADER_MARGIN_SIDE_PERCENTAGE: Float = 14.5f / 100
+    internal const val HEADER_MARGIN_SIDE_PERCENTAGE = 14.5f / 100
 
-    @Dimension(unit = DP) const val HEADER_ICON_SIZE_DP: Int = 24
+    @Dimension(DP) internal const val HEADER_ICON_SIZE_DP = 24f
 
-    @Dimension(unit = DP) const val HEADER_ICON_TITLE_SPACER_HEIGHT_DP: Int = 2
+    @Dimension(DP) internal const val HEADER_ICON_TITLE_SPACER_HEIGHT_SMALL_DP = 2
+    @Dimension(DP) internal const val HEADER_ICON_TITLE_SPACER_HEIGHT_LARGE_DP = 4
 
     // The remaining margins around EdgeButton are within the component itself.
-    @Dimension(unit = DP) const val BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP: Int = 4
+    @Dimension(DP) internal const val BOTTOM_EDGE_BUTTON_TOP_MARGIN_DP = 4f
 
-    const val BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_TOP_PERCENTAGE: Float = 4f / 100
-    const val BOTTOM_SLOT_OTHER_NO_LABEL_MARGIN_BOTTOM_PERCENTAGE: Float = 8.3f / 100
+    @Dimension(DP) internal const val BOTTOM_SLOT_OTHER_MARGIN_TOP_DP = 6f
+    internal const val BOTTOM_SLOT_OTHER_MARGIN_BOTTOM_PERCENTAGE = 5.2f
 
-    const val BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_TOP_PERCENTAGE: Float = 3f / 100
-    const val BOTTOM_SLOT_OTHER_WITH_LABEL_MARGIN_BOTTOM_PERCENTAGE: Float = 5f / 100
-    const val BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE: Float = 26f / 100
+    internal const val BOTTOM_SLOT_OTHER_MARGIN_SIDE_PERCENTAGE = 26f / 100
 
-    @Dimension(unit = DP) const val FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP: Int = 2
+    @Dimension(DP) internal const val FOOTER_LABEL_TO_BOTTOM_SLOT_SPACER_HEIGHT_DP = 2
 
-    const val FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE: Float = 16.64f / 100
-
-    const val BOTTOM_SLOT_EMPTY_MARGIN_BOTTOM_PERCENTAGE: Float = 14f / 100
-    const val MAIN_SLOT_WITH_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 3f / 100
-    const val MAIN_SLOT_WITH_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 6f / 100
-    const val MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITH_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 7.3f / 100
-    const val MAIN_SLOT_WITHOUT_BOTTOM_SLOT_WITHOUT_TITLE_MARGIN_SIDE_PERCENTAGE: Float = 8.3f / 100
+    internal const val FOOTER_LABEL_SLOT_MARGIN_SIDE_PERCENTAGE = 16.64f / 100
 }
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayoutMargins.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayoutMargins.kt
new file mode 100644
index 0000000..d38e0c5
--- /dev/null
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/PrimaryLayoutMargins.kt
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material3
+
+import androidx.annotation.FloatRange
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.ModifiersBuilders.Padding
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.percentageHeightToDp
+import androidx.wear.protolayout.material3.PrimaryLayoutDefaults.percentageWidthToDp
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.DEFAULT_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MAX_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MID_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MIN_PRIMARY_LAYOUT_MARGIN
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.DEFAULT
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.MAX
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.MID
+import androidx.wear.protolayout.material3.PrimaryLayoutMarginsImpl.Companion.MIN
+import kotlin.Float.Companion.NaN
+
+/**
+ * The set of margins for the [primaryLayout]'s customization.
+ *
+ * It is highly recommended to use these predefined values that are optimized for different screen
+ * sizes, content's corners and slots presences. Those are:
+ * * [MIN_PRIMARY_LAYOUT_MARGIN]
+ * * [MID_PRIMARY_LAYOUT_MARGIN]
+ * * [DEFAULT_PRIMARY_LAYOUT_MARGIN]
+ * * [MAX_PRIMARY_LAYOUT_MARGIN].
+ */
+public abstract class PrimaryLayoutMargins internal constructor() {
+    public companion object {
+        /**
+         * Default side margins for the main slot of [primaryLayout] that works for the majority of
+         * [Shapes] inside the main content components, usually across round toward medium round
+         * corners.
+         *
+         * The actual returned values depend on presence of slots in [primaryLayout] which will be
+         * applied automatically.
+         */
+        @JvmField
+        public val DEFAULT_PRIMARY_LAYOUT_MARGIN: PrimaryLayoutMargins =
+            PrimaryLayoutMarginsImpl(DEFAULT)
+
+        /**
+         * Min side margins for the main slot of [primaryLayout] that should be used only when the
+         * main slot contains content components with fully rounded corners, such as [Shapes.full]
+         * to avoid clipping.
+         *
+         * The actual returned values depend on presence of slots in [primaryLayout] which will be
+         * applied automatically.
+         */
+        @JvmField
+        public val MIN_PRIMARY_LAYOUT_MARGIN: PrimaryLayoutMargins = PrimaryLayoutMarginsImpl(MIN)
+
+        /**
+         * Mid side margins for the main slot of [primaryLayout] that should be used when the main
+         * slot contains content components with fully rounded to medium round corners, for example,
+         * larger than [Shapes.medium] to avoid clipping.
+         *
+         * The actual returned values depend on presence of slots in [primaryLayout] which will be
+         * applied automatically.
+         */
+        @JvmField
+        public val MID_PRIMARY_LAYOUT_MARGIN: PrimaryLayoutMargins = PrimaryLayoutMarginsImpl(MID)
+
+        /**
+         * Max side margins for the main slot of [primaryLayout] that should be used when the main
+         * slot contains content components with square corners, for example, smaller than
+         * [Shapes.medium] to avoid clipping.
+         *
+         * The actual returned values depend on presence of slots in [primaryLayout] which will be
+         * applied automatically.
+         */
+        @JvmField
+        public val MAX_PRIMARY_LAYOUT_MARGIN: PrimaryLayoutMargins = PrimaryLayoutMarginsImpl(MAX)
+
+        /**
+         * Creates new set of margins to be used for [primaryLayout] customization. The passed in
+         * values represent percentages based on the screen width.
+         *
+         * It is highly recommended to use predefined values instead of creating this custom one,
+         * because they are optimized for different screen sizes, content's corners and slots
+         * presences. Those predefined ones are:
+         * * [MIN_PRIMARY_LAYOUT_MARGIN]
+         * * [MID_PRIMARY_LAYOUT_MARGIN]
+         * * [DEFAULT_PRIMARY_LAYOUT_MARGIN]
+         * * [MAX_PRIMARY_LAYOUT_MARGIN].
+         *
+         * @param start Percentage of the screen width that should be applied as margin on the start
+         *   side
+         * @param end Percentage of the screen width that should be applied as margin on the end
+         *   side
+         */
+        public fun customizedPrimaryLayoutMargin(
+            @FloatRange(from = 0.0, to = 1.0) start: Float,
+            @FloatRange(from = 0.0, to = 1.0) end: Float
+        ): PrimaryLayoutMargins = CustomPrimaryLayoutMargins(start = start, end = end)
+
+        /**
+         * Creates new set of margins to be used for [primaryLayout] customization. The passed in
+         * values represent percentages based on the screen width and screen height.
+         *
+         * It is highly recommended to use predefined values instead of creating this custom one,
+         * because they are optimized for different screen sizes, content's corners and slots
+         * presences. Those predefined ones are:
+         * * [MIN_PRIMARY_LAYOUT_MARGIN]
+         * * [MID_PRIMARY_LAYOUT_MARGIN]
+         * * [DEFAULT_PRIMARY_LAYOUT_MARGIN]
+         * * [MAX_PRIMARY_LAYOUT_MARGIN].
+         *
+         * @param start Percentage of the screen width that should be applied as margin on the start
+         *   side
+         * @param end Percentage of the screen width that should be applied as margin on the end
+         *   side
+         * @param bottom Percentage of the screen height that should be applied as margin on the
+         *   bottom
+         */
+        public fun customizedPrimaryLayoutMargin(
+            @FloatRange(from = 0.0, to = 1.0) start: Float,
+            @FloatRange(from = 0.0, to = 1.0) end: Float,
+            @FloatRange(from = 0.0, to = 1.0) bottom: Float
+        ): PrimaryLayoutMargins =
+            CustomPrimaryLayoutMargins(start = start, end = end, bottom = bottom)
+    }
+}
+
+/**
+ * The predefined set of margin style sizes to be used by [primaryLayout] tod define default values.
+ */
+internal class PrimaryLayoutMarginsImpl internal constructor(internal val size: Int) :
+    PrimaryLayoutMargins() {
+    companion object {
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(AnnotationRetention.SOURCE)
+        @IntDef(DEFAULT, MIN, MID, MAX)
+        annotation class PrimaryLayoutStyleSizes
+
+        internal const val DEFAULT = 0
+        internal const val MIN = 1
+        internal const val MID = 2
+        internal const val MAX = 3
+    }
+}
+
+/**
+ * The custom set of margins for the [primaryLayout]'s customization.
+ *
+ * It is highly recommended to use predefined values instead of creating this custom once, because
+ * they are optimized for different screen sizes, content's corners and slots presences. Those
+ * predefined once are:
+ * * [MIN_PRIMARY_LAYOUT_MARGIN]
+ * * [MID_PRIMARY_LAYOUT_MARGIN]
+ * * [DEFAULT_PRIMARY_LAYOUT_MARGIN]
+ * * [MAX_PRIMARY_LAYOUT_MARGIN].
+ */
+internal class CustomPrimaryLayoutMargins
+/**
+ * Creates new set of margins to be used for [primaryLayout] customization. The passed in values
+ * represent percentages based on the screen width.
+ *
+ * @param start Percentage of the screen width that should be applied as margin on the start side
+ * @param end Percentage of the screen width that should be applied as margin on the end side
+ */
+(
+    @FloatRange(from = 0.0, to = 1.0) internal val start: Float,
+    @FloatRange(from = 0.0, to = 1.0) internal val end: Float
+) : PrimaryLayoutMargins() {
+    internal var bottom: Float = NaN
+
+    /**
+     * Creates new set of margins to be used for [primaryLayout] customization. The passed in values
+     * represent percentages based on the screen width and screen height.
+     *
+     * @param start Percentage of the screen width that should be applied as margin on the start
+     *   side
+     * @param end Percentage of the screen width that should be applied as margin on the end side
+     * @param bottom Percentage of the screen height that should be applied as margin on the bottom
+     */
+    constructor(
+        @FloatRange(from = 0.0, to = 1.0) start: Float,
+        @FloatRange(from = 0.0, to = 1.0) end: Float,
+        @FloatRange(from = 0.0, to = 1.0) bottom: Float
+    ) : this(start = start, end = end) {
+        this.bottom = bottom
+    }
+
+    /** Returns the given margins as [Padding] object. */
+    internal fun toPadding(scope: MaterialScope): Padding =
+        if (bottom.isNaN()) {
+            scope.percentagePadding(start = start, end = end)
+        } else {
+            scope.percentagePadding(start = start, end = end, bottom = bottom)
+        }
+}
+
+/**
+ * Default values for margins used in [primaryLayout] based on slots presence from [materialScope].
+ */
+internal object PredefinedPrimaryLayoutMargins {
+    /**
+     * Default side margins for the main slot of [primaryLayout] that works for the majority of
+     * [Shapes] inside the main content components, usually across round toward medium round
+     * corners.
+     *
+     * The actual returned values depend on presence of slots in [primaryLayout] which will be
+     * applied automatically.
+     */
+    fun MaterialScope.defaultPrimaryLayoutMargins(): Padding =
+        if (layoutSlotsPresence.isTitleSlotPresent) {
+            if (layoutSlotsPresence.isBottomSlotPresent) {
+                if (layoutSlotsPresence.isBottomSlotEdgeButton)
+                    defaultPrimaryLayoutMarginsWithTitleWithEdgeButton()
+                else defaultPrimaryLayoutMarginsWithBottomSlotAsOther()
+            } else {
+                defaultPrimaryLayoutMarginsWithTitleWithoutBottomSlot()
+            }
+        } else {
+            if (layoutSlotsPresence.isBottomSlotPresent) {
+                if (layoutSlotsPresence.isBottomSlotEdgeButton)
+                    defaultPrimaryLayoutMarginsWithoutTitleWithEdgeButton()
+                else defaultPrimaryLayoutMarginsWithBottomSlotAsOther()
+            } else {
+                defaultPrimaryLayoutMarginsWithoutTitleWithoutBottomSlot()
+            }
+        }
+
+    /**
+     * Min side margins for the main slot of [primaryLayout] that should be used only when the main
+     * slot contains content components with fully rounded corners, such as [Shapes.full] to avoid
+     * clipping.
+     *
+     * The actual returned values depend on presence of slots in [primaryLayout] which will be
+     * applied automatically.
+     */
+    internal fun MaterialScope.minPrimaryLayoutMargins(): Padding =
+        // Values are the same regardless of title slot presence.
+        if (layoutSlotsPresence.isBottomSlotPresent) {
+            if (layoutSlotsPresence.isBottomSlotEdgeButton) minPrimaryLayoutMarginsWithEdgeButton()
+            else minPrimaryLayoutMarginsWithBottomSlotAsOther()
+        } else {
+            minPrimaryLayoutMarginsWithoutBottomSlot()
+        }
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains content components with fully rounded to medium round corners, for example, larger
+     * than [Shapes.medium] to avoid clipping.
+     *
+     * The actual returned values depend on presence of slots in [primaryLayout] which will be
+     * applied automatically.
+     */
+    internal fun MaterialScope.midPrimaryLayoutMargins(): Padding =
+        if (layoutSlotsPresence.isTitleSlotPresent) {
+            if (layoutSlotsPresence.isBottomSlotPresent) {
+                if (layoutSlotsPresence.isBottomSlotEdgeButton)
+                    midPrimaryLayoutMarginsWithTitleWithEdgeButton()
+                else midPrimaryLayoutMarginsWithTitleWithBottomSlotAsOther()
+            } else {
+                midPrimaryLayoutMarginsWithTitleWithoutBottomSlot()
+            }
+        } else {
+            if (layoutSlotsPresence.isBottomSlotPresent) {
+                if (layoutSlotsPresence.isBottomSlotEdgeButton)
+                    midPrimaryLayoutMarginsWithoutTitleWithEdgeButton()
+                else midPrimaryLayoutMarginsWithoutTitleWithBottomSlotAsOther()
+            } else {
+                midPrimaryLayoutMarginsWithoutTitleWithoutBottomSlot()
+            }
+        }
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains content components with square corners, for example, smaller than [Shapes.medium] to
+     * avoid clipping.
+     *
+     * The actual returned values depend on presence of slots in [primaryLayout] which will be
+     * applied automatically.
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMargins(): Padding =
+        if (layoutSlotsPresence.isTitleSlotPresent) {
+            if (layoutSlotsPresence.isBottomSlotPresent) {
+                if (layoutSlotsPresence.isBottomSlotEdgeButton)
+                    maxPrimaryLayoutMarginsWithTitleWithEdgeButton()
+                else maxPrimaryLayoutMarginsWithTitleWithBottomSlotAsOther()
+            } else {
+                maxPrimaryLayoutMarginsWithTitleWithoutBottomSlot()
+            }
+        } else {
+            if (layoutSlotsPresence.isBottomSlotPresent) {
+                if (layoutSlotsPresence.isBottomSlotEdgeButton)
+                    maxPrimaryLayoutMarginsWithoutTitleWithEdgeButton()
+                else maxPrimaryLayoutMarginsWithoutTitleWithBottomSlotAsOther()
+            } else {
+                maxPrimaryLayoutMarginsWithoutTitleWithoutBottomSlot()
+            }
+        }
+
+    // Separate all cases internally so it's easier to track from the spec.
+
+    // Bottom slot as EdgeButton. Bottom margin are not allowed to be customized in these cases.
+
+    /**
+     * Default side margins for the main slot of [primaryLayout] that should be used when there is a
+     * title slot in the layout and bottom slot is set to be [edgeButton].
+     *
+     * These values work for the majority of [Shapes], usually across round toward medium round
+     * corners.
+     */
+    internal fun MaterialScope.defaultPrimaryLayoutMarginsWithTitleWithEdgeButton(): Padding =
+        percentagePadding(start = 7.1f / 100, end = 7.1f / 100)
+
+    /**
+     * Default side margins for the main slot of [primaryLayout] that should be used when the title
+     * slot is not present in the layout and bottom slot is set to be [edgeButton].
+     *
+     * These values work for the majority of [Shapes], usually across round toward medium round
+     * corners.
+     */
+    internal fun MaterialScope.defaultPrimaryLayoutMarginsWithoutTitleWithEdgeButton(): Padding =
+        percentagePadding(start = 12f / 100, end = 12f / 100)
+
+    /**
+     * Min side margins for the main slot of [primaryLayout] that should be used only when the main
+     * slot contains fully rounded corners, such as [Shapes.full] to avoid clipping, and when the
+     * bottom slot is set to be [edgeButton].
+     *
+     * This can be used regardless of the title slot presence.
+     */
+    internal fun MaterialScope.minPrimaryLayoutMarginsWithEdgeButton(): Padding =
+        percentagePadding(start = 3f / 100, end = 3f / 100)
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains fully rounded to medium round corners, for example, larger than [Shapes.medium] to
+     * avoid clipping, and when there's title slot present and bottom slot is set to be
+     * [edgeButton].
+     */
+    internal fun MaterialScope.midPrimaryLayoutMarginsWithTitleWithEdgeButton(): Padding =
+        percentagePadding(start = 5.2f / 100, end = 5.2f / 100)
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains fully rounded to medium round corners, for example, larger than [Shapes.medium] to
+     * avoid clipping, and when title slot is not present and bottom slot is set to be [edgeButton].
+     */
+    internal fun MaterialScope.midPrimaryLayoutMarginsWithoutTitleWithEdgeButton(): Padding =
+        percentagePadding(start = 7.1f / 100, end = 7.1f / 100)
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains square corners, for example, smaller than [Shapes.medium] to avoid clipping, and
+     * when there's title slot present and bottom slot is set to be [edgeButton].
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMarginsWithTitleWithEdgeButton(): Padding =
+        percentagePadding(start = 10f / 100, end = 10f / 100)
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains square corners, for example, smaller than [Shapes.medium] to avoid clipping, and
+     * when title slot is not present and bottom slot is set to be [edgeButton].
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMarginsWithoutTitleWithEdgeButton(): Padding =
+        percentagePadding(start = 15.5f / 100, end = 15.5f / 100)
+
+    // Bottom slot as other content. Bottom margin are not allowed to be customized in these cases.
+
+    /**
+     * Default side margins for the main slot of [primaryLayout] that should be used when the bottom
+     * slot is set to some other content besides [edgeButton].
+     *
+     * These values work for the majority of [Shapes], usually across round toward medium round
+     * corners. This can be used regardless of title slot presence.
+     */
+    internal fun MaterialScope.defaultPrimaryLayoutMarginsWithBottomSlotAsOther(): Padding =
+        percentagePadding(start = 12f / 100, end = 12f / 100)
+
+    /**
+     * Min side margins for the main slot of [primaryLayout] that should be used only when the main
+     * slot contains fully rounded corners, such as [Shapes.full] to avoid clipping, and when the
+     * bottom slot is set to some other content besides [edgeButton].
+     *
+     * This can be used regardless of the title slot presence.
+     */
+    internal fun MaterialScope.minPrimaryLayoutMarginsWithBottomSlotAsOther(): Padding =
+        percentagePadding(start = 3f / 100, end = 3f / 100)
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains fully rounded to medium round corners, for example, larger than [Shapes.medium] to
+     * avoid clipping, and when there's title slot present and bottom slot is set to some other
+     * content besides [edgeButton].
+     */
+    internal fun MaterialScope.midPrimaryLayoutMarginsWithTitleWithBottomSlotAsOther(): Padding =
+        percentagePadding(start = 7.1f / 100, end = 7.1f / 100)
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains fully rounded to medium round corners, for example, larger than [Shapes.medium] to
+     * avoid clipping, and when title slot is not present and bottom slot is set to some other
+     * content besides [edgeButton].
+     */
+    internal fun MaterialScope.midPrimaryLayoutMarginsWithoutTitleWithBottomSlotAsOther(): Padding =
+        percentagePadding(start = 8.3f / 100, end = 8.3f / 100)
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains square corners, for example, smaller than [Shapes.medium] to avoid clipping, and
+     * when there's title slot present and bottom slot is set to some other content besides
+     * [edgeButton].
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMarginsWithTitleWithBottomSlotAsOther(): Padding =
+        percentagePadding(start = 14f / 100, end = 14f / 100)
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains square corners, for example, smaller than [Shapes.medium] to avoid clipping, and
+     * when title slot is not present and bottom slot is set to some other content besides
+     * [edgeButton].
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMarginsWithoutTitleWithBottomSlotAsOther(): Padding =
+        percentagePadding(start = 15.5f / 100, end = 15.5f / 100)
+
+    // No bottom slot. Bottom margin are allowed to be customized in these cases.
+
+    /**
+     * Default side margins for the main slot of [primaryLayout] that should be used when title slot
+     * is present but the bottom slot is not.
+     *
+     * These values work for the majority of [Shapes], usually across round toward medium round
+     * corners.
+     */
+    internal fun MaterialScope.defaultPrimaryLayoutMarginsWithTitleWithoutBottomSlot(): Padding =
+        percentagePadding(start = 10f / 100, end = 10f / 100, bottom = 16.64f / 100)
+
+    /**
+     * Default side margins for the main slot of [primaryLayout] that should be used when neither
+     * title slot or bottom slot are present.
+     *
+     * These values work for the majority of [Shapes], usually across round toward medium round
+     * corners.
+     */
+    internal fun MaterialScope.defaultPrimaryLayoutMarginsWithoutTitleWithoutBottomSlot(): Padding =
+        percentagePadding(start = 12f / 100, end = 12f / 100, bottom = 14f / 100)
+
+    /**
+     * Min side margins for the main slot of [primaryLayout] that should be used only when the main
+     * slot contains fully rounded corners, such as [Shapes.full] to avoid clipping, and when the
+     * bottom slot is not present.
+     *
+     * This can be used regardless of the title slot presence.
+     */
+    internal fun MaterialScope.minPrimaryLayoutMarginsWithoutBottomSlot(): Padding =
+        percentagePadding(start = 3f / 100, end = 3f / 100, bottom = 10f / 100)
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains fully rounded to medium round corners, for example, larger than [Shapes.medium] to
+     * avoid clipping, and when there's title slot present and bottom slot is not.
+     */
+    internal fun MaterialScope.midPrimaryLayoutMarginsWithTitleWithoutBottomSlot(): Padding =
+        percentagePadding(start = 5.2f / 100, end = 5.2f / 100, bottom = 19.6f / 100)
+
+    /**
+     * Mid side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains fully rounded to medium round corners, for example, larger than [Shapes.medium] to
+     * avoid clipping, and when neither title slot or bottom slot are present.
+     */
+    internal fun MaterialScope.midPrimaryLayoutMarginsWithoutTitleWithoutBottomSlot(): Padding =
+        percentagePadding(start = 8.3f / 100, end = 8.3f / 100, bottom = 19.6f / 100)
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains square corners, for example, smaller than [Shapes.medium] to avoid clipping, and
+     * when there's title slot present and bottom slot is not.
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMarginsWithTitleWithoutBottomSlot(): Padding =
+        percentagePadding(start = 14f / 100, end = 14f / 100, bottom = 16.64f / 100)
+
+    /**
+     * Max side margins for the main slot of [primaryLayout] that should be used when the main slot
+     * contains square corners, for example, smaller than [Shapes.medium] to avoid clipping, and
+     * when neither title slot or bottom slot are present.
+     */
+    internal fun MaterialScope.maxPrimaryLayoutMarginsWithoutTitleWithoutBottomSlot(): Padding =
+        percentagePadding(start = 16.64f / 100, end = 16.64f / 100, bottom = 14f / 100)
+
+    internal fun MaterialScope.maxSideMargin() = percentageWidthToDp(16.64f / 100)
+
+    internal fun MaterialScope.maxBottomMargin() = percentageHeightToDp(19.6f / 100)
+}
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/MaterialScopeTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/MaterialScopeTest.kt
index cf470ad..1f3bd04 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/MaterialScopeTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/MaterialScopeTest.kt
@@ -56,7 +56,8 @@
                 defaultTextElementStyle = TextElementStyle(),
                 defaultIconStyle = IconStyle(),
                 defaultBackgroundImageStyle = BackgroundImageStyle(),
-                defaultAvatarImageStyle = AvatarImageStyle()
+                defaultAvatarImageStyle = AvatarImageStyle(),
+                layoutSlotsPresence = LayoutSlotsPresence()
             )
 
         assertThat(scopeWithDefaultTheme.deviceConfiguration).isEqualTo(DEVICE_PARAMETERS)
@@ -88,7 +89,8 @@
                 defaultTextElementStyle = TextElementStyle(),
                 defaultIconStyle = IconStyle(),
                 defaultBackgroundImageStyle = BackgroundImageStyle(),
-                defaultAvatarImageStyle = AvatarImageStyle()
+                defaultAvatarImageStyle = AvatarImageStyle(),
+                layoutSlotsPresence = LayoutSlotsPresence()
             )
 
         assertThat(materialScope.deviceConfiguration).isEqualTo(DEVICE_PARAMETERS)
@@ -124,7 +126,8 @@
                 defaultTextElementStyle = TextElementStyle(),
                 defaultIconStyle = IconStyle(),
                 defaultBackgroundImageStyle = BackgroundImageStyle(),
-                defaultAvatarImageStyle = AvatarImageStyle()
+                defaultAvatarImageStyle = AvatarImageStyle(),
+                layoutSlotsPresence = LayoutSlotsPresence()
             )
 
         assertThat(isDynamicColorSchemeEnabled(materialScope.context)).isFalse()
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/PrimaryLayoutMarginsTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/PrimaryLayoutMarginsTest.kt
new file mode 100644
index 0000000..024847d
--- /dev/null
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/PrimaryLayoutMarginsTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material3
+
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.wear.protolayout.material3.MaterialScopeTest.Companion.DEVICE_PARAMETERS
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.customizedPrimaryLayoutMargin
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(AndroidJUnit4::class)
+@DoNotInstrument
+class PrimaryLayoutMarginsTest {
+    @Test
+    fun customizedMargins_horizontal_buildsPadding() {
+        val start = 0.2f
+        val end = 0.3f
+        val margins: CustomPrimaryLayoutMargins =
+            customizedPrimaryLayoutMargin(start = start, end = end) as CustomPrimaryLayoutMargins
+
+        assertThat(margins.toPadding(SCOPE).start!!.value)
+            .isEqualTo(start * DEVICE_PARAMETERS.screenWidthDp)
+        assertThat(margins.toPadding(SCOPE).end!!.value)
+            .isEqualTo(end * DEVICE_PARAMETERS.screenWidthDp)
+    }
+
+    @Test
+    fun customizedMargins_all_buildsPadding() {
+        val start = 0.2f
+        val end = 0.3f
+        val bottom = 0.4f
+        val margins: CustomPrimaryLayoutMargins =
+            customizedPrimaryLayoutMargin(start = start, end = end, bottom = bottom)
+                as CustomPrimaryLayoutMargins
+
+        assertThat(margins.toPadding(SCOPE).start!!.value)
+            .isEqualTo(start * DEVICE_PARAMETERS.screenWidthDp)
+        assertThat(margins.toPadding(SCOPE).end!!.value)
+            .isEqualTo(end * DEVICE_PARAMETERS.screenWidthDp)
+        assertThat(margins.toPadding(SCOPE).bottom!!.value)
+            .isEqualTo(bottom * DEVICE_PARAMETERS.screenWidthDp)
+    }
+
+    companion object {
+        val SCOPE =
+            MaterialScope(
+                context = getApplicationContext(),
+                deviceConfiguration = DEVICE_PARAMETERS,
+                allowDynamicTheme = true,
+                theme =
+                    MaterialTheme(
+                        colorScheme = dynamicColorScheme(context = getApplicationContext())
+                    ),
+                defaultTextElementStyle = TextElementStyle(),
+                defaultIconStyle = IconStyle(),
+                defaultBackgroundImageStyle = BackgroundImageStyle(),
+                defaultAvatarImageStyle = AvatarImageStyle(),
+                layoutSlotsPresence = LayoutSlotsPresence()
+            )
+    }
+}
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
index 183279b..6b0db65 100644
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
+++ b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
@@ -36,6 +36,7 @@
 import androidx.wear.protolayout.material3.DataCardStyle.Companion.extraLargeDataCardStyle
 import androidx.wear.protolayout.material3.DataCardStyle.Companion.smallCompactDataCardStyle
 import androidx.wear.protolayout.material3.MaterialScope
+import androidx.wear.protolayout.material3.PrimaryLayoutMargins.Companion.MAX_PRIMARY_LAYOUT_MARGIN
 import androidx.wear.protolayout.material3.TextButtonStyle.Companion.smallTextButtonStyle
 import androidx.wear.protolayout.material3.appCard
 import androidx.wear.protolayout.material3.avatarImage
@@ -126,7 +127,8 @@
 ): LayoutElementBuilders.LayoutElement =
     materialScope(context = context, deviceConfiguration = requestParams.deviceConfiguration) {
         primaryLayout(
-            mainSlot = { graphicDataCardSample() },
+            mainSlot = { oneSlotButtons() },
+            margins = MAX_PRIMARY_LAYOUT_MARGIN,
             bottomSlot = {
                 textEdgeButton(
                     onClick = clickable(),