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

Commit 4c824ba3 authored by András Kurucz's avatar András Kurucz
Browse files

[Dual Shade] Blur Notifications when QS is open over LS

Blur the NSSL's content when it is displayed on the LockScreen, and is
covered by the QuickSettings overlay.

Given the limitation of Flexiglass, that the NSSL is always z-ordered
above everything, this CL overrides NSSL's draw phase so that it can
create the visual impression of opening the QS over Notifications by:
 - drawing everything (but HUNs) to a separate canvas with a blur effect
 - clipping the blurred content by the QS shape
 - drawing the HUN on top of everything without blur, without clipping

Bug: 388469101
Test: Open the QS over the LS, and reveive a HUN
       - have a few notifications on the LS
       - drag down from top-right to open QS shade
       - receive a HUN
Flag: com.android.systemui.scene_container
Change-Id: I18b264f5b13ea5335dab1b23a99dbe97944248e5
parent 658b0cad
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -1242,6 +1242,9 @@
    <dimen name="min_window_blur_radius">1px</dimen>
    <dimen name="max_window_blur_radius">23px</dimen>

    <!-- Blur radius of the Notification Shade content (notifications, footer, shelf) -->
    <dimen name="max_shade_content_blur_radius">@dimen/max_window_blur_radius</dimen>

    <!-- Blur radius behind Notification Shade -->
    <dimen name="max_shade_window_blur_radius">60dp</dimen>

+85 −11
Original line number Diff line number Diff line
@@ -52,6 +52,9 @@ import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RenderEffect;
import android.graphics.RenderNode;
import android.graphics.Shader;
import android.os.Bundle;
import android.os.SystemClock;
import android.os.Trace;
@@ -513,6 +516,13 @@ public class NotificationStackScrollLayout
    /** The clip path defining where we are NOT allowed to draw. */
    private final Path mNegativeRoundedClipPath = new Path();

    /** RenderNode to blur notifications which will be reused (redrawn) whenever NSSL is drawn. */
    private final RenderNode mBlurNode = new RenderNode("BlurNode");

    /** Radius of the blur effect applied to the content of the NSSL. */
    private float mBlurRadius = 0f;
    @Nullable private RenderEffect mBlurEffect = null;

    /**
     * The clip Path used to clip the launching notification. This may be different
     * from the normal path, as the views launch animation could start clipped.
@@ -5974,6 +5984,24 @@ public class NotificationStackScrollLayout
        invalidate();
    }

    @Override
    public void setBlurRadius(float blurRadius) {
        if (mBlurRadius != blurRadius) {
            mBlurRadius = blurRadius;
            updateBlurEffect();
            invalidate();
        }
    }

    private void updateBlurEffect() {
        if (mBlurRadius > 0) {
            mBlurEffect =
                    RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.CLAMP);
        } else {
            mBlurEffect = null;
        }
    }

    /**
     * Set rounded rect clipping bounds on this view.
     */
@@ -6144,11 +6172,42 @@ public class NotificationStackScrollLayout

    @Override
    protected void dispatchDraw(@NonNull Canvas canvas) {
        if (mBlurEffect != null) {
            // reuse the cached RenderNode to blur
            mBlurNode.setPosition(0, 0, canvas.getWidth(), canvas.getHeight());
            mBlurNode.setRenderEffect(mBlurEffect);
            Canvas blurCanvas = mBlurNode.beginRecording();
            // draw all the children (except HUNs) on the blurred canvas
            super.dispatchDraw(blurCanvas);
            mBlurNode.endRecording();
            // apply clipping to the canvas
            int saveCount = canvas.save();
            applyClipToCanvas(canvas);
            // draw the blurred content to the clipped canvas
            canvas.drawRenderNode(mBlurNode);
            // restore the canvas, so it doesn't clip anymore
            canvas.restoreToCount(saveCount);
            // draw the children that were left out during the dispatchDraw phase
            for (int i = 0; i < getChildCount(); i++) {
                // TODO(b/388469101) draw these children in z-order
                ExpandableView child = getChildAtIndex(i);
                if (shouldSkipBlurForChild(child)) {
                    super.drawChild(canvas, child, getDrawingTime());
                }
            }
        } else {
            if (!mLaunchingNotification) {
            // When launching notifications, we're clipping the children individually instead of in
            // dispatchDraw
                // When launching notifications, we're clipping the children individually instead
                //  of in dispatchDraw
                applyClipToCanvas(canvas);
            }
            super.dispatchDraw(canvas);
        }
    }

    private void applyClipToCanvas(Canvas canvas) {
        if (mShouldUseRoundedRectClipping) {
                // Let's clip rounded.
            // clip by the positive path if it is defined
            canvas.clipPath(mRoundedClipPath);
        }
        if (mShouldUseNegativeRoundedRectClipping) {
@@ -6156,14 +6215,21 @@ public class NotificationStackScrollLayout
            canvas.clipOutPath(mNegativeRoundedClipPath);
        }
    }
        super.dispatchDraw(canvas);
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        boolean shouldUseClipping =
                mShouldUseRoundedRectClipping || mShouldUseNegativeRoundedRectClipping;
        if (mLaunchingNotification && shouldUseClipping) {
        if (mBlurEffect != null) {
            if (shouldSkipBlurForChild(child)) {
                // skip drawing this child during the regular dispatchDraw pass
                return false;
            } else {
                // draw the child as if nothing happened, non-blurred elements shouldn't be
                // affected by clipping either
                return super.drawChild(canvas, child, drawingTime);
            }
        } else if (mLaunchingNotification && shouldUseClipping) {
            // Let's clip children individually during notification launch
            canvas.save();
            ExpandableView expandableView = (ExpandableView) child;
@@ -6194,6 +6260,14 @@ public class NotificationStackScrollLayout
        }
    }

    private boolean shouldSkipBlurForChild(View child) {
        if (child instanceof ExpandableView row) {
            return row.isHeadsUpState();
        } else {
            return false;
        }
    }

    /**
     * Calculate the total translation needed when dismissing.
     */
+7 −0
Original line number Diff line number Diff line
@@ -58,6 +58,13 @@ interface NotificationScrollView {
     */
    fun setNegativeClippingShape(shape: ShadeScrimShape?)

    /**
     * Sets a blur effect on the view. A radius of 0 means no blur.
     *
     * @param radius blur radius in pixels
     */
    fun setBlurRadius(radius: Float)

    /** set the y position in px of the top of the stack in this view's coordinates */
    fun setStackTop(stackTop: Float)

+5 −0
Original line number Diff line number Diff line
@@ -94,6 +94,7 @@ constructor(
                }
            }
            launch { viewModel.qsExpandFraction.collectTraced { view.setQsExpandFraction(it) } }
            launch { viewModel.blurRadius(maxBlurRadius).collect(view::setBlurRadius) }
            launch {
                viewModel.isShowingStackOnLockscreen.collectTraced {
                    view.setShowingStackOnLockscreen(it)
@@ -146,6 +147,10 @@ constructor(
            }
        }

    /** blur radius to be applied when the QS panel is fully expanded */
    private val maxBlurRadius: Flow<Int> =
        configuration.getDimensionPixelSize(R.dimen.max_shade_content_blur_radius)

    /** flow of the scrim clipping radius */
    private val scrimRadius: Flow<Int>
        get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius)
+36 −0
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 *
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.statusbar.notification.stack.ui.viewmodel

import com.android.compose.animation.scene.ContentKey
@@ -46,11 +48,14 @@ import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
import dagger.Lazy
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
@@ -193,6 +198,37 @@ constructor(
    val qsExpandFraction: Flow<Float> =
        shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction")

    /** Blur radius to be applied to Notifications. */
    fun blurRadius(maxBlurRadius: Flow<Int>) =
        combine(blurFraction, maxBlurRadius) { fraction, maxRadius -> fraction * maxRadius }

    /**
     * Scale of the blur effect that should be applied to Notifications.
     *
     * 0 -> don't blur (default, removes all blur render effects) 1 -> do the full blur (apply a
     * render effect with the max blur radius)
     */
    private val blurFraction: Flow<Float> =
        if (SceneContainerFlag.isEnabled) {
            shadeModeInteractor.shadeMode.flatMapLatest { shadeMode ->
                when (shadeMode) {
                    ShadeMode.Dual ->
                        combineTransform(
                            shadeInteractor.shadeExpansion,
                            shadeInteractor.qsExpansion,
                        ) { notificationShadeExpansion, qsExpansion ->
                            if (notificationShadeExpansion == 0f) {
                                // Blur out notifications as the QS overlay panel expands
                                emit(qsExpansion)
                            }
                        }
                    else -> flowOf(0f)
                }
            }
        } else {
            flowOf(0f)
        }

    /** Whether we should close any open notification guts. */
    val shouldCloseGuts: Flow<Boolean> = stackAppearanceInteractor.shouldCloseGuts