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

Commit bc1f67ec authored by Yein Jo's avatar Yein Jo
Browse files

Add MultiRippleView and its controller to support UMO touch ripple

effects.

The current RippleView is designed for a single ripple effect. For UMO
touch ripple, multiple touch ripple effects should be supported, thus
adding a new MultiRippleView. RippleView may be refactored such that it
takes in RippleAnimation later.

Design doc: go/surface-effects-umo
Bug: 237282226
Test: MultiRippleControllerTest, RippleAnimationTest

Change-Id: Ic199a5552b0c40e5c09c12f06c280ff054f462cb
parent 1e50de89
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -149,7 +149,7 @@ class WiredChargingRippleController @Inject constructor(
    }

    fun startRipple() {
        if (rippleView.rippleInProgress || rippleView.parent != null) {
        if (rippleView.rippleInProgress() || rippleView.parent != null) {
            // Skip if ripple is still playing, or not playing but already added the parent
            // (which might happen just before the animation starts or right after
            // the animation ends.)
+2 −2
Original line number Diff line number Diff line
@@ -33,9 +33,9 @@ import android.widget.TextView;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.ripple.RippleAnimationConfig;
import com.android.systemui.ripple.RippleShader.RippleShape;
import com.android.systemui.ripple.RippleView;
import com.android.systemui.ripple.RippleViewKt;

import java.text.NumberFormat;

@@ -150,7 +150,7 @@ final class WirelessChargingLayout extends FrameLayout {
            mRippleView.setColor(color, 28);
        } else {
            mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
            mRippleView.setColor(color, RippleViewKt.RIPPLE_DEFAULT_ALPHA);
            mRippleView.setColor(color, RippleAnimationConfig.RIPPLE_DEFAULT_ALPHA);
        }

        OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
+1 −1
Original line number Diff line number Diff line
@@ -189,7 +189,7 @@ class MediaTttChipControllerReceiver @Inject constructor(
    }

    private fun startRipple(rippleView: ReceiverChipRippleView) {
        if (rippleView.rippleInProgress) {
        if (rippleView.rippleInProgress()) {
            // Skip if ripple is still playing
            return
        }
+42 −0
Original line number Diff line number Diff line
/*
 * 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.ripple

import androidx.annotation.VisibleForTesting

/** Controller that handles playing [RippleAnimation]. */
class MultiRippleController(private val multipleRippleView: MultiRippleView) {

    companion object {
        /** Max number of ripple animations at a time. */
        @VisibleForTesting const val MAX_RIPPLE_NUMBER = 10
    }

    fun play(rippleAnimation: RippleAnimation) {
        if (multipleRippleView.ripples.size >= MAX_RIPPLE_NUMBER) {
            return
        }

        multipleRippleView.ripples.add(rippleAnimation)

        // Remove ripple once the animation is done
        rippleAnimation.play { multipleRippleView.ripples.remove(rippleAnimation) }

        // Trigger drawing
        multipleRippleView.invalidate()
    }
}
+67 −0
Original line number Diff line number Diff line
/*
 * 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.ripple

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.util.Log
import android.view.View

/**
 * A view that allows multiple ripples to play.
 *
 * Use [MultiRippleController] to play ripple animations.
 */
class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    internal val ripples = ArrayList<RippleAnimation>()
    private val ripplePaint = Paint()
    private var isWarningLogged = false

    companion object {
        const val TAG = "MultiRippleView"
    }

    override fun onDraw(canvas: Canvas?) {
        if (canvas == null || !canvas.isHardwareAccelerated) {
            // Drawing with the ripple shader requires hardware acceleration, so skip
            // if it's unsupported.
            if (!isWarningLogged) {
                // Only log once to not spam.
                Log.w(
                    TAG,
                    "Can't draw ripple shader. $canvas does not support hardware acceleration."
                )
                isWarningLogged = true
            }
            return
        }

        var shouldInvalidate = false

        ripples.forEach { anim ->
            ripplePaint.shader = anim.rippleShader
            canvas.drawPaint(ripplePaint)

            shouldInvalidate = shouldInvalidate || anim.isPlaying()
        }

        if (shouldInvalidate) invalidate()
    }
}
Loading