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

Commit 9964ad83 authored by Nick Chameyev's avatar Nick Chameyev Committed by Automerger Merge Worker
Browse files

Merge "Add unfold animation to launcher icons and widgets" into sc-v2-dev am:...

Merge "Add unfold animation to launcher icons and widgets" into sc-v2-dev am: ef994701 am: 94a5d90c

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15390554

Change-Id: I28ee98a75413b1fa3a3dbd9c6172167326a39aa4
parents 66e31ac4 94a5d90c
Loading
Loading
Loading
Loading
+146 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.shared.animation

import android.graphics.Point
import android.util.MathUtils.lerp
import android.view.Surface
import android.view.View
import android.view.WindowManager
import com.android.unfold.UnfoldTransitionProgressProvider
import java.lang.ref.WeakReference

/**
 * Creates an animation where all registered views are moved into their final location
 * by moving from the center of the screen to the sides
 */
class UnfoldMoveFromCenterAnimator(
    private val windowManager: WindowManager,
    /**
     * Allows to set custom translation applier
     * Could be useful when a view could be translated from
     * several sources and we want to set the translation
     * using custom methods instead of [View.setTranslationX] or
     * [View.setTranslationY]
     */
    var translationApplier: TranslationApplier = object : TranslationApplier {}
) : UnfoldTransitionProgressProvider.TransitionProgressListener {

    private val screenSize = Point()
    private var isVerticalFold = false

    private val animatedViews: MutableList<AnimatedView> = arrayListOf()
    private val tmpArray = IntArray(2)

    /**
     * Updates display properties in order to calculate the initial position for the views
     * Must be called before [registerViewForAnimation]
     */
    fun updateDisplayProperties() {
        windowManager.defaultDisplay.getSize(screenSize)

        // Simple implementation to get current fold orientation,
        // this might not be correct on all devices
        // TODO: use JetPack WindowManager library to get the fold orientation
        isVerticalFold = windowManager.defaultDisplay.rotation == Surface.ROTATION_0 ||
            windowManager.defaultDisplay.rotation == Surface.ROTATION_180
    }

    /**
     * Registers a view to be animated, the view should be measured and layouted
     * After finishing the animation it is necessary to clear
     * the views using [clearRegisteredViews]
     */
    fun registerViewForAnimation(view: View) {
        val animatedView = createAnimatedView(view)
        animatedViews.add(animatedView)
    }

    /**
     * Unregisters all registered views and resets their translation
     */
    fun clearRegisteredViews() {
        onTransitionProgress(1f)
        animatedViews.clear()
    }

    override fun onTransitionProgress(progress: Float) {
        animatedViews.forEach {
            it.view.get()?.let { view ->
                translationApplier.apply(
                    view = view,
                    x = lerp(it.startTranslationX, it.finishTranslationX, progress),
                    y = lerp(it.startTranslationY, it.finishTranslationY, progress)
                )
            }
        }
    }

    private fun createAnimatedView(view: View): AnimatedView {
        val viewLocation = tmpArray
        view.getLocationOnScreen(viewLocation)

        val viewX = viewLocation[0].toFloat()
        val viewY = viewLocation[1].toFloat()

        val viewCenterX = viewX + view.width / 2
        val viewCenterY = viewY + view.height / 2

        val translationXDiff: Float
        val translationYDiff: Float

        if (isVerticalFold) {
            val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
            translationXDiff = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
            translationYDiff = 0f
        } else {
            val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
            translationXDiff = 0f
            translationYDiff = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE
        }

        return AnimatedView(
            view = WeakReference(view),
            startTranslationX = view.translationX + translationXDiff,
            startTranslationY = view.translationY + translationYDiff,
            finishTranslationX = view.translationX,
            finishTranslationY = view.translationY
        )
    }

    /**
     * Interface that allows to use custom logic to apply translation to view
     */
    interface TranslationApplier {
        /**
         * Called when we need to apply [x] and [y] translation to [view]
         */
        fun apply(view: View, x: Float, y: Float) {
            view.translationX = x
            view.translationY = y
        }
    }

    private class AnimatedView(
        val view: WeakReference<View>,
        val startTranslationX: Float,
        val startTranslationY: Float,
        val finishTranslationX: Float,
        val finishTranslationY: Float
    )
}

private const val TRANSLATION_PERCENTAGE = 0.3f
+5 −0
Original line number Diff line number Diff line
@@ -90,4 +90,9 @@ oneway interface IOverviewProxy {
     * Sent when behavior changes. See WindowInsetsController#@Behavior
     */
    void onSystemBarAttributesChanged(int displayId, int behavior) = 20;

    /**
     * Sent when screen turned on and ready to use (blocker scrim is hidden)
     */
    void onScreenTurnedOn() = 21;
}
+3 −3
Original line number Diff line number Diff line
@@ -31,8 +31,8 @@ interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgre
    fun destroy()

    interface TransitionProgressListener {
        fun onTransitionStarted()
        fun onTransitionFinished()
        fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float)
        fun onTransitionStarted() {}
        fun onTransitionFinished() {}
        fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
    }
}
+24 −0
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ import com.android.internal.util.ScreenshotHelper;
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBar;
import com.android.systemui.navigationbar.NavigationBarController;
@@ -543,6 +544,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
            Optional<OneHanded> oneHandedOptional,
            BroadcastDispatcher broadcastDispatcher,
            ShellTransitions shellTransitions,
            ScreenLifecycle screenLifecycle,
            Optional<StartingSurface> startingSurface,
            SmartspaceTransitionController smartspaceTransitionController) {
        super(broadcastDispatcher);
@@ -608,6 +610,13 @@ public class OverviewProxyService extends CurrentUserTracker implements
        // Listen for user setup
        startTracking();

        screenLifecycle.addObserver(new ScreenLifecycle.Observer() {
            @Override
            public void onScreenTurnedOn() {
                notifyScreenTurnedOn();
            }
        });

        // Connect to the service
        updateEnabledState();
        startConnectionToCurrentUser();
@@ -900,6 +909,21 @@ public class OverviewProxyService extends CurrentUserTracker implements
        }
    }

    /**
     * Notifies the Launcher that screen turned on and ready to use
     */
    public void notifyScreenTurnedOn() {
        try {
            if (mOverviewProxy != null) {
                mOverviewProxy.onScreenTurnedOn();
            } else {
                Log.e(TAG_OPS, "Failed to get overview proxy for screen turned on event.");
            }
        } catch (RemoteException e) {
            Log.e(TAG_OPS, "Failed to call notifyScreenTurnedOn()", e);
        }
    }

    void notifyToggleRecentApps() {
        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
            mConnectionCallbacks.get(i).onToggleRecentApps();
+173 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.shared.animation

import android.graphics.Point
import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.Surface.ROTATION_0
import android.view.Surface.ROTATION_90
import android.view.View
import android.view.WindowManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.junit.MockitoJUnit
import org.mockito.Mockito.`when` as whenever

@RunWith(AndroidTestingRunner::class)
@SmallTest
class UnfoldMoveFromCenterAnimatorTest : SysuiTestCase() {

    @Mock
    private lateinit var windowManager: WindowManager

    @get:Rule
    val mockito = MockitoJUnit.rule()

    private lateinit var animator: UnfoldMoveFromCenterAnimator

    @Before
    fun before() {
        animator = UnfoldMoveFromCenterAnimator(windowManager)
    }

    @Test
    fun testRegisterViewOnTheLeftOfVerticalFold_halfProgress_viewTranslatedToTheRight() {
        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
        val view = createView(x = 20)
        animator.registerViewForAnimation(view)
        animator.onTransitionStarted()

        animator.onTransitionProgress(0.5f)

        // Positive translationX -> translated to the right
        assertThat(view.translationX).isWithin(0.1f).of(3.75f)
    }

    @Test
    fun testRegisterViewOnTheLeftOfVerticalFold_zeroProgress_viewTranslatedToTheRight() {
        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
        val view = createView(x = 20)
        animator.registerViewForAnimation(view)
        animator.onTransitionStarted()

        animator.onTransitionProgress(0f)

        // Positive translationX -> translated to the right
        assertThat(view.translationX).isWithin(0.1f).of(7.5f)
    }

    @Test
    fun testRegisterViewOnTheLeftOfVerticalFold_fullProgress_viewTranslatedToTheOriginalPosition() {
        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
        val view = createView(x = 20)
        animator.registerViewForAnimation(view)
        animator.onTransitionStarted()

        animator.onTransitionProgress(1f)

        assertThat(view.translationX).isEqualTo(0f)
    }

    @Test
    fun testRegisterViewAndUnregister_halfProgress_viewIsNotUpdated() {
        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
        val view = createView(x = 20)
        animator.registerViewForAnimation(view)
        animator.onTransitionStarted()
        animator.clearRegisteredViews()

        animator.onTransitionProgress(0.5f)

        assertThat(view.translationX).isEqualTo(0f)
    }

    @Test
    fun testRegisterViewUpdateProgressAndUnregister_halfProgress_viewIsNotUpdated() {
        givenScreen(width = 100, height = 100, rotation = ROTATION_0)
        val view = createView(x = 20)
        animator.registerViewForAnimation(view)
        animator.onTransitionStarted()
        animator.onTransitionProgress(0.2f)
        animator.clearRegisteredViews()

        animator.onTransitionProgress(0.5f)

        assertThat(view.translationX).isEqualTo(0f)
    }

    @Test
    fun testRegisterViewOnTheTopOfHorizontalFold_halfProgress_viewTranslatedToTheBottom() {
        givenScreen(width = 100, height = 100, rotation = ROTATION_90)
        val view = createView(y = 20)
        animator.registerViewForAnimation(view)
        animator.onTransitionStarted()

        animator.onTransitionProgress(0.5f)

        // Positive translationY -> translated to the bottom
        assertThat(view.translationY).isWithin(0.1f).of(3.75f)
    }

    private fun createView(
        x: Int = 0,
        y: Int = 0,
        width: Int = 10,
        height: Int = 10,
        translationX: Float = 0f,
        translationY: Float = 0f
    ): View {
        val view = spy(View(context))
        doAnswer {
            val location = (it.arguments[0] as IntArray)
            location[0] = x
            location[1] = y
            Unit
        }.`when`(view).getLocationOnScreen(any())

        whenever(view.width).thenReturn(width)
        whenever(view.height).thenReturn(height)

        return view.apply {
            setTranslationX(translationX)
            setTranslationY(translationY)
        }
    }

    private fun givenScreen(width: Int = 100,
                            height: Int = 100,
                            rotation: Int = ROTATION_0) {
        val display = mock(Display::class.java)
        whenever(display.getSize(any())).thenAnswer {
            val size = (it.arguments[0] as Point)
            size.set(width, height)
            Unit
        }
        whenever(display.rotation).thenReturn(rotation)
        whenever(windowManager.defaultDisplay).thenReturn(display)

        animator.updateDisplayProperties()
    }
}