Einsätze: abgerundete Ecken verwenden

Ab Android 12 (API-Level 31) kannst du RoundedCorner und WindowInsets.getRoundedCorner(int position) verwenden, um den Radius und den Mittelpunkt für abgerundete Ecken des Gerätebildschirms abzurufen. Diese APIs verhindern, dass die UI-Elemente Ihrer App auf Bildschirmen mit abgerundeten Ecken abgeschnitten werden. Das Framework stellt die getPrivacyIndicatorBounds() API bereit, die das begrenzte Rechteck aller sichtbaren Mikrofon- und Kameraanzeigen zurückgibt.

Wenn diese APIs in Ihrer App implementiert sind, haben sie keine Auswirkungen auf Geräte mit nicht abgerundeten Bildschirmen.

Bild mit abgerundeten Ecken mit Radien und einem Mittelpunkt
Abbildung 1: Abgerundete Ecken mit Radien und einem Mittelpunkt

Wenn Sie dieses Feature implementieren möchten, rufen Sie die RoundedCorner-Informationen mithilfe von WindowInsets.getRoundedCorner(int position) relativ zu den Grenzen der Anwendung ab. Wenn die App nicht den gesamten Bildschirm einnimmt, wendet die API die abgerundete Ecke an, indem der Mittelpunkt der abgerundeten Ecke auf den Fenstergrenzen der App stützt.

Das folgende Code-Snippet zeigt, wie eine App verhindern kann, dass die Benutzeroberfläche abgeschnitten wird, indem ein Rand der Ansicht basierend auf den Informationen aus RoundedCorner festgelegt wird. In diesem Fall ist es die obere rechte abgerundete Ecke.

Kotlin

// Get the top-right rounded corner from WindowInsets.
val insets = rootWindowInsets
val topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT) ?: return

// Get the location of the close button in window coordinates.
val location = IntArray(2)
closeButton!!.getLocationInWindow(location)
val buttonRightInWindow = location[0] + closeButton.width
val buttonTopInWindow = location[1]

// Find the point on the quarter circle with a 45-degree angle.
val offset = (topRight.radius * Math.sin(Math.toRadians(45.0))).toInt()
val topBoundary = topRight.center.y - offset
val rightBoundary = topRight.center.x + offset

// Check whether the close button exceeds the boundary.
if (buttonRightInWindow < rightBoundary << buttonTopInWindow > topBoundary) {
   return
}

// Set the margin to avoid truncating.
val parentLocation = IntArray(2)
getLocationInWindow(parentLocation)
val lp = closeButton.layoutParams as FrameLayout.LayoutParams
lp.rightMargin = Math.max(buttonRightInWindow - rightBoundary, 0)
lp.topMargin = Math.max(topBoundary - buttonTopInWindow, 0)
closeButton.layoutParams = lp

Java

// Get the top-right rounded corner from WindowInsets.
final WindowInsets insets = getRootWindowInsets();
final RoundedCorner topRight = insets.getRoundedCorner(POSITION_TOP_RIGHT);
if (topRight == null) {
   return;
}

// Get the location of the close button in window coordinates.
int [] location = new int[2];
closeButton.getLocationInWindow(location);
final int buttonRightInWindow = location[0] + closeButton.getWidth();
final int buttonTopInWindow = location[1];

// Find the point on the quarter circle with a 45-degree angle.
final int offset = (int) (topRight.getRadius() * Math.sin(Math.toRadians(45)));
final int topBoundary = topRight.getCenter().y - offset;
final int rightBoundary = topRight.getCenter().x + offset;

// Check whether the close button exceeds the boundary.
if (buttonRightInWindow < rightBoundary << buttonTopInWindow > topBoundary) {
   return;
}

// Set the margin to avoid truncating.
int [] parentLocation = new int[2];
getLocationInWindow(parentLocation);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) closeButton.getLayoutParams();
lp.rightMargin = Math.max(buttonRightInWindow - rightBoundary, 0);
lp.topMargin = Math.max(topBoundary - buttonTopInWindow, 0);
closeButton.setLayoutParams(lp);

Vorsicht beim Abschneiden

Wenn die Benutzeroberfläche den gesamten Bildschirm ausfüllt, können abgerundete Ecken zu Problemen mit dem Abschneiden von Inhalten führen. Abbildung 2 zeigt beispielsweise ein Symbol in der Ecke des Bildschirms, wobei das Layout hinter den Systemleisten gezeichnet wird:

Ein Symbol mit abgerundeten Ecken
Abbildung 2: Ein Symbol mit abgerundeten Ecken.

Du kannst das vermeiden, indem du auf abgerundete Ecken achtest und einen Innenabstand verwendest, damit der Inhalt deiner App nicht in die Ecken deines Geräts gelenkt wird, wie im folgenden Beispiel gezeigt:

Kotlin

class InsetsLayout(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        val insets = rootWindowInsets

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && insets != null) {
            applyRoundedCornerPadding(insets)
        }
        super.onLayout(changed, left, top, right, bottom)

    }

    @RequiresApi(Build.VERSION_CODES.S)
    private fun applyRoundedCornerPadding(insets: WindowInsets) {
        val topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)
        val topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)
        val bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)
        val bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)

        val leftRadius = max(topLeft?.radius ?: 0, bottomLeft?.radius ?: 0)
        val topRadius = max(topLeft?.radius ?: 0, topRight?.radius ?: 0)
        val rightRadius = max(topRight?.radius ?: 0, bottomRight?.radius ?: 0)
        val bottomRadius = max(bottomLeft?.radius ?: 0, bottomRight?.radius ?: 0)

        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val windowBounds = windowManager.currentWindowMetrics.bounds
        val safeArea = Rect(
            windowBounds.left + leftRadius,
            windowBounds.top + topRadius,
            windowBounds.right - rightRadius,
            windowBounds.bottom - bottomRadius
        )

        val location = intArrayOf(0, 0)
        getLocationInWindow(location)

        val leftMargin = location[0] - windowBounds.left
        val topMargin = location[1] - windowBounds.top
        val rightMargin = windowBounds.right - right - location[0]
        val bottomMargin = windowBounds.bottom - bottom - location[1]

        val layoutBounds = Rect(
            location[0] + paddingLeft,
            location[1] + paddingTop,
            location[0] + width - paddingRight,
            location[1] + height - paddingBottom
        )

        if (layoutBounds != safeArea && layoutBounds.contains(safeArea)) {
            setPadding(
                calculatePadding(leftRadius, leftMargin, paddingLeft),
                calculatePadding(topRadius, topMargin, paddingTop),
                calculatePadding(rightRadius, rightMargin, paddingRight),
                calculatePadding(bottomRadius, bottomMargin, paddingBottom)
            )
        }
    }

    private fun calculatePadding(radius1: Int?, radius2: Int?, margin: Int, padding: Int): Int =
        (max(radius1 ?: 0, radius2 ?: 0) - margin - padding).coerceAtLeast(0)
}

Java

public class InsetsLayout extends FrameLayout {
    public InsetsLayout(@NonNull Context context) {
        super(context);
    }

    public InsetsLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        WindowInsets insets = getRootWindowInsets();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && insets != null) {
            applyRoundedCornerPadding(insets);
        }
        super.onLayout(changed, left, top, right, bottom);
    }

    @RequiresApi(Build.VERSION_CODES.S)
    private void applyRoundedCornerPadding(WindowInsets insets) {
        RoundedCorner topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT);
        RoundedCorner topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT);
        RoundedCorner bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
        RoundedCorner bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
        int radiusTopLeft = 0;
        int radiusTopRight = 0;
        int radiusBottomLeft = 0;
        int radiusBottomRight = 0;
        if (topLeft != null) radiusTopLeft = topLeft.getRadius();
        if (topRight != null) radiusTopRight = topRight.getRadius();
        if (bottomLeft != null) radiusBottomLeft = bottomLeft.getRadius();
        if (bottomRight != null) radiusBottomRight = bottomRight.getRadius();

        int leftRadius = Math.max(radiusTopLeft, radiusBottomLeft);
        int topRadius = Math.max(radiusTopLeft, radiusTopRight);
        int rightRadius = Math.max(radiusTopRight, radiusBottomRight);
        int bottomRadius = Math.max(radiusBottomLeft, radiusBottomRight);

        WindowManager windowManager =
                (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        Rect windowBounds = windowManager.getCurrentWindowMetrics().getBounds();
        Rect safeArea = new Rect(
                windowBounds.left + leftRadius,
                windowBounds.top + topRadius,
                windowBounds.right - rightRadius,
                windowBounds.bottom - bottomRadius
        );
        int[] location = {0, 0};
        getLocationInWindow(location);

        int leftMargin = location[0] - windowBounds.left;
        int topMargin = location[1] - windowBounds.top;
        int rightMargin = windowBounds.right - getRight() - location[0];
        int bottomMargin = windowBounds.bottom - getBottom() - location[1];

        Rect layoutBounds = new Rect(
                location[0] + getPaddingLeft(),
                location[1] + getPaddingTop(),
                location[0] + getWidth() - getPaddingRight(),
                location[1] + getHeight() - getPaddingBottom()
        );

        if (!layoutBounds.equals(safeArea) && layoutBounds.contains(safeArea)) {
            setPadding(
                    calculatePadding(radiusTopLeft, radiusBottomLeft,
                                         leftMargin, getPaddingLeft()),
                    calculatePadding(radiusTopLeft, radiusTopRight,
                                         topMargin, getPaddingTop()),
                    calculatePadding(radiusTopRight, radiusBottomRight,
                                         rightMargin, getPaddingRight()),
                    calculatePadding(radiusBottomLeft, radiusBottomRight,
                                         bottomMargin, getPaddingBottom())
            );
        }
    }

    private int calculatePadding(int radius1, int radius2, int margin, int padding) {
        return Math.max(Math.max(radius1, radius2) - margin - padding, 0);
    }
}

Mit diesem Layout wird festgelegt, ob sich die UI bis zum Bereich der abgerundeten Ecken ausdehnt, und an dieser Stelle wird ein Innenrand hinzugefügt. In Abbildung 3 ist die Entwickleroption „Layoutgrenzen anzeigen“ aktiviert, um den angewendeten Abstand besser darzustellen:

Ein Symbol mit einem Innenrand, mit dem es von der Ecke wegbewegt wird.
Abbildung 3: Ein Symbol mit einem Innenrand, mit dem es von der Ecke wegbewegt wird.

Zu diesem Zweck werden bei diesem Layout zwei Rechtecke berechnet: safeArea ist der Bereich innerhalb der Radien der abgerundeten Ecken und layoutBounds ist die Größe des Layouts abzüglich der Abstände. Wenn layoutArea vollständig safeArea enthält, werden die untergeordneten Elemente des Layouts möglicherweise abgeschnitten. In diesem Fall wird ein Innenrand hinzugefügt, um sicherzustellen, dass das Layout innerhalb von safeArea bleibt.

Wenn Sie prüfen, ob layoutBounds safeArea vollständig umschließt, vermeiden Sie das Hinzufügen von Innenabständen, wenn das Layout nicht die Ränder des Displays ausdehnt. In Abbildung 4 ist das Layout zu sehen, wenn es sich nicht hinter der Navigationsleiste befindet. In diesem Fall dehnt sich das Layout nicht weit genug nach unten aus, um innerhalb der abgerundeten Ecken zu liegen, da sie in den Bereich passen, der von der Navigationsleiste eingenommen wird. Es ist kein Abstand erforderlich.

Ein Layout, das sich nicht hinter die System- und Navigationsleisten zieht.
Abbildung 4: Ein Layout, das nicht hinter die System- und Navigationsleisten rückt.