【问题标题】:Calculate difference of two shapes two form new shape (boolean algebra)计算两个形状的差异两个形成新形状(布尔代数)
【发布时间】:2021-12-13 03:50:57
【问题描述】:

查看提供的示例图片。我想要实现的是从卡片视图背景(或任何其他图像)中剪切出随机形式(在示例中为半圆)。

这样最后我就有了带有切口形状的背景图像。最终结果将是图片三,其中操作按钮周围是透明的。

注意事项:

  • 高程和阴影不会丢失
  • 我不想使用静态背景,所以它必须是真正的抠图,而不是示例中的背景颜色叠加
  • UI 必须动态调整和调整大小 - 切口也是如此

如何做到这一点?

【问题讨论】:

    标签: android imageview drawable


    【解决方案1】:

    可以使用MaterialShapeDrawable 和自定义ShapeAppearanceModel 来实现这个半圆形切口。这个切口与BottomAppBar 中使用的行为类似,它的顶部边缘有这个半圆。 BottomAppBar 在其ShapeAppearanceModel 中使用BottomAppBarTopEdgeTreatment 仅用于顶部。您需要的是类似的行为,但不是顶部边缘一侧位于底部边缘一侧。

    在下面的示例中,我使用了与BottomAppBarTopEdgeTreatment 相同的代码,唯一的区别是您可以使用CutoutCircleEdgeTreatment 的构造函数中使用的cutoutEndSpacingDp 将半圆移动到特定的x 位置。

    1.获取最新的 Material Design Library ('com.google.android.material:material:1.4.0') 并将其添加到您的大依赖中。

    2.创建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.您可以使用上面的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>
    

    对于没有 MaterialShapeDrawable 的非材质组件,例如 ConstraintLayout、RelativeLayout 等:

    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>
    

    结果:

    【讨论】:

    • 感谢您的详细回答。我也没想到这会是一个如此复杂的任务。我期望通过 XML 实现一些混合模式。我认为框架必须在未来考虑这一点。但是你很好用
    猜你喜欢
    • 2018-03-30
    • 1970-01-01
    • 2021-10-21
    • 1970-01-01
    • 2015-09-14
    • 1970-01-01
    • 2019-08-27
    • 1970-01-01
    • 2016-10-29
    相关资源
    最近更新 更多