blob: a74f2f8a5e34e2b267b8847e35e3f11f6bfd3ffd [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui
import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.Drawable
import android.hardware.graphics.common.AlphaInterpretation
import android.hardware.graphics.common.DisplayDecorationSupport
import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM
import android.view.DisplayCutout.BOUNDS_POSITION_LEFT
import android.view.DisplayCutout.BOUNDS_POSITION_LENGTH
import android.view.DisplayCutout.BOUNDS_POSITION_TOP
import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT
import android.view.RoundedCorner
import android.view.RoundedCorners
import android.view.Surface
import androidx.annotation.VisibleForTesting
import kotlin.math.ceil
import kotlin.math.floor
/**
* When the HWC of the device supports Composition.DISPLAY_DECORATION, we use this layer to draw
* screen decorations.
*/
class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDecorationSupport)
: DisplayCutoutBaseView(context) {
val colorMode: Int
private val useInvertedAlphaColor: Boolean
private val color: Int
private val bgColor: Int
private val cornerFilter: ColorFilter
private val cornerBgFilter: ColorFilter
private val clearPaint: Paint
@JvmField val transparentRect: Rect = Rect()
private val debugTransparentRegionPaint: Paint?
private val tempRect: Rect = Rect()
private var hasTopRoundedCorner = false
private var hasBottomRoundedCorner = false
private var roundedCornerTopSize = 0
private var roundedCornerBottomSize = 0
private var roundedCornerDrawableTop: Drawable? = null
private var roundedCornerDrawableBottom: Drawable? = null
init {
if (displayDecorationSupport.format != PixelFormat.R_8) {
throw IllegalArgumentException("Attempting to use unsupported mode " +
"${PixelFormat.formatToString(displayDecorationSupport.format)}")
}
if (DEBUG_COLOR) {
color = Color.GREEN
bgColor = Color.TRANSPARENT
colorMode = ActivityInfo.COLOR_MODE_DEFAULT
useInvertedAlphaColor = false
debugTransparentRegionPaint = Paint().apply {
color = 0x2f00ff00 // semi-transparent green
style = Paint.Style.FILL
}
} else {
colorMode = ActivityInfo.COLOR_MODE_A8
useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation ==
AlphaInterpretation.COVERAGE
if (useInvertedAlphaColor) {
color = Color.TRANSPARENT
bgColor = Color.BLACK
} else {
color = Color.BLACK
bgColor = Color.TRANSPARENT
}
debugTransparentRegionPaint = null
}
cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT)
clearPaint = Paint()
clearPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
parent.requestTransparentRegion(this)
if (!DEBUG_COLOR) {
viewRootImpl.setDisplayDecoration(true)
}
if (useInvertedAlphaColor) {
paint.set(clearPaint)
} else {
paint.color = color
paint.style = Paint.Style.FILL
}
}
override fun onUpdate() {
parent.requestTransparentRegion(this)
}
override fun onDraw(canvas: Canvas) {
// If updating onDraw, also update gatherTransparentRegion
if (useInvertedAlphaColor) {
canvas.drawColor(bgColor)
}
// We may clear the color(if useInvertedAlphaColor is true) of the rounded corner rects
// before drawing rounded corners. If the cutout happens to be inside one of these rects, it
// will be cleared, so we have to draw rounded corners before cutout.
drawRoundedCorners(canvas)
// Cutouts are drawn in DisplayCutoutBaseView.onDraw()
super.onDraw(canvas)
debugTransparentRegionPaint?.let {
canvas.drawRect(transparentRect, it)
}
}
override fun gatherTransparentRegion(region: Region?): Boolean {
region?.let {
calculateTransparentRect()
if (DEBUG_COLOR) {
// Since we're going to draw a rectangle where the layer would
// normally be transparent, treat the transparent region as
// empty. We still want this method to be called, though, so
// that it calculates the transparent rect at the right time
// to match !DEBUG_COLOR.
region.setEmpty()
} else {
region.op(transparentRect, Region.Op.INTERSECT)
}
}
// Always return false - views underneath this should always be visible.
return false
}
/**
* The transparent rect is calculated by subtracting the regions of cutouts, cutout protect and
* rounded corners from the region with fullscreen display size.
*/
@VisibleForTesting
fun calculateTransparentRect() {
transparentRect.set(0, 0, width, height)
// Remove cutout region.
removeCutoutFromTransparentRegion()
// Remove cutout protection region.
removeCutoutProtectionFromTransparentRegion()
// Remove rounded corner region.
removeRoundedCornersFromTransparentRegion()
}
private fun removeCutoutFromTransparentRegion() {
displayInfo.displayCutout?.let {
cutout ->
if (!cutout.boundingRectLeft.isEmpty) {
transparentRect.left =
cutout.boundingRectLeft.right.coerceAtLeast(transparentRect.left)
}
if (!cutout.boundingRectTop.isEmpty) {
transparentRect.top =
cutout.boundingRectTop.bottom.coerceAtLeast(transparentRect.top)
}
if (!cutout.boundingRectRight.isEmpty) {
transparentRect.right =
cutout.boundingRectRight.left.coerceAtMost(transparentRect.right)
}
if (!cutout.boundingRectBottom.isEmpty) {
transparentRect.bottom =
cutout.boundingRectBottom.top.coerceAtMost(transparentRect.bottom)
}
}
}
private fun removeCutoutProtectionFromTransparentRegion() {
if (protectionRect.isEmpty) {
return
}
val centerX = protectionRect.centerX()
val centerY = protectionRect.centerY()
val scaledDistanceX = (centerX - protectionRect.left) * cameraProtectionProgress
val scaledDistanceY = (centerY - protectionRect.top) * cameraProtectionProgress
tempRect.set(
floor(centerX - scaledDistanceX).toInt(),
floor(centerY - scaledDistanceY).toInt(),
ceil(centerX + scaledDistanceX).toInt(),
ceil(centerY + scaledDistanceY).toInt()
)
// Find out which edge the protectionRect belongs and remove that edge from the transparent
// region.
val leftDistance = tempRect.left
val topDistance = tempRect.top
val rightDistance = width - tempRect.right
val bottomDistance = height - tempRect.bottom
val minDistance = minOf(leftDistance, topDistance, rightDistance, bottomDistance)
when (minDistance) {
leftDistance -> {
transparentRect.left = tempRect.right.coerceAtLeast(transparentRect.left)
}
topDistance -> {
transparentRect.top = tempRect.bottom.coerceAtLeast(transparentRect.top)
}
rightDistance -> {
transparentRect.right = tempRect.left.coerceAtMost(transparentRect.right)
}
bottomDistance -> {
transparentRect.bottom = tempRect.top.coerceAtMost(transparentRect.bottom)
}
}
}
private fun removeRoundedCornersFromTransparentRegion() {
var hasTopOrBottomCutouts = false
var hasLeftOrRightCutouts = false
displayInfo.displayCutout?.let {
cutout ->
hasTopOrBottomCutouts = !cutout.boundingRectTop.isEmpty ||
!cutout.boundingRectBottom.isEmpty
hasLeftOrRightCutouts = !cutout.boundingRectLeft.isEmpty ||
!cutout.boundingRectRight.isEmpty
}
// The goal is to remove the rounded corner areas as small as possible so that we can have a
// larger transparent region. Therefore, we should always remove from the short edge sides
// if possible.
val isShortEdgeTopBottom = width < height
if (isShortEdgeTopBottom) {
// Short edges on top & bottom.
if (!hasTopOrBottomCutouts && hasLeftOrRightCutouts) {
// If there are cutouts only on left or right edges, remove left and right sides
// for rounded corners.
transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT)
.coerceAtLeast(transparentRect.left)
transparentRect.right =
(width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT))
.coerceAtMost(transparentRect.right)
} else {
// If there are cutouts on top or bottom edges or no cutout at all, remove top
// and bottom sides for rounded corners.
transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP)
.coerceAtLeast(transparentRect.top)
transparentRect.bottom =
(height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM))
.coerceAtMost(transparentRect.bottom)
}
} else {
// Short edges on left & right.
if (hasTopOrBottomCutouts && !hasLeftOrRightCutouts) {
// If there are cutouts only on top or bottom edges, remove top and bottom sides
// for rounded corners.
transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP)
.coerceAtLeast(transparentRect.top)
transparentRect.bottom =
(height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM))
.coerceAtMost(transparentRect.bottom)
} else {
// If there are cutouts on left or right edges or no cutout at all, remove left
// and right sides for rounded corners.
transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT)
.coerceAtLeast(transparentRect.left)
transparentRect.right =
(width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT))
.coerceAtMost(transparentRect.right)
}
}
}
private fun getRoundedCornerSizeByPosition(position: Int): Int {
val delta = displayRotation - Surface.ROTATION_0
return when ((position + delta) % BOUNDS_POSITION_LENGTH) {
BOUNDS_POSITION_LEFT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize)
BOUNDS_POSITION_TOP -> roundedCornerTopSize
BOUNDS_POSITION_RIGHT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize)
BOUNDS_POSITION_BOTTOM -> roundedCornerBottomSize
else -> throw IllegalArgumentException("Incorrect position: $position")
}
}
private fun drawRoundedCorners(canvas: Canvas) {
if (!hasTopRoundedCorner && !hasBottomRoundedCorner) {
return
}
var degree: Int
for (i in RoundedCorner.POSITION_TOP_LEFT
until RoundedCorners.ROUNDED_CORNER_POSITION_LENGTH) {
canvas.save()
degree = getRoundedCornerRotationDegree(90 * i)
canvas.rotate(degree.toFloat())
canvas.translate(
getRoundedCornerTranslationX(degree).toFloat(),
getRoundedCornerTranslationY(degree).toFloat())
if (hasTopRoundedCorner && (i == RoundedCorner.POSITION_TOP_LEFT ||
i == RoundedCorner.POSITION_TOP_RIGHT)) {
drawRoundedCorner(canvas, roundedCornerDrawableTop, roundedCornerTopSize)
} else if (hasBottomRoundedCorner && (i == RoundedCorner.POSITION_BOTTOM_LEFT ||
i == RoundedCorner.POSITION_BOTTOM_RIGHT)) {
drawRoundedCorner(canvas, roundedCornerDrawableBottom, roundedCornerBottomSize)
}
canvas.restore()
}
}
private fun drawRoundedCorner(canvas: Canvas, drawable: Drawable?, size: Int) {
if (useInvertedAlphaColor) {
canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), clearPaint)
drawable?.colorFilter = cornerBgFilter
} else {
drawable?.colorFilter = cornerFilter
}
drawable?.draw(canvas)
// Clear color filter when we are done with drawing.
drawable?.clearColorFilter()
}
private fun getRoundedCornerRotationDegree(defaultDegree: Int): Int {
return (defaultDegree - 90 * displayRotation + 360) % 360
}
private fun getRoundedCornerTranslationX(degree: Int): Int {
return when (degree) {
0, 90 -> 0
180 -> -width
270 -> -height
else -> throw IllegalArgumentException("Incorrect degree: $degree")
}
}
private fun getRoundedCornerTranslationY(degree: Int): Int {
return when (degree) {
0, 270 -> 0
90 -> -width
180 -> -height
else -> throw IllegalArgumentException("Incorrect degree: $degree")
}
}
/**
* Update the rounded corner drawables.
*/
fun updateRoundedCornerDrawable(top: Drawable, bottom: Drawable) {
roundedCornerDrawableTop = top
roundedCornerDrawableBottom = bottom
updateRoundedCornerDrawableBounds()
invalidate()
}
/**
* Update the rounded corner existence and size.
*/
fun updateRoundedCornerExistenceAndSize(
hasTop: Boolean,
hasBottom: Boolean,
topSize: Int,
bottomSize: Int
) {
if (hasTopRoundedCorner == hasTop &&
hasBottomRoundedCorner == hasBottom &&
roundedCornerTopSize == topSize &&
roundedCornerBottomSize == bottomSize) {
return
}
hasTopRoundedCorner = hasTop
hasBottomRoundedCorner = hasBottom
roundedCornerTopSize = topSize
roundedCornerBottomSize = bottomSize
updateRoundedCornerDrawableBounds()
// Use requestLayout() to trigger transparent region recalculated
requestLayout()
}
private fun updateRoundedCornerDrawableBounds() {
if (roundedCornerDrawableTop != null) {
roundedCornerDrawableTop?.setBounds(0, 0, roundedCornerTopSize,
roundedCornerTopSize)
}
if (roundedCornerDrawableBottom != null) {
roundedCornerDrawableBottom?.setBounds(0, 0, roundedCornerBottomSize,
roundedCornerBottomSize)
}
invalidate()
}
companion object {
private val DEBUG_COLOR = ScreenDecorations.DEBUG_COLOR
}
}