العناصر الداخلية: تطبيق زوايا مستديرة

بدءًا من نظام التشغيل Android 12 (المستوى 31 لواجهة برمجة التطبيقات)، يمكنك استخدام RoundedCorner وWindowInsets.getRoundedCorner(int position) لتحديد النطاق الجغرافي والنقطة المركزية للزوايا المستديرة لشاشة الجهاز. وتعمل واجهات برمجة التطبيقات هذه على منع اقتطاع عناصر واجهة المستخدم في التطبيق على الشاشات ذات الزوايا المستديرة. يوفر إطار العمل واجهة برمجة التطبيقات getPrivacyIndicatorBounds()، التي تعرض المستطيل المحاط بأي مؤشرات مرئية للميكروفون والكاميرا.

وعند تنفيذها في تطبيقك، لا يكون لواجهات برمجة التطبيقات هذه أي تأثير في الأجهزة ذات الشاشات غير المستديرة.

صورة تعرض زوايا مستديرة مع نصف قطر قطري ونقطة مركزية
الشكل 1. زوايا مستديرة مع أنصاف نصف قطر ونقطة مركزية

لتطبيق هذه الميزة، يجب الحصول على معلومات RoundedCorner باستخدام السمة WindowInsets.getRoundedCorner(int position) وفقًا لحدود التطبيق. إذا كان التطبيق لا يشغل الشاشة بأكملها، ستطبِّق واجهة برمجة التطبيقات الزاوية المستديرة من خلال تثبيت النقطة المركزية للزاوية المستديرة على حدود نافذة التطبيق.

يعرض مقتطف الرمز التالي كيف يمكن لتطبيق تجنُّب اقتطاع واجهة المستخدم الخاصة به من خلال ضبط هامش للعرض بناءً على المعلومات الواردة من 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. هو تنسيق لا يتم رسمه خلف النظام وأشرطة التنقل.