Ekler: Yuvarlatılmış köşeler uygula

Android 12'den (API düzeyi 31) itibaren, cihaz ekranının yuvarlatılmış köşelerinin yarıçapını ve merkez noktasını almak için RoundedCorner ve WindowInsets.getRoundedCorner(int position) özelliklerini kullanabilirsiniz. Bu API'ler, uygulamanızın kullanıcı arayüzü öğelerinin yuvarlatılmış köşeleri olan ekranlarda kısaltılmasını önler. Çerçeve, görünür tüm mikrofon ve kamera göstergelerinin sınırlı dikdörtgenini döndüren getPrivacyIndicatorBounds() API'sini sağlar.

Bu API'lerin, uygulamanızda uygulandığında yuvarlatılmamış ekranlara sahip cihazlar üzerinde hiçbir etkisi olmaz.

Yarıçap ve merkez noktasıyla yuvarlatılmış köşeleri gösteren resim
Şekil 1. Yarıçapı ve merkez noktası olan yuvarlatılmış köşeler.

Bu özelliği uygulamak için WindowInsets.getRoundedCorner(int position) ile uygulama sınırlarına göre RoundedCorner bilgilerini alın. Uygulama tüm ekranı kaplamıyorsa API, yuvarlatılmış köşenin merkez noktasını uygulamanın pencere sınırlarına dayandırarak yuvarlatılmış köşeyi uygular.

Aşağıdaki kod snippet'inde, bir uygulamanın RoundedCorner tarafından sağlanan bilgilere dayanarak görüntüleme payını ayarlayarak kullanıcı arayüzünün kısaltılmasını nasıl önleyebileceği gösterilmektedir. Bu örnekte, sağ üst köşedir.

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

Kırpmaya dikkat edin

Kullanıcı arayüzünüz ekranın tamamını kapsıyorsa köşelerin yuvarlatılması içerik kırpma ile ilgili sorunlara neden olabilir. Örneğin, Şekil 2'de ekranın köşesinde bir simge gösterilmektedir. Düzeni sistem çubuklarının arkasına çizilmiştir:

Köşeleri yuvarlanmış şekilde kırpılan bir simge
Şekil 2. Köşeleri yuvarlanmış bir simge kırpılıyor.

Aşağıdaki örnekte gösterildiği gibi, yuvarlatılmış köşeleri kontrol ederek ve uygulamanızın içeriğini cihazın köşelerinden uzak tutmak için dolgu uygulayarak bunu önleyebilirsiniz:

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

Bu düzen, kullanıcı arayüzünün yuvarlatılmış köşelere genişleyip uzanmadığını belirler ve köşelere dolgu ekler. Şekil 3'te, uygulanan dolguyu daha net bir şekilde göstermek için "Düzen sınırlarını göster" geliştirici seçeneği etkinleştirilmiştir:

Köşeden uzağa taşımak için dolgu uygulanan bir simge.
Şekil 3. Köşeden uzağa taşımak için dolgu uygulanan bir simge.

Bu düzeni belirlemek için bu düzen iki dikdörtgen hesaplar: safeArea, yuvarlak köşelerin yarıçapları içindeki alandır ve layoutBounds, düzenin boyutundan dolgunun çıkarılmasıyla elde edilir. layoutArea öğesi tamamen safeArea içeriyorsa düzenin alt öğeleri kırpılabilir. Bu durumda, düzenin safeArea içinde kalmasını sağlamak için dolgu eklenir.

layoutBounds öğesinin safeArea öğesini tamamen içine alıp almadığını kontrol ederek düzen ekranın kenarlarına genişlemediğinde dolgu eklemekten kaçınmış olursunuz. Şekil 4'te, gezinme çubuğunun arkasına çizilmeyen düzen gösterilmektedir. Bu durumda, düzen gezinme çubuğunun yerleştirildiği alana sığdığından yuvarlatılmış köşeler içinde olacak kadar aşağı genişlemez. Dolgu gerekmez.

Sistemin ve gezinme çubuklarının arkasına çizilmeyen bir düzen.
Şekil 4. Sistemin ve gezinme çubuklarının arkasına çizilmeyen bir düzen.