Phần lồng ghép: Áp dụng góc tròn

Kể từ Android 12 (API cấp 31), bạn có thể dùng RoundedCornerWindowInsets.getRoundedCorner(int position) để lấy bán kính và tâm điểm cho các góc bo tròn của màn hình thiết bị. Các API này giúp các thành phần trên giao diện người dùng của ứng dụng không bị cắt bớt trên màn hình có góc bo tròn. Khung này cung cấp getPrivacyIndicatorBounds() API. API này trả về hình chữ nhật viền bao quanh cho mọi chỉ báo micrô và máy ảnh hiển thị.

Khi được triển khai trong ứng dụng, các API này không ảnh hưởng đến thiết bị có màn hình không cong.

Hình ảnh cho thấy một góc bo tròn có bán kính và điểm giữa
Hình 1. Các góc bo tròn có bán kính và một điểm chính giữa.

Để triển khai tính năng này, hãy lấy thông tin RoundedCorner bằng cách sử dụng WindowInsets.getRoundedCorner(int position) tương ứng với các ranh giới của ứng dụng. Nếu ứng dụng không chiếm toàn bộ màn hình, thì API sẽ áp dụng góc tròn bằng cách dựa vào điểm giữa của góc bo tròn trên ranh giới cửa sổ của ứng dụng.

Đoạn mã sau đây cho thấy cách một ứng dụng có thể tránh việc giao diện người dùng bị cắt bớt bằng cách đặt lề của chế độ xem dựa trên thông tin từ RoundedCorner. Trong trường hợp này, đó là góc bo tròn trên cùng bên phải.

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

Hãy cẩn thận khi cắt đoạn

Nếu giao diện người dùng lấp đầy toàn bộ màn hình, thì các góc bo tròn có thể gây ra vấn đề về việc cắt nội dung. Ví dụ: Hình 2 cho thấy một biểu tượng ở góc màn hình với bố cục được vẽ phía sau các thanh hệ thống:

Biểu tượng được cắt theo các góc tròn
Hình 2. Một biểu tượng được cắt bớt theo các góc tròn.

Bạn có thể tránh điều này bằng cách kiểm tra các góc bo tròn và áp dụng khoảng đệm để đảm bảo nội dung ứng dụng không có góc của thiết bị, như trong ví dụ sau:

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

Bố cục này xác định xem giao diện người dùng có mở rộng đến khu vực bo tròn các góc hay không và thêm khoảng đệm ở vị trí có thể. Hình 3 đã bật tuỳ chọn "Hiển thị giới hạn bố cục" dành cho nhà phát triển để hiển thị khoảng đệm đang được áp dụng rõ ràng hơn:

Biểu tượng có khoảng đệm được áp dụng để di chuyển biểu tượng ra khỏi góc.
Hình 3. Một biểu tượng có khoảng đệm được áp dụng để di chuyển biểu tượng ra khỏi góc.

Để xác định điều này, bố cục này sẽ tính toán 2 hình chữ nhật: safeArea là diện tích trong bán kính của các góc tròn và layoutBounds là kích thước của bố cục trừ đi bất kỳ khoảng đệm nào. Nếu layoutArea chứa đầy đủ safeArea, thì các phần tử con của bố cục có thể bị cắt bớt. Nếu trong trường hợp này, khoảng đệm sẽ được thêm vào để đảm bảo bố cục vẫn ở bên trong safeArea.

Bằng cách kiểm tra xem layoutBounds có chứa đầy đủ safeArea hay không, bạn tránh thêm khoảng đệm khi bố cục không mở rộng đến các cạnh của màn hình. Hình 4 cho thấy bố cục khi bố cục này không được vẽ phía sau thanh điều hướng. Trong trường hợp này, bố cục không kéo dài xuống đủ sâu để nằm trong các góc tròn, vì chúng vừa với khu vực mà thanh điều hướng chiếm. Không yêu cầu khoảng đệm.

Bố cục không vẽ phía sau hệ thống và thanh điều hướng.
Hình 4. Bố cục không vẽ phía sau hệ thống và thanh điều hướng.