قسمت های داخلی: گوشه های گرد را اعمال کنید

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

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

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

برای پیاده سازی این ویژگی، اطلاعات 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);

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

اگر UI شما کل صفحه نمایش را پر کند، گوشه های گرد می توانند باعث ایجاد مشکل در برش محتوا شوند. به عنوان مثال، شکل 2 نمادی را در گوشه نمایشگر نشان می دهد که طرح آن در پشت نوارهای سیستم کشیده شده است:

نمادی که توسط گوشه های گرد بریده می شود
شکل 2. نمادی که توسط گوشه های گرد بریده شده است.

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

کاتلین

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

این طرح‌بندی تعیین می‌کند که آیا UI به ناحیه گوشه‌های گرد گسترش می‌یابد یا نه و در جایی که انجام می‌شود، بالشتک اضافه می‌کند. در شکل 3 گزینه توسعه دهنده "Show layout bounds" فعال شده است تا padding را با وضوح بیشتری نشان دهد:

نمادی با بالشتک اعمال شده تا آن را از گوشه دور کند.
شکل 3. یک نماد با بالشتک اعمال شده برای دور کردن آن از گوشه.

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

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

طرحی که پشت سیستم و نوارهای ناوبری ترسیم نمی کند.
شکل 4. طرحی که پشت سیستم و نوارهای ناوبری ترسیم نمی کند.