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

بدءًا من الإصدار 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. تخطيط لا يُرسم خلف النظام وأشرطة التنقل