חלקים פנימיים: מחילים פינות מעוגלות

החל מ-Android 12 (רמת API 31), אפשר להשתמש RoundedCorner ו- WindowInsets.getRoundedCorner(int position) כדי לקבל את הרדיוס ואת נקודת המרכז לפינות המעוגלות של מסך המכשיר. ממשקי ה-API האלה למנוע חיתוך של רכיבי ממשק המשתמש של האפליקציה במסכים עם עיגול פינות. המסגרת מספקת getPrivacyIndicatorBounds() API, שמחזיר את המלבן התחום של כל מיקרופון ומצלמה גלויים אינדיקטורים.

כאשר ממשקי ה-API האלה מוטמעים באפליקציה שלך, הם לא משפיעים על מכשירים עם במסכים שאינם מעוגלים.

תמונה של פינות מעוגלות עם רדיוס ונקודת מרכז
איור 1. פינות מעוגלות עם רדיוסים ומרכז נקודה.

כדי להטמיע את התכונה הזו, צריך לקבל את הפרטים של RoundedCorner באמצעות WindowInsets.getRoundedCorner(int position) ביחס לגבולות של תרגום מכונה. אם האפליקציה לא מוצגת במסך מלא, ה-API מחיל את פינה מעוגלת על ידי ביסוס נקודת המרכז של הפינה המעוגלת על החלון את גבולות האפליקציה.

קטע הקוד הבא מראה איך אפליקציה יכולה למנוע חיתוך של ממשק המשתמש שלה על ידי הגדרה של שולי התצוגה על סמך המידע מ-RoundedCorner. כאן מצוינת, כי זו הפינה הימנית העליונה המעוגלת.

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

צריך להיזהר מחיתוך

אם ממשק המשתמש ממלא את כל המסך, פינות מעוגלות עלולות לגרום לבעיות בתוכן. באמצעות חיתוך. לדוגמה, איור 2 מציג סמל בפינת המסך עם הפריסה שמשורטטת מאחורי פסי המערכת:

סמל שנחתך על ידי פינות מעוגלות
איור 2. סמל שנחתך באמצעות עיגול ופינות.

כדי למנוע מצב כזה, מומלץ לבדוק אם יש פינות מעוגלות ולהשתמש במרווח פנימי כדי לשמור של תוכן האפליקציה מחוץ לפינות המכשיר, כפי שמוצג דוגמה:

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

הפריסה הזו קובעת אם ממשק המשתמש משתרע על פני האזור של הפינות המעוגלות ומוסיפה מרווח פנימי במקום שבו היא עושה זאת. איור 3 כולל את 'הצגת גבולות הפריסה' מפתח מופעלת כדי להציג את המרווח הפנימי מיושם בצורה ברורה יותר:

סמל עם מרווח פנימי כדי להרחיק אותו מהפינה.
איור 3. סמל עם מרווח פנימי כדי להרחיק אותו מהפינה.

כדי לקבוע זאת, הפריסה הזו מחשבת שני מלבנים: safeArea הוא השטח שבתוך הרדיוסים של הפינות העגולות, ו-layoutBounds הוא הגודל של הפריסה ללא מרווח פנימי. אם layoutArea מכיל safeArea במלואו, הצאצאים של הפריסה עשויים להיחתך. במקרה כזה, המרווח הפנימי הוא כדי להבטיח שהפריסה תישאר בתוך safeArea.

בדיקה אם layoutBounds כולל את כל safeArea, וכך מונעת הוספה של המרווח הפנימי כשהפריסה לא מתרחבת אל שולי המסך. איור 4 מציגה את הפריסה כשלא משרטטת אותה מאחורי סרגל הניווט. במקרה הזה, הפריסה לא נרחבת מספיק למטה כדי להימצא בתוך הפינות המעוגלות, הן מתאימות לאזור שסרגל הניווט מאכלס. אין צורך במרווח פנימי.

פריסה שלא משורטטת מאחורי המערכת וסרגלי הניווט.
איור 4. פריסה שלא משורטטת מאחורי המערכת וסרגלי הניווט.