Ensure that launched Views implement LaunchableView
This CL throws an exception when a GhostedViewLaunchAnimatorController, ActivityLaunchAnimator.Controller or DialogLaunchAnimator.Controller is created for a View that does not implement LaunchableView, which can lead to unexpected visibility bugs if the View visibility is changed during a launch animation. Bug: 243636422 Test: DialogLaunchAnimatorTest Test: ActivityLaunchAnimatorTest Test: GhostedViewLaunchAnimatorControllerTest Test: Launched all dialogs and activities animated by DialogLaunchAnimator and ActivityLaunchAnimator Change-Id: I1269089d3b1473e838d75b5b63659c2907493c0c
This commit is contained in:
@@ -41,6 +41,7 @@ import androidx.annotation.BinderThread
|
||||
import androidx.annotation.UiThread
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.internal.policy.ScreenDecorationsUtils
|
||||
import java.lang.IllegalArgumentException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "ActivityLaunchAnimator"
|
||||
@@ -338,13 +339,24 @@ class ActivityLaunchAnimator(
|
||||
* Return a [Controller] that will animate and expand [view] into the opening window.
|
||||
*
|
||||
* Important: The view must be attached to a [ViewGroup] when calling this function and
|
||||
* during the animation. For safety, this method will return null when it is not.
|
||||
* during the animation. For safety, this method will return null when it is not. The
|
||||
* view must also implement [LaunchableView], otherwise this method will throw.
|
||||
*
|
||||
* Note: The background of [view] should be a (rounded) rectangle so that it can be
|
||||
* properly animated.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromView(view: View, cujType: Int? = null): Controller? {
|
||||
// Make sure the View we launch from implements LaunchableView to avoid visibility
|
||||
// issues.
|
||||
if (view !is LaunchableView) {
|
||||
throw IllegalArgumentException(
|
||||
"An ActivityLaunchAnimator.Controller was created from a View that does " +
|
||||
"not implement LaunchableView. This can lead to subtle bugs where the" +
|
||||
" visibility of the View we are launching from is not what we expected."
|
||||
)
|
||||
}
|
||||
|
||||
if (view.parent !is ViewGroup) {
|
||||
Log.e(
|
||||
TAG,
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.android.systemui.animation.back.BackAnimationSpec
|
||||
import com.android.systemui.animation.back.applyTo
|
||||
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
|
||||
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
|
||||
import java.lang.IllegalArgumentException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val TAG = "DialogLaunchAnimator"
|
||||
@@ -157,12 +158,23 @@ constructor(
|
||||
* Create a [Controller] that can animate [source] to and from a dialog.
|
||||
*
|
||||
* Important: The view must be attached to a [ViewGroup] when calling this function and
|
||||
* during the animation. For safety, this method will return null when it is not.
|
||||
* during the animation. For safety, this method will return null when it is not. The
|
||||
* view must also implement [LaunchableView], otherwise this method will throw.
|
||||
*
|
||||
* Note: The background of [view] should be a (rounded) rectangle so that it can be
|
||||
* properly animated.
|
||||
*/
|
||||
fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
|
||||
// Make sure the View we launch from implements LaunchableView to avoid visibility
|
||||
// issues.
|
||||
if (source !is LaunchableView) {
|
||||
throw IllegalArgumentException(
|
||||
"A DialogLaunchAnimator.Controller was created from a View that does not " +
|
||||
"implement LaunchableView. This can lead to subtle bugs where the " +
|
||||
"visibility of the View we are launching from is not what we expected."
|
||||
)
|
||||
}
|
||||
|
||||
if (source.parent !is ViewGroup) {
|
||||
Log.e(
|
||||
TAG,
|
||||
@@ -249,23 +261,6 @@ constructor(
|
||||
}
|
||||
?: controller
|
||||
|
||||
if (
|
||||
animatedParent == null &&
|
||||
controller is ViewDialogLaunchAnimatorController &&
|
||||
controller.source !is LaunchableView
|
||||
) {
|
||||
// Make sure the View we launch from implements LaunchableView to avoid visibility
|
||||
// issues. Given that we don't own dialog decorViews so we can't enforce it for launches
|
||||
// from a dialog.
|
||||
// TODO(b/243636422): Throw instead of logging to enforce this.
|
||||
Log.w(
|
||||
TAG,
|
||||
"A dialog was launched from a View that does not implement LaunchableView. This " +
|
||||
"can lead to subtle bugs where the visibility of the View we are " +
|
||||
"launching from is not what we expected."
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure we don't run the launch animation from the same source twice at the same time.
|
||||
if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
|
||||
Log.e(
|
||||
@@ -613,10 +608,16 @@ private class AnimatedDialog(
|
||||
}
|
||||
|
||||
// Animate that view with the background. Throw if we didn't find one, because
|
||||
// otherwise
|
||||
// it's not clear what we should animate.
|
||||
// otherwise it's not clear what we should animate.
|
||||
if (viewGroupWithBackground == null) {
|
||||
error("Unable to find ViewGroup with background")
|
||||
}
|
||||
|
||||
if (viewGroupWithBackground !is LaunchableView) {
|
||||
error("The animated ViewGroup with background must implement LaunchableView")
|
||||
}
|
||||
|
||||
viewGroupWithBackground
|
||||
?: throw IllegalStateException("Unable to find ViewGroup with background")
|
||||
} else {
|
||||
// We will make the dialog window (and therefore its DecorView) fullscreen to make
|
||||
// it possible to animate outside its bounds.
|
||||
@@ -639,7 +640,7 @@ private class AnimatedDialog(
|
||||
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
)
|
||||
|
||||
val dialogContentWithBackground = FrameLayout(dialog.context)
|
||||
val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
|
||||
dialogContentWithBackground.background = decorView.background
|
||||
|
||||
// Make the window background transparent. Note that setting the window (or
|
||||
@@ -720,7 +721,10 @@ private class AnimatedDialog(
|
||||
|
||||
// Make the background view invisible until we start the animation. We use the transition
|
||||
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
|
||||
// b/204944038#comment17).
|
||||
// b/204944038#comment17). Given that this background implements LaunchableView, we call
|
||||
// setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is
|
||||
// restored at the end of the animation.
|
||||
dialogContentWithBackground.setShouldBlockVisibilityChanges(true)
|
||||
dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
|
||||
|
||||
// Make sure the dialog is visible instantly and does not do any window animation.
|
||||
|
||||
@@ -34,6 +34,7 @@ import android.view.ViewGroup
|
||||
import android.view.ViewGroupOverlay
|
||||
import android.widget.FrameLayout
|
||||
import com.android.internal.jank.InteractionJankMonitor
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.LinkedList
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
@@ -46,7 +47,8 @@ private const val TAG = "GhostedViewLaunchAnimatorController"
|
||||
* of the ghosted view.
|
||||
*
|
||||
* Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
|
||||
* the animation.
|
||||
* the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
|
||||
* during this controller instantiation.
|
||||
*
|
||||
* Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
|
||||
* whenever possible instead.
|
||||
@@ -101,6 +103,15 @@ constructor(
|
||||
private val background: Drawable?
|
||||
|
||||
init {
|
||||
// Make sure the View we launch from implements LaunchableView to avoid visibility issues.
|
||||
if (ghostedView !is LaunchableView) {
|
||||
throw IllegalArgumentException(
|
||||
"A GhostedViewLaunchAnimatorController was created from a View that does not " +
|
||||
"implement LaunchableView. This can lead to subtle bugs where the visibility " +
|
||||
"of the View we are launching from is not what we expected."
|
||||
)
|
||||
}
|
||||
|
||||
/** Find the first view with a background in [view] and its children. */
|
||||
fun findBackground(view: View): Drawable? {
|
||||
if (view.background != null) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.animation
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
|
||||
/** A [FrameLayout] that also implements [LaunchableView]. */
|
||||
open class LaunchableFrameLayout : FrameLayout, LaunchableView {
|
||||
private val delegate =
|
||||
LaunchableViewDelegate(
|
||||
this,
|
||||
superSetVisibility = { super.setVisibility(it) },
|
||||
)
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int,
|
||||
defStyleRes: Int
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||
delegate.setShouldBlockVisibilityChanges(block)
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
delegate.setVisibility(visibility)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import com.android.internal.jank.InteractionJankMonitor
|
||||
/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
|
||||
class ViewDialogLaunchAnimatorController
|
||||
internal constructor(
|
||||
internal val source: View,
|
||||
private val source: View,
|
||||
override val cuj: DialogCuj?,
|
||||
) : DialogLaunchAnimator.Controller {
|
||||
override val viewRoot: ViewRootImpl?
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:paddingVertical="@dimen/dream_overlay_complication_home_controls_padding">
|
||||
|
||||
<ImageView
|
||||
<com.android.systemui.common.ui.view.LaunchableImageView
|
||||
android:id="@+id/home_controls_chip"
|
||||
android:layout_height="@dimen/keyguard_affordance_fixed_height"
|
||||
android:layout_width="@dimen/keyguard_affordance_fixed_width"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_weight="1">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<com.android.systemui.common.ui.view.LaunchableConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@android:id/list"
|
||||
@@ -55,6 +55,6 @@
|
||||
app:flow_horizontalGap="@dimen/global_actions_lite_padding"
|
||||
app:flow_verticalGap="@dimen/global_actions_lite_padding"
|
||||
app:flow_horizontalStyle="packed"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.android.systemui.common.ui.view.LaunchableConstraintLayout>
|
||||
</com.android.systemui.globalactions.GlobalActionsLayoutLite>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
android:layout_gravity="end">
|
||||
<!-- We add a background behind the UserAvatarView with the same color and with a circular shape
|
||||
so that this view can be expanded into a Dialog or an Activity. -->
|
||||
<FrameLayout
|
||||
<com.android.systemui.animation.LaunchableFrameLayout
|
||||
android:id="@+id/kg_multi_user_avatar_with_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -42,5 +42,5 @@
|
||||
systemui:framePadding="0dp"
|
||||
systemui:frameWidth="0dp">
|
||||
</com.android.systemui.statusbar.phone.UserAvatarView>
|
||||
</FrameLayout>
|
||||
</com.android.systemui.animation.LaunchableFrameLayout>
|
||||
</FrameLayout>
|
||||
@@ -106,7 +106,7 @@
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
|
||||
app:layout_constraintHeight_min="@dimen/min_clickable_item_size">
|
||||
<LinearLayout
|
||||
<com.android.systemui.common.ui.view.LaunchableLinearLayout
|
||||
android:id="@+id/media_seamless_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -135,7 +135,7 @@
|
||||
android:textDirection="locale"
|
||||
android:textSize="12sp"
|
||||
android:lineHeight="16sp" />
|
||||
</LinearLayout>
|
||||
</com.android.systemui.common.ui.view.LaunchableLinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Song name -->
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:layout_marginStart="5dp"
|
||||
>
|
||||
<LinearLayout
|
||||
<com.android.systemui.common.ui.view.LaunchableLinearLayout
|
||||
android:id="@+id/ongoing_call_chip_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/ongoing_appops_chip_height"
|
||||
@@ -55,5 +55,5 @@
|
||||
android:textColor="?android:attr/colorPrimary"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
</com.android.systemui.common.ui.view.LaunchableLinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.common.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.android.systemui.animation.LaunchableView
|
||||
import com.android.systemui.animation.LaunchableViewDelegate
|
||||
|
||||
/** A [ConstraintLayout] that also implements [LaunchableView]. */
|
||||
open class LaunchableConstraintLayout : ConstraintLayout, LaunchableView {
|
||||
private val delegate =
|
||||
LaunchableViewDelegate(
|
||||
this,
|
||||
superSetVisibility = { super.setVisibility(it) },
|
||||
)
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int,
|
||||
defStyleRes: Int
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||
delegate.setShouldBlockVisibilityChanges(block)
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
delegate.setVisibility(visibility)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import com.android.systemui.animation.LaunchableView
|
||||
import com.android.systemui.animation.LaunchableViewDelegate
|
||||
|
||||
/** A [LinearLayout] that also implements [LaunchableView]. */
|
||||
class LaunchableLinearLayout : LinearLayout, LaunchableView {
|
||||
open class LaunchableLinearLayout : LinearLayout, LaunchableView {
|
||||
private val delegate =
|
||||
LaunchableViewDelegate(
|
||||
this,
|
||||
|
||||
@@ -17,11 +17,11 @@ package com.android.systemui.privacy
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.LaunchableFrameLayout
|
||||
import com.android.systemui.statusbar.events.BackgroundAnimatableView
|
||||
|
||||
class OngoingPrivacyChip @JvmOverloads constructor(
|
||||
@@ -29,7 +29,7 @@ class OngoingPrivacyChip @JvmOverloads constructor(
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttrs: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
|
||||
) : LaunchableFrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
|
||||
|
||||
private var iconMargin = 0
|
||||
private var iconSize = 0
|
||||
|
||||
@@ -20,10 +20,21 @@ import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.android.systemui.animation.LaunchableView;
|
||||
import com.android.systemui.animation.LaunchableViewDelegate;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* A Button which doesn't have overlapping drawing commands
|
||||
*/
|
||||
public class AlphaOptimizedButton extends Button {
|
||||
public class AlphaOptimizedButton extends Button implements LaunchableView {
|
||||
private LaunchableViewDelegate mDelegate = new LaunchableViewDelegate(this,
|
||||
(visibility) -> {
|
||||
super.setVisibility(visibility);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
public AlphaOptimizedButton(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -45,4 +56,14 @@ public class AlphaOptimizedButton extends Button {
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShouldBlockVisibilityChanges(boolean block) {
|
||||
mDelegate.setShouldBlockVisibilityChanges(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(int visibility) {
|
||||
mDelegate.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ package com.android.systemui.statusbar.phone.userswitcher
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.common.ui.view.LaunchableLinearLayout
|
||||
|
||||
class StatusBarUserSwitcherContainer(
|
||||
context: Context?,
|
||||
attrs: AttributeSet?
|
||||
) : LinearLayout(context, attrs) {
|
||||
) : LaunchableLinearLayout(context, attrs) {
|
||||
lateinit var text: TextView
|
||||
private set
|
||||
lateinit var avatar: ImageView
|
||||
|
||||
@@ -27,6 +27,8 @@ import android.view.ViewTreeObserver
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import com.android.systemui.animation.LaunchableView
|
||||
import com.android.systemui.animation.LaunchableViewDelegate
|
||||
import com.android.systemui.statusbar.CrossFadeHelper
|
||||
|
||||
/**
|
||||
@@ -38,7 +40,7 @@ class TransitionLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), LaunchableView {
|
||||
|
||||
private val boundsRect = Rect()
|
||||
private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
|
||||
@@ -50,7 +52,11 @@ class TransitionLayout @JvmOverloads constructor(
|
||||
|
||||
private var desiredMeasureWidth = 0
|
||||
private var desiredMeasureHeight = 0
|
||||
private var transitionVisibility = View.VISIBLE
|
||||
private val delegate =
|
||||
LaunchableViewDelegate(
|
||||
this,
|
||||
superSetVisibility = { super.setVisibility(it) },
|
||||
)
|
||||
|
||||
/**
|
||||
* The measured state of this view which is the one we will lay ourselves out with. This
|
||||
@@ -83,11 +89,12 @@ class TransitionLayout @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTransitionVisibility(visibility: Int) {
|
||||
// We store the last transition visibility assigned to this view to restore it later if
|
||||
// necessary.
|
||||
super.setTransitionVisibility(visibility)
|
||||
transitionVisibility = visibility
|
||||
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||
delegate.setShouldBlockVisibilityChanges(block)
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
delegate.setVisibility(visibility)
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
@@ -173,14 +180,6 @@ class TransitionLayout @JvmOverloads constructor(
|
||||
translationY = currentState.translation.y
|
||||
|
||||
CrossFadeHelper.fadeIn(this, currentState.alpha)
|
||||
|
||||
// CrossFadeHelper#fadeIn will change this view visibility, which overrides the transition
|
||||
// visibility. We set the transition visibility again to make sure that this view plays well
|
||||
// with GhostView, which sets the transition visibility and is used for activity launch
|
||||
// animations.
|
||||
if (transitionVisibility != View.VISIBLE) {
|
||||
setTransitionVisibility(transitionVisibility)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyCurrentStateOnPredraw() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import android.view.RemoteAnimationAdapter
|
||||
import android.view.RemoteAnimationTarget
|
||||
import android.view.SurfaceControl
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
@@ -26,6 +27,7 @@ import junit.framework.Assert.assertTrue
|
||||
import junit.framework.AssertionFailedError
|
||||
import kotlin.concurrent.thread
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -195,6 +197,13 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
verify(controller).onLaunchAnimationStart(anyBoolean())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun creatingControllerFromNormalViewThrows() {
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
ActivityLaunchAnimator.Controller.fromView(FrameLayout(mContext))
|
||||
}
|
||||
}
|
||||
|
||||
private fun fakeWindow(): RemoteAnimationTarget {
|
||||
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
||||
val taskInfo = ActivityManager.RunningTaskInfo()
|
||||
|
||||
@@ -26,6 +26,7 @@ import junit.framework.Assert.assertNull
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -260,6 +261,13 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
assertThat(touchSurface.visibility).isEqualTo(View.GONE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun creatingControllerFromNormalViewThrows() {
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
DialogLaunchAnimator.Controller.fromView(FrameLayout(mContext))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAndShowDialog(
|
||||
animator: DialogLaunchAnimator = dialogLaunchAnimator,
|
||||
): TestDialog {
|
||||
|
||||
@@ -16,58 +16,34 @@
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewParent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.jank.InteractionJankMonitor
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import org.junit.Before
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
|
||||
@Mock lateinit var interactionJankMonitor: InteractionJankMonitor
|
||||
@Mock lateinit var view: View
|
||||
@Mock lateinit var rootView: ViewGroup
|
||||
@Mock lateinit var viewParent: ViewParent
|
||||
@Mock lateinit var drawable: Drawable
|
||||
lateinit var controller: GhostedViewLaunchAnimatorController
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
whenever(view.rootView).thenReturn(rootView)
|
||||
whenever(view.background).thenReturn(drawable)
|
||||
whenever(view.height).thenReturn(0)
|
||||
whenever(view.width).thenReturn(0)
|
||||
whenever(view.parent).thenReturn(viewParent)
|
||||
whenever(view.visibility).thenReturn(View.VISIBLE)
|
||||
whenever(view.invalidate()).then { /* NO-OP */ }
|
||||
whenever(view.getLocationOnScreen(any())).then { /* NO-OP */ }
|
||||
whenever(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
|
||||
whenever(interactionJankMonitor.end(anyInt())).thenReturn(true)
|
||||
controller = GhostedViewLaunchAnimatorController(view, 0, interactionJankMonitor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animatingOrphanViewDoesNotCrash() {
|
||||
val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
|
||||
|
||||
val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext))
|
||||
controller.onIntentStarted(willAnimate = true)
|
||||
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
|
||||
controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
|
||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun creatingControllerFromNormalViewThrows() {
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
GhostedViewLaunchAnimatorController(FrameLayout(mContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,13 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.internal.logging.UiEventLogger;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.common.ui.view.LaunchableImageView;
|
||||
import com.android.systemui.controls.ControlsServiceInfo;
|
||||
import com.android.systemui.controls.controller.ControlsController;
|
||||
import com.android.systemui.controls.controller.StructureInfo;
|
||||
@@ -90,7 +90,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase {
|
||||
private View mView;
|
||||
|
||||
@Mock
|
||||
private ImageView mHomeControlsView;
|
||||
private LaunchableImageView mHomeControlsView;
|
||||
|
||||
@Mock
|
||||
private ActivityStarter mActivityStarter;
|
||||
|
||||
@@ -35,6 +35,7 @@ import android.view.View
|
||||
import com.android.internal.logging.MetricsLogger
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.LaunchableFrameLayout
|
||||
import com.android.systemui.classifier.FalsingManagerFake
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.plugins.qs.QSTile
|
||||
@@ -339,7 +340,7 @@ class CustomTileTest : SysuiTestCase() {
|
||||
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
|
||||
tile.qsTile.activityLaunchForClick = pi
|
||||
|
||||
tile.handleClick(mock(View::class.java))
|
||||
tile.handleClick(mock(LaunchableFrameLayout::class.java))
|
||||
|
||||
testableLooper.processAllMessages()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user