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

Commit cfd3326e authored by Liran Binyamin's avatar Liran Binyamin Committed by Android (Google) Code Review
Browse files

Merge "Update taskbar window size for flyout" into main

parents 28dbcc72 c33fe5bd
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.taskbar.TaskbarInsetsController;
import com.android.launcher3.taskbar.TaskbarStashController;
import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController;
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
import com.android.launcher3.util.MultiPropertyFactory;
@@ -117,6 +118,8 @@ public class BubbleBarViewController {
    public boolean mOverflowAdded;

    private BubbleBarViewAnimator mBubbleBarViewAnimator;
    private final FrameLayout mBubbleBarContainer;
    private BubbleBarFlyoutController mBubbleBarFlyoutController;

    private final TimeSource mTimeSource = System::currentTimeMillis;

@@ -127,6 +130,7 @@ public class BubbleBarViewController {
            FrameLayout bubbleBarContainer) {
        mActivity = activity;
        mBarView = barView;
        mBubbleBarContainer = bubbleBarContainer;
        mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
        mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
        mIconSize = activity.getResources().getDimensionPixelSize(
@@ -141,6 +145,8 @@ public class BubbleBarViewController {
        mBubbleDragController = bubbleControllers.bubbleDragController;
        mTaskbarStashController = controllers.taskbarStashController;
        mTaskbarInsetsController = controllers.taskbarInsetsController;
        mBubbleBarFlyoutController = new BubbleBarFlyoutController(
                mBubbleBarContainer, createFlyoutPositioner(), createFlyoutTopBoundaryListener());
        mBubbleBarViewAnimator = new BubbleBarViewAnimator(
                mBarView, mBubbleStashController, mBubbleBarController::showExpandedView);
        mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
@@ -266,6 +272,21 @@ public class BubbleBarViewController {
        };
    }

    private BubbleBarFlyoutController.TopBoundaryListener createFlyoutTopBoundaryListener() {
        return new BubbleBarFlyoutController.TopBoundaryListener() {
            @Override
            public void extendTopBoundary(int space) {
                int defaultSize = mActivity.getDefaultTaskbarWindowSize();
                mActivity.setTaskbarWindowSize(defaultSize + space);
            }

            @Override
            public void resetTopBoundary() {
                mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize());
            }
        };
    }

    private void onBubbleClicked(BubbleView bubbleView) {
        bubbleView.markSeen();
        BubbleBarItem bubble = bubbleView.getBubble();
+42 −6
Original line number Diff line number Diff line
@@ -21,20 +21,30 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.animation.ValueAnimator
import com.android.launcher3.R
import com.android.systemui.util.doOnEnd
import com.android.systemui.util.doOnStart

/** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
class BubbleBarFlyoutController(
class BubbleBarFlyoutController
@JvmOverloads
constructor(
    private val container: FrameLayout,
    private val positioner: BubbleBarFlyoutPositioner,
    private val topBoundaryListener: TopBoundaryListener,
    private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container),
) {

    private companion object {
        const val EXPAND_COLLAPSE_ANIMATION_DURATION_MS = 250L
    }

    private var flyout: BubbleBarFlyoutView? = null
    private val horizontalMargin =
        container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)

    fun setUpFlyout(message: BubbleBarFlyoutMessage) {
        flyout?.let(container::removeView)
        val flyout = BubbleBarFlyoutView(container.context, positioner)
        val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)

        flyout.translationY = positioner.targetTy

@@ -48,17 +58,43 @@ class BubbleBarFlyoutController(
        lp.marginEnd = horizontalMargin
        container.addView(flyout, lp)

        val animator = ValueAnimator.ofFloat(0f, 1f)
        val animator =
            ValueAnimator.ofFloat(0f, 1f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS)
        animator.addUpdateListener { _ ->
            flyout.updateExpansionProgress(animator.animatedValue as Float)
        }
        animator.doOnStart {
            val flyoutTop = flyout.top + flyout.translationY
            // If the top position of the flyout is negative, then it's bleeding over the
            // top boundary of its parent view
            if (flyoutTop < 0) topBoundaryListener.extendTopBoundary(space = -flyoutTop.toInt())
        }
        flyout.showFromCollapsed(message) { animator.start() }
        this.flyout = flyout
    }

    fun hideFlyout() {
    fun hideFlyout(endAction: () -> Unit) {
        val flyout = this.flyout ?: return
        val animator =
            ValueAnimator.ofFloat(1f, 0f).setDuration(EXPAND_COLLAPSE_ANIMATION_DURATION_MS)
        animator.addUpdateListener { _ ->
            flyout.updateExpansionProgress(animator.animatedValue as Float)
        }
        animator.doOnEnd {
            container.removeView(flyout)
        this.flyout = null
            this@BubbleBarFlyoutController.flyout = null
            topBoundaryListener.resetTopBoundary()
            endAction()
        }
        animator.start()
    }

    /** Notifies when the top boundary of the flyout view changes. */
    interface TopBoundaryListener {
        /** Requests to extend the top boundary of the parent to fully include the flyout. */
        fun extendTopBoundary(space: Int)

        /** Resets the top boundary of the parent. */
        fun resetTopBoundary()
    }
}
+7 −4
Original line number Diff line number Diff line
@@ -36,14 +36,18 @@ import com.android.launcher3.R
import com.android.launcher3.popup.RoundedArrowDrawable

/** The flyout view used to notify the user of a new bubble notification. */
class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFlyoutPositioner) :
    ConstraintLayout(context) {
class BubbleBarFlyoutView(
    context: Context,
    private val positioner: BubbleBarFlyoutPositioner,
    scheduler: FlyoutScheduler? = null,
) : ConstraintLayout(context) {

    private companion object {
        // the minimum progress of the expansion animation before the content starts fading in.
        const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f
    }

    private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this)
    private val title: TextView by
        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_title) }

@@ -197,11 +201,10 @@ class BubbleBarFlyoutView(context: Context, private val positioner: BubbleBarFly

        // post the request to start the expand animation to the looper so the view can measure
        // itself
        post(expandAnimation)
        scheduler.runAfterLayout(expandAnimation)
    }

    private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
        // the avatar is only displayed in group chat messages
        if (flyoutMessage.icon != null) {
            icon.visibility = VISIBLE
            icon.setImageDrawable(flyoutMessage.icon)
+32 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.launcher3.taskbar.bubbles.flyout

import android.view.View

/** Interface for scheduling jobs by flyout. */
fun interface FlyoutScheduler {
    /** Runs the given [block] after layout. */
    fun runAfterLayout(block: () -> Unit)
}

/** A [FlyoutScheduler] that uses a Handler to schedule jobs. */
class HandlerScheduler(val view: View) : FlyoutScheduler {
    override fun runAfterLayout(block: () -> Unit) {
        view.post(block)
    }
}
+104 −24
Original line number Diff line number Diff line
@@ -22,12 +22,15 @@ import android.graphics.PointF
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.animation.AnimatorTestRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.R
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@@ -36,11 +39,15 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class BubbleBarFlyoutControllerTest {

    @get:Rule val animatorTestRule = AnimatorTestRule()

    private lateinit var flyoutController: BubbleBarFlyoutController
    private lateinit var flyoutContainer: FrameLayout
    private lateinit var topBoundaryListener: FakeTopBoundaryListener
    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message")
    private var onLeft = true
    private var flyoutTy = 50f

    @Before
    fun setUp() {
@@ -50,18 +57,29 @@ class BubbleBarFlyoutControllerTest {
                override val isOnLeft
                    get() = onLeft

                override val targetTy = 50f
                override val targetTy
                    get() = flyoutTy

                override val distanceToCollapsedPosition = PointF(100f, 200f)
                override val collapsedSize = 30f
                override val collapsedColor = Color.BLUE
                override val collapsedElevation = 1f
                override val distanceToRevealTriangle = 50f
            }
        flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner)
        topBoundaryListener = FakeTopBoundaryListener()
        val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
        flyoutController =
            BubbleBarFlyoutController(
                flyoutContainer,
                positioner,
                topBoundaryListener,
                flyoutScheduler,
            )
    }

    @Test
    fun flyoutPosition_left() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
            val flyout = flyoutContainer.getChildAt(0)
@@ -69,10 +87,12 @@ class BubbleBarFlyoutControllerTest {
            assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT)
            assertThat(flyout.translationY).isEqualTo(50f)
        }
    }

    @Test
    fun flyoutPosition_right() {
        onLeft = false
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
            val flyout = flyoutContainer.getChildAt(0)
@@ -80,9 +100,11 @@ class BubbleBarFlyoutControllerTest {
            assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT)
            assertThat(flyout.translationY).isEqualTo(50f)
        }
    }

    @Test
    fun flyoutMessage() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
            val flyout = flyoutContainer.getChildAt(0)
@@ -91,12 +113,70 @@ class BubbleBarFlyoutControllerTest {
            val message = flyout.findViewById<TextView>(R.id.bubble_flyout_text)
            assertThat(message.text).isEqualTo("message")
        }
    }

    @Test
    fun hideFlyout_removedFromContainer() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
        flyoutController.hideFlyout()
            flyoutController.hideFlyout {}
            animatorTestRule.advanceTimeBy(300)
        }
        assertThat(flyoutContainer.childCount).isEqualTo(0)
    }

    @Test
    fun showFlyout_extendsTopBoundary() {
        // set negative translation for the flyout so that it will request to extend the top
        // boundary
        flyoutTy = -50f
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
        }
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            animatorTestRule.advanceTimeBy(300)
        }
        assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(50)
    }

    @Test
    fun showFlyout_withinBoundary() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
        }
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            animatorTestRule.advanceTimeBy(300)
        }
        assertThat(topBoundaryListener.topBoundaryExtendedSpace).isEqualTo(0)
    }

    @Test
    fun hideFlyout_resetsTopBoundary() {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            flyoutController.setUpFlyout(flyoutMessage)
            assertThat(flyoutContainer.childCount).isEqualTo(1)
            flyoutController.hideFlyout {}
            animatorTestRule.advanceTimeBy(300)
        }
        assertThat(topBoundaryListener.topBoundaryReset).isTrue()
    }

    class FakeTopBoundaryListener : BubbleBarFlyoutController.TopBoundaryListener {

        var topBoundaryExtendedSpace = 0
        var topBoundaryReset = false

        override fun extendTopBoundary(space: Int) {
            topBoundaryExtendedSpace = space
        }

        override fun resetTopBoundary() {
            topBoundaryReset = true
        }
    }
}