درون‌نشست‌ها: اعمال گوشه‌های گرد

از Android 12 (سطح میانای برنامه‌سازی کاربردی ۳۱) می‌توانید از RoundedCorner و WindowInsets.getRoundedCorner(int position) برای دریافت شعاع و نقطه مرکزی گوشه‌های گرد صفحه‌نمایش دستگاه استفاده کنید. این API-ها از بریده شدن عناصر میانای کاربر برنامه‌تان در صفحه‌نمایش‌های دارای گوشه‌های گرد جلوگیری می‌کنند. این چارچوب getPrivacyIndicatorBounds() API را ارائه می‌دهد که مستطیل محدودشده هر نشانگر میکروفون و دوربین قابل‌مشاهده را برمی‌گرداند.

وقتی این میاناهای برنامه‌سازی کاربردی در برنامه‌تان پیاده‌سازی شوند، هیچ تأثیری بر دستگاه‌های دارای صفحه‌نمایش غیرگرد ندارند.

تصویری که گوشه‌های گرد با شعاع و نقطه مرکزی را نشان می‌دهد
شکل ۱. گوشه‌های گرد با شعاع و نقطه مرکزی.

برای پیاده‌سازی این ویژگی، اطلاعات RoundedCorner را بااستفاده از WindowInsets.getRoundedCorner(int position) نسبت‌به مرزهای برنامه دریافت کنید. اگر برنامه کل صفحه را اشغال نکند، API گوشه گرد را با قرار دادن نقطه مرکزی گوشه گرد در مرزهای پنجره برنامه اعمال می‌کند.

تکه‌کد زیر نشان می‌دهد که چگونه یک برنامه می‌تواند با تنظیم حاشیه نما براساس اطلاعات RoundedCorner، از بریده شدن واسط کاربر خود جلوگیری کند. در این مورد، گوشه گرد بالا سمت راست است.

کاتلین

// 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

جاوا

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

مراقب برش باشید

اگر واسط کاربر شما کل نمایشگر را پر می‌کند، گوشه‌های گرد می‌تواند باعث ایجاد مشکل در برش محتوا شود. برای مثال، شکل ۲ نمادی را در گوشه نمایشگر نشان می‌دهد که چیدمان آن در پشت نوارهای سیستم رسم شده است:

نمادی که گوشه‌های گرد آن را بریده است
شکل ۲. نمادی که با گوشه‌های گرد بریده شده است.

برای جلوگیری از این مشکل، می‌توانید گوشه‌های گرد را بررسی کنید و با اعمال حاشیه داخلی، محتوای برنامه‌تان را از گوشه‌های دستگاه دور نگه دارید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

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

جاوا

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

این چیدمان تعیین می‌کند که آیا واسط کاربر به ناحیه گوشه‌های گرد گسترش می‌یابد یا نه و درصورت گسترش، حاشیه داخلی اضافه می‌کند. در شکل ۳، گزینه توسعه‌دهنده «نمایش محدوده‌های چیدمان» فعال شده است تا فاصله‌گذاری اعمال‌شده را واضح‌تر نشان دهد:

نمادی با لایه داخلی اعمال‌شده برای دور کردن آن از گوشه.
شکل ۳. نمادی با لایه داخلی اعمال‌شده برای دور کردن آن از گوشه.

برای انجام این تعیین، این چیدمان دو مستطیل را محاسبه می‌کند: safeArea ناحیه داخل شعاع گوشه‌های گرد است، و layoutBounds اندازه چیدمان منهای هرگونه حاشیه داخلی است. اگر layoutArea کاملاً حاوی safeArea باشد، ممکن است فرزندان چیدمان بریده شوند. اگر این‌طور باشد، حاشیه داخلی اضافه می‌شود تا مطمئن شویم چیدمان داخل safeArea باقی می‌ماند.

با بررسی اینکه آیا layoutBounds کاملاً safeArea را دربرمی‌گیرد یا نه، از افزودن حاشیه داخلی زمانی که چیدمان به لبه‌های نمایشگر گسترش نمی‌یابد اجتناب می‌کنید. شکل ۴ چیدمان را وقتی که در پشت نوار پیمایش رسم نشده است نشان می‌دهد. در این حالت، چیدمان به‌اندازه کافی به پایین گسترش نمی‌یابد تا در گوشه‌های گرد قرار گیرد، زیرا آن‌ها در ناحیه اشغال‌شده توسط نوار پیمایش قرار می‌گیرند. به لایه‌گذاری نیازی نیست.

چیدمانی که در پشت نوارهای سیستم و پیمایش رسم نمی‌شود.
شکل ۴. چیدمانی که در پشت نوارهای سیستم و پیمایش رسم نمی‌شود.