计算两种形状的差异两种形式的新形状(布尔代数)
Calculate difference of two shapes two form new shape (boolean algebra)
查看提供的示例图片。我想要实现的是从卡片视图背景(或与此相关的任何其他图像)中剪切出随机形式(在示例中为半圆)。
所以最后我得到了带有镂空形状的背景图像。最终效果应该是图三,操作按钮周围有透明感。
备注:
- 高程和阴影不会丢失
- 我不想使用静态背景,所以它必须是真正的剪切图,而不是像示例中那样带有背景颜色的叠加层
- UI 必须动态调整和调整大小 - 切口也是如此
如何实现?
这个半圆切口可以使用带有自定义 ShapeAppearanceModel
的 MaterialShapeDrawable
来实现。此切口与 BottomAppBar
中使用的行为类似,该半圆进入其顶部边缘。 BottomAppBar
在其 ShapeAppearanceModel
中仅将 BottomAppBarTopEdgeTreatment 用于顶部。您需要的是类似的行为,而不是顶部边缘侧位于底部边缘侧。
在下面的示例中,我使用了 BottomAppBarTopEdgeTreatment 中使用的相同代码,唯一的区别是您可以使用构造函数中使用的 cutoutEndSpacingDp
将半圆移动到特定的 x 位置CutoutCircleEdgeTreatment
.
1.Get 最新的 Material 设计库 ('com.google.android.material:material:1.4.0'
) 并将其添加到您的 grandle depedencies。
2.Create EdgeTreatment
的子类,名称为 CutoutCircleEdgeTreatment
,如下所示:
public class CutoutCircleEdgeTreatment extends EdgeTreatment {
private static final int ARC_QUARTER = 90;
private static final int ARC_HALF = 180;
private static final int ANGLE_UP = 270;
private static final int ANGLE_LEFT = 180;
private static final float ROUNDED_CORNER_FAB_OFFSET = 1.75f;
private float roundedCornerRadius;
private float fabMargin;
private float fabDiameter;
private float cradleVerticalOffset;
private float horizontalOffset;
private float fabCornerSize = -1f;
private float cutoutEndSpacing = 0;
public CutoutCircleEdgeTreatment(Resources res, float fabDiameterDp, float cutoutEndSpacingDp){
fabMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
roundedCornerRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
cradleVerticalOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
fabDiameter = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fabDiameterDp, res.getDisplayMetrics());
cutoutEndSpacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, cutoutEndSpacingDp, res.getDisplayMetrics());
}
@Override
public void getEdgePath(float length, float center, float interpolation, @NonNull ShapePath shapePath) {
horizontalOffset = -center + ((fabMargin * 2 + fabDiameter)/2) + cutoutEndSpacing;
//below is the same code used in BottomAppBarTopEdgeTreatment getEdgePath()
if (fabDiameter == 0) {
// There is no cutout to draw.
shapePath.lineTo(length, 0);
return;
}
float cradleDiameter = fabMargin * 2 + fabDiameter;
float cradleRadius = cradleDiameter / 2f;
float roundedCornerOffset = interpolation * roundedCornerRadius;
float middle = center + horizontalOffset;
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
float verticalOffset =
interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius;
float verticalOffsetRatio = verticalOffset / cradleRadius;
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0);
return; // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
float cornerSize = fabCornerSize * interpolation;
boolean useCircleCutout = fabCornerSize == -1 || java.lang.Math.abs(fabCornerSize * 2f - fabDiameter) < .1f;
float arcOffset = 0;
if (!useCircleCutout) {
verticalOffset = 0;
arcOffset = ROUNDED_CORNER_FAB_OFFSET;
}
float distanceBetweenCenters = cradleRadius + roundedCornerOffset;
float distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters;
float distanceY = verticalOffset + roundedCornerOffset;
float distanceX = (float) Math.sqrt(distanceBetweenCentersSquared - (distanceY * distanceY));
// Calculate the x position of the rounded corner circles.
float leftRoundedCornerCircleX = middle - distanceX;
float rightRoundedCornerCircleX = middle + distanceX;
// Calculate the arc between the center of the two circles.
float cornerRadiusArcLength = (float) Math.toDegrees(Math.atan(distanceX / distanceY));
float cutoutArcOffset = (ARC_QUARTER - cornerRadiusArcLength) + arcOffset;
// Draw the starting line up to the left rounded corner.
shapePath.lineTo(/* x= */ leftRoundedCornerCircleX, /* y= */ 0);
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ leftRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ leftRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP,
/* sweepAngle= */ cornerRadiusArcLength);
if (useCircleCutout) {
// Draw the cutout circle.
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -cradleRadius - verticalOffset,
/* right= */ middle + cradleRadius,
/* bottom= */ cradleRadius - verticalOffset,
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ cutoutArcOffset * 2 - ARC_HALF);
} else {
float cutoutDiameter = fabMargin + cornerSize * 2f;
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle - cradleRadius + cutoutDiameter,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ (cutoutArcOffset * 2 - ARC_HALF) / 2f);
shapePath.lineTo(middle + cradleRadius - (cornerSize + fabMargin / 2f), /* y= */
(cornerSize + fabMargin));
shapePath.addArc(
/* left= */ middle + cradleRadius - (cornerSize * 2f + fabMargin),
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle + cradleRadius,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ 90,
/* sweepAngle= */ -90 + cutoutArcOffset);
}
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ rightRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ rightRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP - cornerRadiusArcLength,
/* sweepAngle= */ cornerRadiusArcLength);
// Draw the ending line after the right rounded corner.
shapePath.lineTo(/* x= */ length, /* y= */ 0);
}
}
从上面的构造函数中,您可以传递 fabDiameterDp
(以 dps 为单位的圆直径)和 cutoutEndSpacingDp
(以 dps 为单位)将半圆移动到特定 x 所需的结束间距从头到尾的位置。当然,您可以根据自己的需要进行任何其他修改。
3.You 可以像下面这样使用上面的 CutoutCircleEdgeTreatment
:
对于 Material 组件,如 MaterialCardView 等,它们已经有一个 MaterialShapeDrawable 作为背景:
MaterialCardView materialCardView = findViewById(R.id.materialCardView);
materialCardView.setShapeAppearanceModel(materialCardView.getShapeAppearanceModel()
.toBuilder()
.setBottomEdge(new CutoutCircleEdgeTreatment(getResources(), 80, 20))
.build());
Xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#81afdc"
android:padding="20dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/materialCardView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp"
android:layout_marginEnd="35dp"
app:backgroundTint="@android:color/holo_orange_dark"
app:elevation="3dp"
app:fabCustomSize="70dp"
app:layout_constraintEnd_toEndOf="@+id/materialCardView"
app:layout_constraintTop_toBottomOf="@+id/materialCardView"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
对于非Material 组件,如 ConstraintLayout、RelativeLayout 等,它们没有 MaterialShapeDrawable:
RelativeLayout relativeLayout = findViewById(R.id.relativeLayout);
ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel()
.toBuilder()
.setBottomEdge(new CutoutCircleEdgeTreatment(getResources(), 80, 20))
.build();
MaterialShapeDrawable shapeDrawable = new MaterialShapeDrawable(shapeAppearanceModel);
shapeDrawable.setFillColor(ContextCompat.getColorStateList(this, android.R.color.white));
ViewCompat.setBackground(relativeLayout, shapeDrawable);
Xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#81afdc"
android:padding="20dp">
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp"
android:layout_marginEnd="25dp"
app:backgroundTint="@android:color/holo_orange_dark"
app:elevation="3dp"
app:fabCustomSize="70dp"
app:layout_constraintEnd_toEndOf="@+id/relativeLayout"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
结果:
查看提供的示例图片。我想要实现的是从卡片视图背景(或与此相关的任何其他图像)中剪切出随机形式(在示例中为半圆)。
所以最后我得到了带有镂空形状的背景图像。最终效果应该是图三,操作按钮周围有透明感。
备注:
- 高程和阴影不会丢失
- 我不想使用静态背景,所以它必须是真正的剪切图,而不是像示例中那样带有背景颜色的叠加层
- UI 必须动态调整和调整大小 - 切口也是如此
如何实现?
这个半圆切口可以使用带有自定义 ShapeAppearanceModel
的 MaterialShapeDrawable
来实现。此切口与 BottomAppBar
中使用的行为类似,该半圆进入其顶部边缘。 BottomAppBar
在其 ShapeAppearanceModel
中仅将 BottomAppBarTopEdgeTreatment 用于顶部。您需要的是类似的行为,而不是顶部边缘侧位于底部边缘侧。
在下面的示例中,我使用了 BottomAppBarTopEdgeTreatment 中使用的相同代码,唯一的区别是您可以使用构造函数中使用的 cutoutEndSpacingDp
将半圆移动到特定的 x 位置CutoutCircleEdgeTreatment
.
1.Get 最新的 Material 设计库 ('com.google.android.material:material:1.4.0'
) 并将其添加到您的 grandle depedencies。
2.Create EdgeTreatment
的子类,名称为 CutoutCircleEdgeTreatment
,如下所示:
public class CutoutCircleEdgeTreatment extends EdgeTreatment {
private static final int ARC_QUARTER = 90;
private static final int ARC_HALF = 180;
private static final int ANGLE_UP = 270;
private static final int ANGLE_LEFT = 180;
private static final float ROUNDED_CORNER_FAB_OFFSET = 1.75f;
private float roundedCornerRadius;
private float fabMargin;
private float fabDiameter;
private float cradleVerticalOffset;
private float horizontalOffset;
private float fabCornerSize = -1f;
private float cutoutEndSpacing = 0;
public CutoutCircleEdgeTreatment(Resources res, float fabDiameterDp, float cutoutEndSpacingDp){
fabMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
roundedCornerRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
cradleVerticalOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0, res.getDisplayMetrics());
fabDiameter = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, fabDiameterDp, res.getDisplayMetrics());
cutoutEndSpacing = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, cutoutEndSpacingDp, res.getDisplayMetrics());
}
@Override
public void getEdgePath(float length, float center, float interpolation, @NonNull ShapePath shapePath) {
horizontalOffset = -center + ((fabMargin * 2 + fabDiameter)/2) + cutoutEndSpacing;
//below is the same code used in BottomAppBarTopEdgeTreatment getEdgePath()
if (fabDiameter == 0) {
// There is no cutout to draw.
shapePath.lineTo(length, 0);
return;
}
float cradleDiameter = fabMargin * 2 + fabDiameter;
float cradleRadius = cradleDiameter / 2f;
float roundedCornerOffset = interpolation * roundedCornerRadius;
float middle = center + horizontalOffset;
// The center offset of the cutout tweens between the vertical offset when attached, and the
// cradleRadius as it becomes detached.
float verticalOffset =
interpolation * cradleVerticalOffset + (1 - interpolation) * cradleRadius;
float verticalOffsetRatio = verticalOffset / cradleRadius;
if (verticalOffsetRatio >= 1.0f) {
// Vertical offset is so high that there's no curve to draw in the edge, i.e., the fab is
// actually above the edge so just draw a straight line.
shapePath.lineTo(length, 0);
return; // Early exit.
}
// Calculate the path of the cutout by calculating the location of two adjacent circles. One
// circle is for the rounded corner. If the rounded corner circle radius is 0 the corner will
// not be rounded. The other circle is the cutout.
// Calculate the X distance between the center of the two adjacent circles using pythagorean
// theorem.
float cornerSize = fabCornerSize * interpolation;
boolean useCircleCutout = fabCornerSize == -1 || java.lang.Math.abs(fabCornerSize * 2f - fabDiameter) < .1f;
float arcOffset = 0;
if (!useCircleCutout) {
verticalOffset = 0;
arcOffset = ROUNDED_CORNER_FAB_OFFSET;
}
float distanceBetweenCenters = cradleRadius + roundedCornerOffset;
float distanceBetweenCentersSquared = distanceBetweenCenters * distanceBetweenCenters;
float distanceY = verticalOffset + roundedCornerOffset;
float distanceX = (float) Math.sqrt(distanceBetweenCentersSquared - (distanceY * distanceY));
// Calculate the x position of the rounded corner circles.
float leftRoundedCornerCircleX = middle - distanceX;
float rightRoundedCornerCircleX = middle + distanceX;
// Calculate the arc between the center of the two circles.
float cornerRadiusArcLength = (float) Math.toDegrees(Math.atan(distanceX / distanceY));
float cutoutArcOffset = (ARC_QUARTER - cornerRadiusArcLength) + arcOffset;
// Draw the starting line up to the left rounded corner.
shapePath.lineTo(/* x= */ leftRoundedCornerCircleX, /* y= */ 0);
// Draw the arc for the left rounded corner circle. The bounding box is the area around the
// circle's center which is at `(leftRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ leftRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ leftRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP,
/* sweepAngle= */ cornerRadiusArcLength);
if (useCircleCutout) {
// Draw the cutout circle.
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -cradleRadius - verticalOffset,
/* right= */ middle + cradleRadius,
/* bottom= */ cradleRadius - verticalOffset,
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ cutoutArcOffset * 2 - ARC_HALF);
} else {
float cutoutDiameter = fabMargin + cornerSize * 2f;
shapePath.addArc(
/* left= */ middle - cradleRadius,
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle - cradleRadius + cutoutDiameter,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ ANGLE_LEFT - cutoutArcOffset,
/* sweepAngle= */ (cutoutArcOffset * 2 - ARC_HALF) / 2f);
shapePath.lineTo(middle + cradleRadius - (cornerSize + fabMargin / 2f), /* y= */
(cornerSize + fabMargin));
shapePath.addArc(
/* left= */ middle + cradleRadius - (cornerSize * 2f + fabMargin),
/* top= */ -(cornerSize + fabMargin),
/* right= */ middle + cradleRadius,
/* bottom= */ (fabMargin + cornerSize),
/* startAngle= */ 90,
/* sweepAngle= */ -90 + cutoutArcOffset);
}
// Draw an arc for the right rounded corner circle. The bounding box is the area around the
// circle's center which is at `(rightRoundedCornerCircleX, roundedCornerOffset)`.
shapePath.addArc(
/* left= */ rightRoundedCornerCircleX - roundedCornerOffset,
/* top= */ 0,
/* right= */ rightRoundedCornerCircleX + roundedCornerOffset,
/* bottom= */ roundedCornerOffset * 2,
/* startAngle= */ ANGLE_UP - cornerRadiusArcLength,
/* sweepAngle= */ cornerRadiusArcLength);
// Draw the ending line after the right rounded corner.
shapePath.lineTo(/* x= */ length, /* y= */ 0);
}
}
从上面的构造函数中,您可以传递 fabDiameterDp
(以 dps 为单位的圆直径)和 cutoutEndSpacingDp
(以 dps 为单位)将半圆移动到特定 x 所需的结束间距从头到尾的位置。当然,您可以根据自己的需要进行任何其他修改。
3.You 可以像下面这样使用上面的 CutoutCircleEdgeTreatment
:
对于 Material 组件,如 MaterialCardView 等,它们已经有一个 MaterialShapeDrawable 作为背景:
MaterialCardView materialCardView = findViewById(R.id.materialCardView);
materialCardView.setShapeAppearanceModel(materialCardView.getShapeAppearanceModel()
.toBuilder()
.setBottomEdge(new CutoutCircleEdgeTreatment(getResources(), 80, 20))
.build());
Xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#81afdc"
android:padding="20dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/materialCardView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:cardBackgroundColor="@android:color/white"
app:cardCornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp"
android:layout_marginEnd="35dp"
app:backgroundTint="@android:color/holo_orange_dark"
app:elevation="3dp"
app:fabCustomSize="70dp"
app:layout_constraintEnd_toEndOf="@+id/materialCardView"
app:layout_constraintTop_toBottomOf="@+id/materialCardView"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
对于非Material 组件,如 ConstraintLayout、RelativeLayout 等,它们没有 MaterialShapeDrawable:
RelativeLayout relativeLayout = findViewById(R.id.relativeLayout);
ShapeAppearanceModel shapeAppearanceModel = new ShapeAppearanceModel()
.toBuilder()
.setBottomEdge(new CutoutCircleEdgeTreatment(getResources(), 80, 20))
.build();
MaterialShapeDrawable shapeDrawable = new MaterialShapeDrawable(shapeAppearanceModel);
shapeDrawable.setFillColor(ContextCompat.getColorStateList(this, android.R.color.white));
ViewCompat.setBackground(relativeLayout, shapeDrawable);
Xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#81afdc"
android:padding="20dp">
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-35dp"
android:layout_marginEnd="25dp"
app:backgroundTint="@android:color/holo_orange_dark"
app:elevation="3dp"
app:fabCustomSize="70dp"
app:layout_constraintEnd_toEndOf="@+id/relativeLayout"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
结果: