Skip to content

Commit 2f0935a

Browse files
imhappidsn5ft
authored andcommitted
[Card] Support StateListShapeAppearance in MaterialCardView
PiperOrigin-RevId: 785529211
1 parent 39c27cd commit 2f0935a

File tree

6 files changed

+139
-49
lines changed

6 files changed

+139
-49
lines changed

lib/java/com/google/android/material/button/MaterialButtonGroup.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -548,14 +548,12 @@ void updateChildShapes() {
548548
// When horizontal (ltr), keeps the left two original corners for the first button.
549549
if (isFirstVisible) {
550550
cornerPositionBitsToKeep |=
551-
StateListShapeAppearanceModel.CORNER_TOP_LEFT
552-
| StateListShapeAppearanceModel.CORNER_BOTTOM_LEFT;
551+
ShapeAppearanceModel.CORNER_TOP_LEFT | ShapeAppearanceModel.CORNER_BOTTOM_LEFT;
553552
}
554553
// When horizontal (ltr), keeps the right two original corners for the last button.
555554
if (isLastVisible) {
556555
cornerPositionBitsToKeep |=
557-
StateListShapeAppearanceModel.CORNER_TOP_RIGHT
558-
| StateListShapeAppearanceModel.CORNER_BOTTOM_RIGHT;
556+
ShapeAppearanceModel.CORNER_TOP_RIGHT | ShapeAppearanceModel.CORNER_BOTTOM_RIGHT;
559557
}
560558
// If rtl, swap the position bits of left corners and right corners.
561559
if (isRtl) {
@@ -566,14 +564,12 @@ void updateChildShapes() {
566564
// When vertical, keeps the top two original corners for the first button.
567565
if (isFirstVisible) {
568566
cornerPositionBitsToKeep |=
569-
StateListShapeAppearanceModel.CORNER_TOP_LEFT
570-
| StateListShapeAppearanceModel.CORNER_TOP_RIGHT;
567+
ShapeAppearanceModel.CORNER_TOP_LEFT | ShapeAppearanceModel.CORNER_TOP_RIGHT;
571568
}
572569
// When vertical, keeps the bottom two original corners for the last button.
573570
if (isLastVisible) {
574571
cornerPositionBitsToKeep |=
575-
StateListShapeAppearanceModel.CORNER_BOTTOM_LEFT
576-
| StateListShapeAppearanceModel.CORNER_BOTTOM_RIGHT;
572+
ShapeAppearanceModel.CORNER_BOTTOM_LEFT | ShapeAppearanceModel.CORNER_BOTTOM_RIGHT;
577573
}
578574
}
579575
// Overrides the corners that don't need to keep with unary operator.

lib/java/com/google/android/material/card/MaterialCardView.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ private RectF getBoundsAsRectF() {
666666
@Override
667667
public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) {
668668
setClipToOutline(shapeAppearanceModel.isRoundRect(getBoundsAsRectF()));
669-
cardViewHelper.setShapeAppearanceModel(shapeAppearanceModel);
669+
cardViewHelper.setShapeAppearance(shapeAppearanceModel);
670670
}
671671

672672
/**
@@ -677,7 +677,7 @@ public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanc
677677
@NonNull
678678
@Override
679679
public ShapeAppearanceModel getShapeAppearanceModel() {
680-
return cardViewHelper.getShapeAppearanceModel();
680+
return cardViewHelper.getShapeAppearance().getDefaultShape();
681681
}
682682

683683
private void forceRippleRedrawIfNeeded() {

lib/java/com/google/android/material/card/MaterialCardViewHelper.java

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import android.animation.TimeInterpolator;
2525
import android.animation.ValueAnimator;
26+
import android.content.Context;
2627
import android.content.res.ColorStateList;
2728
import android.content.res.TypedArray;
2829
import android.graphics.Color;
@@ -47,6 +48,7 @@
4748
import androidx.annotation.StyleRes;
4849
import androidx.cardview.widget.CardView;
4950
import androidx.core.graphics.drawable.DrawableCompat;
51+
import androidx.dynamicanimation.animation.SpringForce;
5052
import com.google.android.material.animation.AnimationUtils;
5153
import com.google.android.material.card.MaterialCardView.CheckedIconGravity;
5254
import com.google.android.material.color.MaterialColors;
@@ -56,7 +58,9 @@
5658
import com.google.android.material.shape.CutCornerTreatment;
5759
import com.google.android.material.shape.MaterialShapeDrawable;
5860
import com.google.android.material.shape.RoundedCornerTreatment;
61+
import com.google.android.material.shape.ShapeAppearance;
5962
import com.google.android.material.shape.ShapeAppearanceModel;
63+
import com.google.android.material.shape.StateListShapeAppearanceModel;
6064

6165
/** @hide */
6266
@RestrictTo(LIBRARY_GROUP)
@@ -89,6 +93,8 @@ class MaterialCardViewHelper {
8993

9094
private static final int CHECKED_ICON_LAYER_INDEX = 2;
9195

96+
private static final int NOT_SET = -1;
97+
9298
// We need to create a dummy drawable to avoid LayerDrawable crashes on API 28-.
9399
private static final Drawable CHECKED_ICON_NONE =
94100
VERSION.SDK_INT <= VERSION_CODES.P ? new ColorDrawable() : null;
@@ -101,6 +107,7 @@ class MaterialCardViewHelper {
101107

102108
// Will always wrapped in an InsetDrawable
103109
@NonNull private final MaterialShapeDrawable foregroundContentDrawable;
110+
private float cardCornerRadius = NOT_SET;
104111

105112
@Dimension private int checkedIconMargin;
106113
@Dimension private int checkedIconSize;
@@ -112,7 +119,7 @@ class MaterialCardViewHelper {
112119
@Nullable private Drawable checkedIcon;
113120
@Nullable private ColorStateList rippleColor;
114121
@Nullable private ColorStateList checkedIconTint;
115-
@Nullable private ShapeAppearanceModel shapeAppearanceModel;
122+
@NonNull private ShapeAppearance shapeAppearanceModel;
116123
@Nullable private ColorStateList strokeColor;
117124
@Nullable private Drawable rippleDrawable;
118125
@Nullable private LayerDrawable clickableForegroundDrawable;
@@ -135,11 +142,6 @@ public MaterialCardViewHelper(
135142
int defStyleAttr,
136143
@StyleRes int defStyleRes) {
137144
materialCardView = card;
138-
bgDrawable = new MaterialShapeDrawable(card.getContext(), attrs, defStyleAttr, defStyleRes);
139-
bgDrawable.initializeElevationOverlay(card.getContext());
140-
bgDrawable.setShadowColor(Color.DKGRAY);
141-
ShapeAppearanceModel.Builder shapeAppearanceModelBuilder =
142-
bgDrawable.getShapeAppearanceModel().toBuilder();
143145

144146
TypedArray cardViewAttributes =
145147
card.getContext()
@@ -148,15 +150,21 @@ public MaterialCardViewHelper(
148150
androidx.cardview.R.styleable.CardView,
149151
defStyleAttr,
150152
androidx.cardview.R.style.CardView);
153+
bgDrawable = new MaterialShapeDrawable(card.getContext(), attrs, defStyleAttr, defStyleRes);
154+
bgDrawable.initializeElevationOverlay(card.getContext());
155+
bgDrawable.setShadowColor(Color.DKGRAY);
156+
ShapeAppearanceModel.Builder shapeAppearanceModelBuilder =
157+
bgDrawable.getShapeAppearanceModel().toBuilder();
158+
151159
if (cardViewAttributes.hasValue(androidx.cardview.R.styleable.CardView_cardCornerRadius)) {
152-
// If cardCornerRadius is set, let it override the shape appearance.
153-
shapeAppearanceModelBuilder.setAllCornerSizes(
154-
cardViewAttributes.getDimension(
155-
androidx.cardview.R.styleable.CardView_cardCornerRadius, 0));
160+
// If cardCornerRadius is set, remember it so we can let it override the shape appearance
161+
cardCornerRadius = cardViewAttributes.getDimension(
162+
androidx.cardview.R.styleable.CardView_cardCornerRadius, 0);
163+
shapeAppearanceModelBuilder.setAllCornerSizes(cardCornerRadius);
156164
}
157165

158166
foregroundContentDrawable = new MaterialShapeDrawable();
159-
setShapeAppearanceModel(shapeAppearanceModelBuilder.build());
167+
setShapeAppearance(shapeAppearanceModelBuilder.build());
160168

161169
iconFadeAnimInterpolator =
162170
MotionUtils.resolveThemeInterpolator(
@@ -224,6 +232,24 @@ void loadFromAttributes(@NonNull TypedArray attributes) {
224232
fgDrawable =
225233
shouldUseClickableForeground() ? getClickableForeground() : foregroundContentDrawable;
226234
materialCardView.setForeground(insetDrawable(fgDrawable));
235+
236+
// Card corner radius overrides the shape appearance in precedence.
237+
if (cardCornerRadius == NOT_SET) {
238+
StateListShapeAppearanceModel stateListShapeAppearanceModel =
239+
StateListShapeAppearanceModel.create(
240+
materialCardView.getContext(),
241+
attributes,
242+
R.styleable.MaterialCardView_shapeAppearance);
243+
if (stateListShapeAppearanceModel != null) {
244+
SpringForce springForce = createSpringForce(materialCardView.getContext());
245+
bgDrawable.setCornerSpringForce(springForce);
246+
foregroundContentDrawable.setCornerSpringForce(springForce);
247+
if (foregroundShapeDrawable != null) {
248+
foregroundShapeDrawable.setCornerSpringForce(springForce);
249+
}
250+
setShapeAppearance(stateListShapeAppearanceModel);
251+
}
252+
}
227253
}
228254

229255
boolean isBackgroundOverwritten() {
@@ -333,7 +359,9 @@ public void animateCheckedIcon(boolean checked) {
333359
}
334360

335361
void setCornerRadius(float cornerRadius) {
336-
setShapeAppearanceModel(shapeAppearanceModel.withCornerSize(cornerRadius));
362+
cardCornerRadius = cornerRadius;
363+
setShapeAppearance(
364+
shapeAppearanceModel.getDefaultShape().withCornerSize(cornerRadius));
337365
fgDrawable.invalidateSelf();
338366
if (shouldAddCornerPaddingOutsideCardBackground()
339367
|| shouldAddCornerPaddingInsideCardBackground()) {
@@ -525,20 +553,18 @@ void forceRippleRedraw() {
525553
}
526554
}
527555

528-
void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) {
556+
void setShapeAppearance(@NonNull ShapeAppearance shapeAppearanceModel) {
529557
this.shapeAppearanceModel = shapeAppearanceModel;
530-
bgDrawable.setShapeAppearanceModel(shapeAppearanceModel);
531-
bgDrawable.setShadowBitmapDrawingEnable(!bgDrawable.isRoundRect());
532-
if (foregroundContentDrawable != null) {
533-
foregroundContentDrawable.setShapeAppearanceModel(shapeAppearanceModel);
534-
}
535-
558+
bgDrawable.setShapeAppearance(shapeAppearanceModel);
559+
foregroundContentDrawable.setShapeAppearance(shapeAppearanceModel);
536560
if (foregroundShapeDrawable != null) {
537-
foregroundShapeDrawable.setShapeAppearanceModel(shapeAppearanceModel);
561+
foregroundShapeDrawable.setShapeAppearance(shapeAppearanceModel);
538562
}
563+
bgDrawable.setShadowBitmapDrawingEnable(!bgDrawable.isRoundRect());
539564
}
540565

541-
ShapeAppearanceModel getShapeAppearanceModel() {
566+
@NonNull
567+
ShapeAppearance getShapeAppearance() {
542568
return shapeAppearanceModel;
543569
}
544570

@@ -639,15 +665,7 @@ && canClipToOutline()
639665
&& materialCardView.getUseCompatPadding();
640666
}
641667

642-
/**
643-
* Calculates the amount of padding required between the card background shape and the card
644-
* content such that the entire content is within the bounds of the card background shape.
645-
*
646-
* <p>This should only be called when either {@link
647-
* #shouldAddCornerPaddingOutsideCardBackground()} or {@link
648-
* #shouldAddCornerPaddingInsideCardBackground()} returns true.
649-
*/
650-
private float calculateActualCornerPadding() {
668+
private float getMaxCornerPadding(@NonNull ShapeAppearanceModel shapeAppearanceModel) {
651669
return Math.max(
652670
Math.max(
653671
calculateCornerPaddingForCornerTreatment(
@@ -664,6 +682,26 @@ private float calculateActualCornerPadding() {
664682
bgDrawable.getBottomLeftCornerResolvedSize())));
665683
}
666684

685+
/**
686+
* Calculates the amount of padding required between the card background shape and the card
687+
* content such that the entire content is within the bounds of the card background shape.
688+
*
689+
* <p>This should only be called when either {@link
690+
* #shouldAddCornerPaddingOutsideCardBackground()} or {@link
691+
* #shouldAddCornerPaddingInsideCardBackground()} returns true.
692+
*/
693+
private float calculateActualCornerPadding() {
694+
float maxCornerPadding = 0;
695+
ShapeAppearanceModel[] shapeAppearanceModels =
696+
shapeAppearanceModel.getShapeAppearanceModels();
697+
for (ShapeAppearanceModel shapeAppearanceModel : shapeAppearanceModels) {
698+
if (shapeAppearanceModel != null) {
699+
maxCornerPadding = Math.max(maxCornerPadding, getMaxCornerPadding(shapeAppearanceModel));
700+
}
701+
}
702+
return maxCornerPadding;
703+
}
704+
667705
private float calculateCornerPaddingForCornerTreatment(CornerTreatment treatment, float size) {
668706
if (treatment instanceof RoundedCornerTreatment) {
669707
return (float) ((1 - COS_45) * size);
@@ -745,4 +783,12 @@ private boolean isCheckedIconEnd() {
745783
private boolean isCheckedIconBottom() {
746784
return (checkedIconGravity & Gravity.BOTTOM) == Gravity.BOTTOM;
747785
}
786+
787+
@NonNull
788+
private SpringForce createSpringForce(@NonNull Context context) {
789+
return MotionUtils.resolveThemeSpringForce(
790+
context,
791+
R.attr.motionSpringFastSpatial,
792+
R.style.Motion_Material3_Spring_Standard_Fast_Spatial);
793+
}
748794
}

lib/java/com/google/android/material/shape/ShapeAppearance.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,10 @@ public interface ShapeAppearance {
5656
* Returns a {@link ShapeAppearanceModel} for the given state set.
5757
*/
5858
@NonNull ShapeAppearanceModel getShapeForState(@NonNull int[] stateSet);
59+
60+
/**
61+
* Returns a list of the {@link ShapeAppearanceModel} of all states. If this
62+
* ShapeAppearance is stateless, this will be a list of just the default shape.
63+
*/
64+
@NonNull ShapeAppearanceModel[] getShapeAppearanceModels();
5965
}

lib/java/com/google/android/material/shape/ShapeAppearanceModel.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@
4242
public class ShapeAppearanceModel implements ShapeAppearance {
4343
public static final int NUM_CORNERS = 4;
4444

45+
/** Flag representing top left corner of the shape. */
46+
public static final int CORNER_TOP_LEFT = 0x1;
47+
48+
/** Flag representing top right corner of the shape. */
49+
public static final int CORNER_TOP_RIGHT = 0x2;
50+
51+
/** Flag representing bottom left corner of the shape. */
52+
public static final int CORNER_BOTTOM_LEFT = 0x4;
53+
54+
/** Flag representing bottom right corner of the shape. */
55+
public static final int CORNER_BOTTOM_RIGHT = 0x8;
56+
4557
/** Builder to create instances of {@link ShapeAppearanceModel}s. */
4658
public static final class Builder {
4759

@@ -454,6 +466,24 @@ private static float compatCornerTreatmentSize(CornerTreatment treatment) {
454466
return -1;
455467
}
456468

469+
@NonNull
470+
@CanIgnoreReturnValue
471+
public Builder setCornerSizeOverride(int cornerPositionSet, @NonNull CornerSize cornerSize) {
472+
if (containsFlag(cornerPositionSet, CORNER_TOP_LEFT)) {
473+
setTopLeftCornerSize(cornerSize);
474+
}
475+
if (containsFlag(cornerPositionSet, CORNER_TOP_RIGHT)) {
476+
setTopRightCornerSize(cornerSize);
477+
}
478+
if (containsFlag(cornerPositionSet, CORNER_BOTTOM_LEFT)) {
479+
setBottomLeftCornerSize(cornerSize);
480+
}
481+
if (containsFlag(cornerPositionSet, CORNER_BOTTOM_RIGHT)) {
482+
setBottomRightCornerSize(cornerSize);
483+
}
484+
return this;
485+
}
486+
457487
/** Builds an instance of a {@link ShapeAppearanceModel} */
458488
@NonNull
459489
public ShapeAppearanceModel build() {
@@ -870,6 +900,16 @@ public ShapeAppearanceModel getShapeForState(@NonNull int[] stateSet) {
870900
return this;
871901
}
872902

903+
@NonNull
904+
@Override
905+
public ShapeAppearanceModel[] getShapeAppearanceModels() {
906+
return new ShapeAppearanceModel[] { this };
907+
}
908+
909+
static boolean containsFlag(int flagSet, int flag) {
910+
return (flagSet | flag) == flagSet;
911+
}
912+
873913
@NonNull
874914
@Override
875915
public String toString() {

lib/java/com/google/android/material/shape/StateListShapeAppearanceModel.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
import com.google.android.material.R;
2020

2121
import static android.content.res.Resources.ID_NULL;
22+
import static com.google.android.material.shape.ShapeAppearanceModel.CORNER_BOTTOM_LEFT;
23+
import static com.google.android.material.shape.ShapeAppearanceModel.CORNER_BOTTOM_RIGHT;
24+
import static com.google.android.material.shape.ShapeAppearanceModel.CORNER_TOP_LEFT;
25+
import static com.google.android.material.shape.ShapeAppearanceModel.CORNER_TOP_RIGHT;
26+
import static com.google.android.material.shape.ShapeAppearanceModel.containsFlag;
2227

2328
import android.content.Context;
2429
import android.content.res.Resources;
@@ -49,11 +54,6 @@
4954
*/
5055
@RestrictTo(Scope.LIBRARY_GROUP)
5156
public class StateListShapeAppearanceModel implements ShapeAppearance {
52-
public static final int CORNER_TOP_LEFT = 0x1;
53-
public static final int CORNER_TOP_RIGHT = 0x2;
54-
public static final int CORNER_BOTTOM_LEFT = 0x4;
55-
public static final int CORNER_BOTTOM_RIGHT = 0x8;
56-
5757
private static final int INITIAL_CAPACITY = 10;
5858

5959
/** Builder for {@link StateListShapeAppearanceModel}. */
@@ -135,10 +135,6 @@ public Builder setCornerSizeOverride(
135135
return this;
136136
}
137137

138-
private boolean containsFlag(int flagSet, int flag) {
139-
return (flagSet | flag) == flagSet;
140-
}
141-
142138
@NonNull
143139
@CanIgnoreReturnValue
144140
public Builder addStateShapeAppearanceModel(
@@ -345,6 +341,12 @@ public ShapeAppearanceModel getShapeForState(@NonNull int[] stateSet) {
345341
return builder.build();
346342
}
347343

344+
@NonNull
345+
@Override
346+
public ShapeAppearanceModel[] getShapeAppearanceModels() {
347+
return shapeAppearanceModels;
348+
}
349+
348350
private int indexOfStateSet(int[] stateSet) {
349351
final int[][] stateSpecs = this.stateSpecs;
350352
for (int i = 0; i < stateCount; i++) {

0 commit comments

Comments
 (0)