Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bd8c6406 authored by Massimo Carli's avatar Massimo Carli
Browse files

[2/n] Create RoundedCorners components

Creates the Drawable, Views to be used for letterbox rounded
corners implementation in Shell.

Flag: EXEMPT Utilities
Bug: 371500295
Test: m

Change-Id: Id504c99c5787e3e948d4fc6c3de4cb86a51f5473
parent 37d324a5
Loading
Loading
Loading
Loading
+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
+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
+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)
    }
}
+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
}