Loading libs/WindowManager/Shell/res/layout/letterbox_shell_rounded_corners.xml 0 → 100644 +49 −0 Original line number Diff line number Diff line <!-- ~ Copyright 2025 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. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main" android:background="@android:color/transparent" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerTopLeft" android:layout_gravity="left|top" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerTopRight" android:layout_gravity="right|top" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerBottomLeft" android:layout_gravity="left|bottom" android:layout_width="20dp" android:layout_height="20dp" /> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerBottomRight" android:layout_gravity="right|bottom" android:layout_width="20dp" android:layout_height="20dp" /> </FrameLayout> No newline at end of file libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/roundedcorners/LetterboxRoundedCornersFactory.kt 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.compatui.letterbox.roundedcorners import android.graphics.Color import android.graphics.drawable.Drawable import com.android.wm.shell.compatui.letterbox.roundedcorners.RoundedCornersDrawable.FlipType.FLIP_HORIZONTAL import com.android.wm.shell.compatui.letterbox.roundedcorners.RoundedCornersDrawable.FlipType.FLIP_VERTICAL import com.android.wm.shell.compatui.letterbox.roundedcorners.RoundedCornersFactory.Position import com.android.wm.shell.dagger.WMSingleton import javax.inject.Inject /** * [RoundedCornersFactory] implementation returning rounded corners [Drawable]s using * SVG format. */ @WMSingleton class LetterboxRoundedCornersFactory @Inject constructor( ) : RoundedCornersFactory<RoundedCornersDrawable> { override fun getRoundedCornerDrawable( color: Color, position: Position, radius: Float ): RoundedCornersDrawable { val corners = RoundedCornersDrawable(color, radius) return when (position) { Position.TOP_LEFT -> corners Position.TOP_RIGHT -> corners.flip(FLIP_HORIZONTAL) Position.BOTTOM_LEFT -> corners.flip(FLIP_VERTICAL) Position.BOTTOM_RIGHT -> corners.flip(FLIP_HORIZONTAL) .flip(FLIP_VERTICAL) } } } No newline at end of file libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/roundedcorners/RoundedCornersDrawable.kt 0 → 100644 +212 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.compatui.letterbox.roundedcorners import android.animation.Animator import android.animation.ValueAnimator import android.annotation.SuppressLint import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Matrix import android.graphics.Paint import android.graphics.Path import android.graphics.PixelFormat import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable import com.android.wm.shell.common.ShellExecutor /** * Rounded corner [Drawable] implementation */ class RoundedCornersDrawable( private var cornerColor: Color, private var radius: Float = 0f ) : Drawable() { enum class FlipType { FLIP_VERTICAL, FLIP_HORIZONTAL } companion object { @JvmStatic private val ANIMATION_DURATION = 350L // To make the animation visible we add a small delay @JvmStatic private val ANIMATION_DELAY = 200L } private val currentBounds = RectF() private var verticalFlipped = false private var horizontalFlipped = false private var currentRadius = 0f private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = cornerColor.toArgb() style = Paint.Style.FILL } private val trPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL color = Color.TRANSPARENT xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } private val squarePath = Path() private val circlePath = Path() private val path = Path() val radii = floatArrayOf( 0f, 0f, // Top-left corner 0f, 0f, // Top-right corner 0f, 0f, // Bottom-right corner 0f, 0f // Bottom-left corner ) override fun draw(canvas: Canvas) { canvas.drawPath(path, paint) } fun setCornerColor(newColor: Int) { path.reset() paint.color = newColor onBoundsChange(currentBounds.toRect()) invalidateSelf() // Trigger redraw } override fun onBoundsChange(bounds: Rect) { super.onBoundsChange(bounds) squarePath.reset() path.reset() currentBounds.set(bounds) squarePath.addRect(currentBounds, Path.Direction.CW) updatePath(currentRadius) invalidateSelf() } override fun setAlpha(alpha: Int) { trPaint.alpha = alpha } override fun setColorFilter(colorFilter: ColorFilter?) { trPaint.colorFilter = colorFilter } @kotlin.Deprecated("Deprecated in Java") override fun getOpacity(): Int { return PixelFormat.TRANSLUCENT } fun show(executor: ShellExecutor, immediate: Boolean = false) { if (immediate) { currentRadius = radius updatePath(currentRadius) invalidateSelf() return } animateRadius(executor, currentRadius, radius) } fun hide(executor: ShellExecutor, immediate: Boolean = false) { if (immediate) { currentRadius = 0f updatePath(currentRadius) invalidateSelf() return } animateRadius(executor, currentRadius, 0f) } @SuppressLint("Recycle") private fun animateRadius( executor: ShellExecutor, fromRadius: Float, targetRadius: Float ) { ValueAnimator.ofFloat(fromRadius, targetRadius).apply { this.duration = ANIMATION_DURATION addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) {} override fun onAnimationEnd(animation: Animator) { currentRadius = targetRadius } override fun onAnimationCancel(animation: Animator) { currentRadius = fromRadius } override fun onAnimationRepeat(animation: Animator) {} }) addUpdateListener { animation -> updatePath(animation.animatedValue as Float) // Update the path with the new radius invalidateSelf() // Trigger redraw } // This is where start is invoked. executor.executeDelayed(::start, ANIMATION_DELAY) } } private fun updatePath(radius: Float) { path.reset() circlePath.reset() radii[0] = radius radii[1] = radius circlePath.addRoundRect(currentBounds, radii, Path.Direction.CCW) path.op(squarePath, circlePath, Path.Op.DIFFERENCE) path.flip() } private fun RectF.toRect() = Rect(this.top.toInt(), this.left.toInt(), this.right.toInt(), this.bottom.toInt()) fun flip(flipType: FlipType): RoundedCornersDrawable { when (flipType) { FlipType.FLIP_VERTICAL -> verticalFlipped = !verticalFlipped FlipType.FLIP_HORIZONTAL -> horizontalFlipped = !horizontalFlipped } return this } private fun Path.flip() { val matrix = Matrix() if (horizontalFlipped) { matrix.preScale( -1f, 1f, bounds.centerX().toFloat(), bounds.centerY().toFloat() ) // Flip horizontally } if (verticalFlipped) { matrix.preScale( 1f, -1f, bounds.centerX().toFloat(), bounds.centerY().toFloat() ) // Flip vertically } transform(matrix) } } libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/roundedcorners/RoundedCornersFactory.kt 0 → 100644 +36 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.compatui.letterbox.roundedcorners import android.graphics.Color import android.graphics.drawable.Drawable /** * Abstraction for the object responsible of the creation of the rounded corners Drawables. */ interface RoundedCornersFactory<T : Drawable> { enum class Position { TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT } /** * @param color The color of the rounded corner background. * @param position The position of the rounded corner. * @param radius The radius of the rounded corner. * @return The [Drawable] for the rounded corner in a given [position] */ fun getRoundedCornerDrawable(color: Color, position: Position, radius: Float = 0f): T } Loading
libs/WindowManager/Shell/res/layout/letterbox_shell_rounded_corners.xml 0 → 100644 +49 −0 Original line number Diff line number Diff line <!-- ~ Copyright 2025 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. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main" android:background="@android:color/transparent" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerTopLeft" android:layout_gravity="left|top" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerTopRight" android:layout_gravity="right|top" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerBottomLeft" android:layout_gravity="left|bottom" android:layout_width="20dp" android:layout_height="20dp" /> <ImageView android:contentDescription="@null" android:id="@+id/roundedCornerBottomRight" android:layout_gravity="right|bottom" android:layout_width="20dp" android:layout_height="20dp" /> </FrameLayout> No newline at end of file
libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/roundedcorners/LetterboxRoundedCornersFactory.kt 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.compatui.letterbox.roundedcorners import android.graphics.Color import android.graphics.drawable.Drawable import com.android.wm.shell.compatui.letterbox.roundedcorners.RoundedCornersDrawable.FlipType.FLIP_HORIZONTAL import com.android.wm.shell.compatui.letterbox.roundedcorners.RoundedCornersDrawable.FlipType.FLIP_VERTICAL import com.android.wm.shell.compatui.letterbox.roundedcorners.RoundedCornersFactory.Position import com.android.wm.shell.dagger.WMSingleton import javax.inject.Inject /** * [RoundedCornersFactory] implementation returning rounded corners [Drawable]s using * SVG format. */ @WMSingleton class LetterboxRoundedCornersFactory @Inject constructor( ) : RoundedCornersFactory<RoundedCornersDrawable> { override fun getRoundedCornerDrawable( color: Color, position: Position, radius: Float ): RoundedCornersDrawable { val corners = RoundedCornersDrawable(color, radius) return when (position) { Position.TOP_LEFT -> corners Position.TOP_RIGHT -> corners.flip(FLIP_HORIZONTAL) Position.BOTTOM_LEFT -> corners.flip(FLIP_VERTICAL) Position.BOTTOM_RIGHT -> corners.flip(FLIP_HORIZONTAL) .flip(FLIP_VERTICAL) } } } No newline at end of file
libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/roundedcorners/RoundedCornersDrawable.kt 0 → 100644 +212 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.compatui.letterbox.roundedcorners import android.animation.Animator import android.animation.ValueAnimator import android.annotation.SuppressLint import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Matrix import android.graphics.Paint import android.graphics.Path import android.graphics.PixelFormat import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable import com.android.wm.shell.common.ShellExecutor /** * Rounded corner [Drawable] implementation */ class RoundedCornersDrawable( private var cornerColor: Color, private var radius: Float = 0f ) : Drawable() { enum class FlipType { FLIP_VERTICAL, FLIP_HORIZONTAL } companion object { @JvmStatic private val ANIMATION_DURATION = 350L // To make the animation visible we add a small delay @JvmStatic private val ANIMATION_DELAY = 200L } private val currentBounds = RectF() private var verticalFlipped = false private var horizontalFlipped = false private var currentRadius = 0f private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = cornerColor.toArgb() style = Paint.Style.FILL } private val trPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL color = Color.TRANSPARENT xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } private val squarePath = Path() private val circlePath = Path() private val path = Path() val radii = floatArrayOf( 0f, 0f, // Top-left corner 0f, 0f, // Top-right corner 0f, 0f, // Bottom-right corner 0f, 0f // Bottom-left corner ) override fun draw(canvas: Canvas) { canvas.drawPath(path, paint) } fun setCornerColor(newColor: Int) { path.reset() paint.color = newColor onBoundsChange(currentBounds.toRect()) invalidateSelf() // Trigger redraw } override fun onBoundsChange(bounds: Rect) { super.onBoundsChange(bounds) squarePath.reset() path.reset() currentBounds.set(bounds) squarePath.addRect(currentBounds, Path.Direction.CW) updatePath(currentRadius) invalidateSelf() } override fun setAlpha(alpha: Int) { trPaint.alpha = alpha } override fun setColorFilter(colorFilter: ColorFilter?) { trPaint.colorFilter = colorFilter } @kotlin.Deprecated("Deprecated in Java") override fun getOpacity(): Int { return PixelFormat.TRANSLUCENT } fun show(executor: ShellExecutor, immediate: Boolean = false) { if (immediate) { currentRadius = radius updatePath(currentRadius) invalidateSelf() return } animateRadius(executor, currentRadius, radius) } fun hide(executor: ShellExecutor, immediate: Boolean = false) { if (immediate) { currentRadius = 0f updatePath(currentRadius) invalidateSelf() return } animateRadius(executor, currentRadius, 0f) } @SuppressLint("Recycle") private fun animateRadius( executor: ShellExecutor, fromRadius: Float, targetRadius: Float ) { ValueAnimator.ofFloat(fromRadius, targetRadius).apply { this.duration = ANIMATION_DURATION addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) {} override fun onAnimationEnd(animation: Animator) { currentRadius = targetRadius } override fun onAnimationCancel(animation: Animator) { currentRadius = fromRadius } override fun onAnimationRepeat(animation: Animator) {} }) addUpdateListener { animation -> updatePath(animation.animatedValue as Float) // Update the path with the new radius invalidateSelf() // Trigger redraw } // This is where start is invoked. executor.executeDelayed(::start, ANIMATION_DELAY) } } private fun updatePath(radius: Float) { path.reset() circlePath.reset() radii[0] = radius radii[1] = radius circlePath.addRoundRect(currentBounds, radii, Path.Direction.CCW) path.op(squarePath, circlePath, Path.Op.DIFFERENCE) path.flip() } private fun RectF.toRect() = Rect(this.top.toInt(), this.left.toInt(), this.right.toInt(), this.bottom.toInt()) fun flip(flipType: FlipType): RoundedCornersDrawable { when (flipType) { FlipType.FLIP_VERTICAL -> verticalFlipped = !verticalFlipped FlipType.FLIP_HORIZONTAL -> horizontalFlipped = !horizontalFlipped } return this } private fun Path.flip() { val matrix = Matrix() if (horizontalFlipped) { matrix.preScale( -1f, 1f, bounds.centerX().toFloat(), bounds.centerY().toFloat() ) // Flip horizontally } if (verticalFlipped) { matrix.preScale( 1f, -1f, bounds.centerX().toFloat(), bounds.centerY().toFloat() ) // Flip vertically } transform(matrix) } }
libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/roundedcorners/RoundedCornersFactory.kt 0 → 100644 +36 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.wm.shell.compatui.letterbox.roundedcorners import android.graphics.Color import android.graphics.drawable.Drawable /** * Abstraction for the object responsible of the creation of the rounded corners Drawables. */ interface RoundedCornersFactory<T : Drawable> { enum class Position { TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT } /** * @param color The color of the rounded corner background. * @param position The position of the rounded corner. * @param radius The radius of the rounded corner. * @return The [Drawable] for the rounded corner in a given [position] */ fun getRoundedCornerDrawable(color: Color, position: Position, radius: Float = 0f): T }