Wcięcia: zastosuj zaokrąglone narożniki

Od Androida 12 (poziom interfejsu API 31) możesz używać RoundedCorner i WindowInsets.getRoundedCorner(int position), aby określać promień i punkt środkowy zaokrąglonych rogów ekranu urządzenia. Dzięki tym interfejsom API elementy interfejsu aplikacji nie są obcinane na ekranach z zaokrąglonymi rogami. Platforma udostępnia interfejs API getPrivacyIndicatorBounds(), który zwraca ograniczony prostokąt wszystkich widocznych wskaźników mikrofonu i aparatu.

Wdrożone w aplikacji interfejsy API nie mają wpływu na urządzenia z niezaokrąglonymi ekranami.

Obraz przedstawiający zaokrąglone narożniki, promienie i punkt środkowy
Rysunek 1. Zaokrąglone narożniki z promieniami i punktem środkowym.

Aby wdrożyć tę funkcję, uzyskaj informacje RoundedCorner za pomocą parametru WindowInsets.getRoundedCorner(int position) w odniesieniu do granic aplikacji. Jeśli aplikacja nie zajmuje całego ekranu, interfejs API stosuje zaokrąglony róg, opierając jego punkt środkowy na granicach okna aplikacji.

Ten fragment kodu pokazuje, jak uniknąć obcięcia UI aplikacji przez ustawienie marginesu widoku na podstawie informacji z RoundedCorner. W tym przypadku jest to prawy górny zaokrąglony róg.

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);

Uważaj na przycinanie

Jeśli interfejs wypełnia cały ekran, zaokrąglone rogi mogą powodować problemy z przycinaniem treści. Na przykład na rys. 2 w rogu ekranu widać ikonę, a układ układu jest rysowany za pasami systemowymi:

Ikona przycięta przez zaokrąglone narożniki
Rysunek 2. Ikona przycięta przez zaokrąglone rogi.

Aby tego uniknąć, sprawdź zaokrąglone rogi i zastosuj dopełnienie, by treść aplikacji nie znajdowała się w rogach urządzenia, jak pokazano w tym przykładzie:

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);
    }
}

Ten układ określa, czy interfejs obejmuje obszar zaokrąglonych narożników, i dodaje dopełnienie w miejscach, gdzie się znajduje. Na Rysunku 3 opcja programisty „Pokaż granice układu” jest włączona, aby wyraźniej zaprezentować stosowane dopełnienie:

Ikona z dopełnieniem powodującym odsunięcie jej od narożnika.
Rysunek 3. Ikona z dopełnieniem powodującym odsunięcie jej od narożnika.

Aby to ustalić, w tym układzie obliczamy 2 prostokąty: safeArea to pole powierzchni zaokrąglonych rogów, a layoutBounds to rozmiar układu pomniejszony o dopełnienie. Jeśli layoutArea w pełni zawiera element safeArea, elementy podrzędne układu mogą być obcięte. W takim przypadku dodawane jest dopełnienie, aby układ pozostał w elemencie safeArea.

Gdy sprawdzisz, czy layoutBounds w pełni obejmuje element safeArea, unikasz dodawania dopełnienia, gdy układ nie rozciąga się do krawędzi wyświetlacza. Rysunek 4 przedstawia układ, gdy nie jest on rysowany za paskiem nawigacyjnym. W tym przypadku układ nie rozciąga się na tyle daleko w dół, by mieścił się w zaokrąglonych narożnikach, ponieważ mieści się w obszarze zajmowanym przez pasek nawigacyjny. Dopełnienie nie jest wymagane.

Układ, który nie jest rysowany za systemem ani paskami nawigacyjnymi.
Rysunek 4. Układ, który nie jest widoczny za systemem ani paskami nawigacyjnymi.