ตั้งแต่ Android 12 (API ระดับ 31) เป็นต้นไป คุณสามารถใช้
RoundedCorner
และ
WindowInsets.getRoundedCorner(int
position)
เพื่อรับ
รัศมีและจุดศูนย์กลางสำหรับมุมโค้งมนของหน้าจออุปกรณ์ API เหล่านี้
ป้องกันไม่ให้องค์ประกอบ UI ของแอปถูกตัดบนหน้าจอที่มีความโค้งมน
มุม เฟรมเวิร์กนี้จะนำเสนอ
getPrivacyIndicatorBounds()
API ซึ่งแสดงสี่เหลี่ยมผืนผ้าที่มีกรอบของ ไมโครโฟนและกล้องใดก็ตามที่มองเห็นได้
สัญญาณบอกสถานะ
เมื่อใช้งานในแอป API เหล่านี้จะไม่มีผลกับอุปกรณ์ที่มี หน้าจอไม่กลมมน

หากต้องการใช้ฟีเจอร์นี้ โปรดรับข้อมูล RoundedCorner
โดยใช้
WindowInsets.getRoundedCorner(int position)
เมื่อเทียบกับขอบเขตของ
แอปพลิเคชัน หากแอปไม่ครอบคลุมทั้งหน้าจอ API จะใช้
มุมโค้งมนโดยใช้จุดศูนย์กลางของมุมโค้งมนบนหน้าต่าง
ขอบเขตของแอป
ข้อมูลโค้ดต่อไปนี้แสดงให้เห็นวิธีหลีกเลี่ยงไม่ให้แอปถูกตัด UI
ตั้งค่าขอบของมุมมองโดยอิงตามข้อมูลจาก 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 แสดงไอคอนที่มุมของหน้าจอพร้อมกับ เลย์เอาต์ที่วาดอยู่หลังแถบระบบ

คุณหลีกเลี่ยงปัญหานี้ได้โดยตรวจหามุมโค้งมนและใช้ระยะห่างจากขอบเพื่อคงไว้ เนื้อหาของแอปออกจากมุมของอุปกรณ์ดังที่แสดงในภาพต่อไปนี้ ตัวอย่าง:
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 มี "แสดงขอบเขตของเลย์เอาต์" นักพัฒนาแอป เปิดใช้ตัวเลือกเพื่อแสดงระยะห่างจากขอบอย่างชัดเจนมากขึ้น

ในการคำนวณ เลย์เอาต์นี้จะคำนวณสี่เหลี่ยมผืนผ้า 2 รูป: safeArea
เท่ากับ
พื้นที่ภายในรัศมีของมุมกลม และ layoutBounds
คือขนาด
ของเลย์เอาต์ลบด้วยระยะห่างจากขอบด้วย หาก layoutArea
มี safeArea
ครบถ้วนแล้ว
องค์ประกอบย่อยของเลย์เอาต์อาจถูกตัดออก ในกรณีนี้ Padding จะ
เพิ่มเพื่อให้มั่นใจว่าเลย์เอาต์จะยังอยู่ใน safeArea
การตรวจสอบว่า layoutBounds
ครอบคลุม safeArea
อย่างสมบูรณ์หรือไม่ จะเป็นการหลีกเลี่ยงการเพิ่ม
ระยะห่างจากขอบเมื่อเลย์เอาต์ไม่ขยายไปถึงขอบจอแสดงผล รูปที่ 4
แสดงเลย์เอาต์เมื่อไม่ได้วาดไว้หลังแถบนำทาง ในกรณีนี้
เลย์เอาต์ไม่ขยายออกไกลพอที่จะอยู่ในมุมโค้งมน เช่น
ให้พอดีกับพื้นที่ที่แถบนำทาง ไม่ต้องใช้ระยะห่างจากขอบ
