Add support for percentages to ShapeAppearanceModel

PiperOrigin-RevId: 272946740
(cherry picked from commit a130a0c3e048a6311ff70a5bcb7d50b1809f7251)
This commit is contained in:
cketcham 2019-10-04 16:32:42 -04:00 committed by ldjcmu
parent 5ff7b35e74
commit c831ecc99d
11 changed files with 132 additions and 137 deletions

View File

@ -71,8 +71,7 @@
</style>
<style name="ShapeAppearanceOverlay.MaterialComponents.Chip" parent="">
<!-- Replace with 50% -->
<item name="cornerSize">16dp</item>
<item name="cornerSize">50%</item>
</style>
<!-- Style for Chips that appear in text fields as a span.

View File

@ -134,11 +134,6 @@ abstract class BaseMotionStrategy implements MotionStrategy {
animators.add(spec.getAnimator("height", fab, ExtendedFloatingActionButton.HEIGHT));
}
if (spec.hasPropertyValues("cornerRadius") && fab.isUsingPillCorner()) {
animators
.add(spec.getAnimator("cornerRadius", fab, ExtendedFloatingActionButton.CORNER_RADIUS));
}
AnimatorSet set = new AnimatorSet();
AnimatorSetCompat.playTogether(set, animators);
return set;

View File

@ -26,6 +26,7 @@ import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.RectF;
import androidx.annotation.AnimatorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -48,7 +49,7 @@ import com.google.android.material.animation.MotionSpec;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.internal.DescendantOffsetUtils;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.CornerSize;
import com.google.android.material.shape.ShapeAppearanceModel;
import java.util.List;
@ -89,7 +90,6 @@ public class ExtendedFloatingActionButton extends MaterialButton implements Atta
@NonNull private final Behavior<ExtendedFloatingActionButton> behavior;
private boolean isExtended = true;
private boolean isUsingPillCorner = true;
/**
* Callback to be invoked when the visibility or the state of an ExtendedFloatingActionButton
@ -200,7 +200,17 @@ public class ExtendedFloatingActionButton extends MaterialButton implements Atta
ShapeAppearanceModel shapeAppearanceModel =
ShapeAppearanceModel.builder(
context, attrs, defStyleAttr, DEF_STYLE_RES, ShapeAppearanceModel.PILL)
context,
attrs,
defStyleAttr,
DEF_STYLE_RES,
// TODO(b/121352029): Use ShapeAppearanceModel.PILL once this bug is fixed.
new CornerSize() {
@Override
public float getCornerSize(@NonNull RectF bounds) {
return getAdjustedRadius((int) bounds.height());
}
})
.build();
setShapeAppearanceModel(shapeAppearanceModel);
}
@ -215,45 +225,12 @@ public class ExtendedFloatingActionButton extends MaterialButton implements Atta
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (isUsingPillCorner) {
setShapeAppearanceModel(createPillCornerShapeAppearance());
}
}
@NonNull
@Override
public Behavior<ExtendedFloatingActionButton> getBehavior() {
return behavior;
}
@Override
public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) {
if (shapeAppearanceModel.isUsingPillCorner()) {
isUsingPillCorner = true;
shapeAppearanceModel = createPillCornerShapeAppearance();
}
super.setShapeAppearanceModel(shapeAppearanceModel);
}
@NonNull
private ShapeAppearanceModel createPillCornerShapeAppearance() {
return getShapeAppearanceModel().withCornerRadius(getAdjustedRadius(getMeasuredHeight()));
}
@Override
public void setCornerRadius(int cornerRadius) {
isUsingPillCorner = cornerRadius == ShapeAppearanceModel.PILL;
if (isUsingPillCorner) {
cornerRadius = getAdjustedRadius(getMeasuredHeight());
} else if (cornerRadius < 0) {
cornerRadius = 0;
}
super.setCornerRadius(cornerRadius);
}
/**
* Extends or shrinks the fab depending on the value of {@param extended}.
@ -554,10 +531,6 @@ public class ExtendedFloatingActionButton extends MaterialButton implements Atta
setShrinkMotionSpec(MotionSpec.createFromResource(getContext(), id));
}
boolean isUsingPillCorner() {
return isUsingPillCorner;
}
private void performMotion(
@NonNull final MotionStrategy strategy, @Nullable final OnChangedCallback callback) {
if (strategy.shouldCancel()) {
@ -667,26 +640,6 @@ public class ExtendedFloatingActionButton extends MaterialButton implements Atta
}
};
/**
* A Property wrapper around the <code>cornerRadius</code> functionality handled by the {@link
* ExtendedFloatingActionButton#setCornerRadius(int)} and {@link
* ExtendedFloatingActionButton#getCornerRadius()} methods.
*/
static final Property<View, Float> CORNER_RADIUS =
new Property<View, Float>(Float.class, "cornerRadius") {
@Override
public void set(@NonNull View object, @NonNull Float value) {
ExtendedFloatingActionButton efab = ((ExtendedFloatingActionButton) object);
efab.setShapeAppearanceModel(
efab.getShapeAppearanceModel().withCornerRadius(value.intValue()));
}
@Override
public Float get(@NonNull View object) {
return ((MaterialShapeDrawable) object.getBackground()).getTopRightCornerResolvedSize();
}
};
/**
* Returns an adjusted radius value that corrects any rounding errors.
*

View File

@ -76,7 +76,6 @@ import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.VisibilityAwareImageButton;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shadow.ShadowViewDelegate;
import com.google.android.material.shape.AbsoluteCornerSize;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.shape.Shapeable;
import com.google.android.material.stateful.ExtendableSavedState;
@ -245,7 +244,6 @@ public class FloatingActionButton extends VisibilityAwareImageButton
context, attrs, defStyleAttr, DEF_STYLE_RES, ShapeAppearanceModel.PILL)
.build();
boolean usingDefaultCorner = isUsingDefaultCorner(shapeAppearance);
boolean ensureMinTouchTargetSize = a
.getBoolean(R.styleable.FloatingActionButton_ensureMinTouchTargetSize, false);
@ -256,7 +254,7 @@ public class FloatingActionButton extends VisibilityAwareImageButton
expandableWidgetHelper = new ExpandableWidgetHelper(this);
getImpl().setShapeAppearance(shapeAppearance, usingDefaultCorner);
getImpl().setShapeAppearance(shapeAppearance);
getImpl()
.initializeBackgroundDrawable(backgroundTint, backgroundTintMode, rippleColor, borderWidth);
getImpl().setMinTouchTargetSize(minTouchTargetSize);
@ -522,7 +520,7 @@ public class FloatingActionButton extends VisibilityAwareImageButton
/** Sets the {@link ShapeAppearanceModel} for this {@link FloatingActionButton}. */
@Override
public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearance) {
getImpl().setShapeAppearance(shapeAppearance, isUsingDefaultCorner(shapeAppearance));
getImpl().setShapeAppearance(shapeAppearance);
}
/** Returns the {@link ShapeAppearanceModel} for this {@link FloatingActionButton}. */
@ -691,7 +689,6 @@ public class FloatingActionButton extends VisibilityAwareImageButton
customSize = NO_CUSTOM_SIZE;
if (size != this.size) {
this.size = size;
getImpl().updateSize();
requestLayout();
}
}
@ -752,7 +749,6 @@ public class FloatingActionButton extends VisibilityAwareImageButton
if (size != customSize) {
customSize = size;
getImpl().updateSize();
requestLayout();
}
}
@ -1362,11 +1358,6 @@ public class FloatingActionButton extends VisibilityAwareImageButton
getImpl().removeTransformationCallback(new TransformationCallbackWrapper(listener));
}
private boolean isUsingDefaultCorner(@NonNull ShapeAppearanceModel shapeAppearance) {
return ((AbsoluteCornerSize) shapeAppearance.getTopRightCornerSize()).getCornerSize()
== ShapeAppearanceModel.PILL;
}
class TransformationCallbackWrapper<T extends FloatingActionButton>
implements InternalTransformationCallback {

View File

@ -86,7 +86,6 @@ class FloatingActionButtonImpl {
@Nullable BorderDrawable borderDrawable;
@Nullable Drawable contentBackground;
boolean usingDefaultCorner;
boolean ensureMinTouchTargetSize;
boolean shadowPaddingEnabled = true;
float elevation;
@ -305,14 +304,8 @@ class FloatingActionButtonImpl {
}
}
final void setShapeAppearance(
@NonNull ShapeAppearanceModel shapeAppearance, boolean usingDefaultCorner) {
if (usingDefaultCorner) {
shapeAppearance = shapeAppearance.withCornerRadius(view.getSizeDimension() / 2);
}
final void setShapeAppearance(@NonNull ShapeAppearanceModel shapeAppearance) {
this.shapeAppearance = shapeAppearance;
this.usingDefaultCorner = usingDefaultCorner;
if (shapeDrawable != null) {
shapeDrawable.setShapeAppearanceModel(shapeAppearance);
}
@ -645,16 +638,6 @@ class FloatingActionButtonImpl {
// Ignore pre-v21
}
void updateSize() {
if (!usingDefaultCorner || shapeDrawable == null || shapeAppearance == null) {
// Leave shape appearance as is.
return;
}
setShapeAppearance(
shapeAppearance.withCornerRadius(view.getSizeDimension() / 2f), usingDefaultCorner);
}
final void updatePadding() {
Rect rect = tmpRect;
getPadding(rect);
@ -736,9 +719,6 @@ class FloatingActionButtonImpl {
MaterialShapeDrawable createShapeDrawable() {
ShapeAppearanceModel shapeAppearance = checkNotNull(this.shapeAppearance);
if (usingDefaultCorner) {
shapeAppearance = shapeAppearance.withCornerRadius(view.getSizeDimension() / 2f);
}
return new MaterialShapeDrawable(shapeAppearance);
}

View File

@ -229,9 +229,6 @@ class FloatingActionButtonImplLollipop extends FloatingActionButtonImpl {
@Override
MaterialShapeDrawable createShapeDrawable() {
ShapeAppearanceModel shapeAppearance = checkNotNull(this.shapeAppearance);
if (usingDefaultCorner) {
shapeAppearance = shapeAppearance.withCornerRadius(view.getSizeDimension() / 2f);
}
return new AlwaysStatefulMaterialShapeDrawable(shapeAppearance);
}

View File

@ -51,8 +51,7 @@
</style>
<style name="ShapeAppearanceOverlay.MaterialComponents.FloatingActionButton" parent="">
<!-- use @null to change to 50% programmatically. -->
<item name="cornerSize">@null</item>
<item name="cornerSize">50%</item>
</style>
<style name="Widget.MaterialComponents.ExtendedFloatingActionButton" parent="Widget.MaterialComponents.Button">
@ -82,10 +81,15 @@
<item name="iconTint">@color/mtrl_extended_fab_text_color_selector</item>
<item name="rippleColor">@color/mtrl_extended_fab_ripple_color</item>
<item name="shapeAppearanceOverlay">
@style/ShapeAppearanceOverlay.MaterialComponents.FloatingActionButton
@style/ShapeAppearanceOverlay.MaterialComponents.ExtendedFloatingActionButton
</item>
</style>
<style name="ShapeAppearanceOverlay.MaterialComponents.ExtendedFloatingActionButton" parent="">
<!-- TODO(b/121352029): ExtendedFloatingActionButton adds 1px to the height -->
<item name="cornerSize">@null</item>
</style>
<style name="Widget.MaterialComponents.ExtendedFloatingActionButton.Icon" parent="Widget.MaterialComponents.ExtendedFloatingActionButton">
<item name="android:gravity">start|center_vertical</item>
<item name="android:paddingStart" tools:ignore="NewApi">

View File

@ -60,6 +60,7 @@ import android.util.AttributeSet;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.elevation.ElevationOverlayProvider;
import com.google.android.material.shadow.ShadowRenderer;
import com.google.android.material.shape.ShapeAppearanceModel.CornerSizeUnaryOperator;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -1096,7 +1097,20 @@ public class MaterialShapeDrawable extends Drawable implements TintAwareDrawable
private void calculateStrokePath() {
// Adjust corner radius in order to draw the stroke so that the corners of the background are
// drawn on top of the edges.
strokeShapeAppearance = getShapeAppearanceModel().withAdjustedCorners(-getStrokeInsetLength());
strokeShapeAppearance =
getShapeAppearanceModel()
.withTransformedCornerSizes(
new CornerSizeUnaryOperator() {
@NonNull
@Override
public CornerSize apply(@NonNull CornerSize cornerSize) {
// Don't adjust for relative corners they will change by themselves when the
// bounds change.
return cornerSize instanceof RelativeCornerSize
? cornerSize
: new AdjustedCornerSize(-getStrokeInsetLength(), cornerSize);
}
});
pathProvider.calculatePath(
strokeShapeAppearance,

View File

@ -0,0 +1,62 @@
/*
* Copyright 2019 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 com.google.android.material.shape;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import java.util.Arrays;
/**
* A {@link CornerSize} that takes a percent and computes the size used based on the height of the
* shape.
*/
public final class RelativeCornerSize implements CornerSize {
private final float percent;
public RelativeCornerSize(float percent) {
this.percent = percent;
}
/** Returns the relative percent used for this {@link CornerSize} */
public float getRelativePercent() {
return percent;
}
@Override
public float getCornerSize(@NonNull RectF bounds) {
return percent * bounds.height();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RelativeCornerSize)) {
return false;
}
RelativeCornerSize that = (RelativeCornerSize) o;
return percent == that.percent;
}
@Override
public int hashCode() {
Object[] hashedFields = {percent};
return Arrays.hashCode(hashedFields);
}
}

View File

@ -413,15 +413,6 @@ public class ShapeAppearanceModel {
return this;
}
/** Adjusts all the corners by the offset. */
@NonNull
public Builder adjustCorners(@Dimension float offset) {
return setTopLeftCornerSize(new AdjustedCornerSize(offset, topLeftCornerSize))
.setTopRightCornerSize(new AdjustedCornerSize(offset, topRightCornerSize))
.setBottomRightCornerSize(new AdjustedCornerSize(offset, bottomRightCornerSize))
.setBottomLeftCornerSize(new AdjustedCornerSize(offset, bottomLeftCornerSize));
}
/** Pulls the corner size from specific CornerTreatments for backwards compatibility */
private static float compatCornerTreatmentSize(CornerTreatment treatment) {
if (treatment instanceof RoundedCornerTreatment) {
@ -563,13 +554,15 @@ public class ShapeAppearanceModel {
// floats.
return new AbsoluteCornerSize(
TypedValue.complexToDimensionPixelSize(value.data, a.getResources().getDisplayMetrics()));
} else if (value.type == TypedValue.TYPE_FRACTION) {
return new RelativeCornerSize(value.getFraction(1.0f, 1.0f));
} else {
return defaultValue;
}
}
// Constant corner radius value to indicate that shape should use 50% height corner radii
public static final Integer PILL = -1;
public static final CornerSize PILL = new RelativeCornerSize(0.5f);
CornerTreatment topLeftCorner;
CornerTreatment topRightCorner;
@ -739,18 +732,6 @@ public class ShapeAppearanceModel {
return bottomEdge;
}
/** Checks if all four corners of this ShapeAppearanceModel are of size {@link #PILL}. */
public boolean isUsingPillCorner() {
return isCornerPill(getTopRightCornerSize())
&& isCornerPill(getTopLeftCornerSize())
&& isCornerPill(getBottomLeftCornerSize())
&& isCornerPill(getBottomRightCornerSize());
}
private boolean isCornerPill(CornerSize cornerSize) {
return ((AbsoluteCornerSize) cornerSize).getCornerSize() == PILL;
}
/** Returns a builder with the edges and corners from this {@link ShapeAppearanceModel} */
@NonNull
public Builder toBuilder() {
@ -772,12 +753,31 @@ public class ShapeAppearanceModel {
}
/**
* Returns a copy of this {@link ShapeAppearanceModel} with the same edges and corners, but with
* the corner radius for all corners offset by an adjustment.
* A UnaryOperator that takes and returns a CornerSize.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public interface CornerSizeUnaryOperator {
@NonNull
CornerSize apply(@NonNull CornerSize cornerSize);
}
/**
* Returns a copy of this {@link ShapeAppearanceModel} with the same edges and corners, but with
* the corner radius for all corners converted by a {@link CornerSizeUnaryOperator}.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@NonNull
public ShapeAppearanceModel withAdjustedCorners(float offset) {
return toBuilder().adjustCorners(offset).build();
public ShapeAppearanceModel withTransformedCornerSizes(@NonNull CornerSizeUnaryOperator op) {
return toBuilder()
.setTopLeftCornerSize(op.apply(getTopLeftCornerSize()))
.setTopRightCornerSize(op.apply(getTopLeftCornerSize()))
.setBottomLeftCornerSize(op.apply(getBottomLeftCornerSize()))
.setBottomRightCornerSize(op.apply(getBottomLeftCornerSize()))
.build();
}
/**

View File

@ -26,15 +26,15 @@
<declare-styleable name="ShapeAppearance">
<!-- Corner size to be used in the ShapeAppearance. All corners default to this value -->
<attr name="cornerSize" format="dimension"/>
<attr name="cornerSize" format="dimension|fraction"/>
<!-- Top left corner size to be used in the ShapeAppearance. -->
<attr name="cornerSizeTopLeft" format="dimension"/>
<attr name="cornerSizeTopLeft" format="dimension|fraction"/>
<!-- Top right corner size to be used in the ShapeAppearance. -->
<attr name="cornerSizeTopRight" format="dimension"/>
<attr name="cornerSizeTopRight" format="dimension|fraction"/>
<!-- Bottom right corner size to be used in the ShapeAppearance. -->
<attr name="cornerSizeBottomRight" format="dimension"/>
<attr name="cornerSizeBottomRight" format="dimension|fraction"/>
<!-- Bottom left corner size to be used in the ShapeAppearance. -->
<attr name="cornerSizeBottomLeft" format="dimension"/>
<attr name="cornerSizeBottomLeft" format="dimension|fraction"/>
<!-- Corner family to be used in the ShapeAppearance. All corners default to this value -->
<attr name="cornerFamily" format="enum">