Merge "Making transitions between gone hosts work better" into rvc-dev am: acd4508f46

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

Change-Id: I0d713ea0032777e9a9e1b0eb8ee2480d6be32256
This commit is contained in:
TreeHugger Robot
2020-06-26 06:29:19 +00:00
committed by Automerger Merge Worker
11 changed files with 387 additions and 131 deletions

View File

@@ -48,5 +48,6 @@
android:layout_height="48dp"
android:layout_marginBottom="4dp"
android:tint="@color/media_primary_text"
android:forceHasOverlappingRendering="false"
/>
</FrameLayout>

View File

@@ -24,6 +24,7 @@
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_horizontal|fill_vertical"
android:forceHasOverlappingRendering="false"
android:background="@drawable/qs_media_background">
<!-- As per Material Design on Biderectionality, this is forced to LTR in code -->

View File

@@ -5,6 +5,7 @@ import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
import android.util.MathUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -328,9 +329,32 @@ class MediaCarouselController @Inject constructor(
updatePlayerToState(mediaPlayer, immediately)
}
maybeResetSettingsCog()
updatePageIndicatorAlpha()
}
}
private fun updatePageIndicatorAlpha() {
val hostStates = mediaHostStatesManager.mediaHostStates
val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
val startAlpha = if (startIsVisible) 1.0f else 0.0f
val endAlpha = if (endIsVisible) 1.0f else 0.0f
var alpha = 1.0f
if (!endIsVisible || !startIsVisible) {
var progress = currentTransitionProgress
if (!endIsVisible) {
progress = 1.0f - progress
}
// Let's fade in quickly at the end where the view is visible
progress = MathUtils.constrain(
MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress),
0.0f,
1.0f)
alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
}
pageIndicator.alpha = alpha
}
private fun updatePageIndicatorLocation() {
// Update the location of the page indicator, carousel clipping
val translationX = if (isRtl) {
@@ -352,8 +376,10 @@ class MediaCarouselController @Inject constructor(
var height = 0
for (mediaPlayer in mediaPlayers.values) {
val controller = mediaPlayer.mediaViewController
width = Math.max(width, controller.currentWidth)
height = Math.max(height, controller.currentHeight)
// When transitioning the view to gone, the view gets smaller, but the translation
// Doesn't, let's add the translation
width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
}
if (width != currentCarouselWidth || height != currentCarouselHeight) {
currentCarouselWidth = width

View File

@@ -329,7 +329,7 @@ class MediaHierarchyManager @Inject constructor(
applyTargetStateIfNotAnimating()
} else if (animate) {
animator.cancel()
if (currentAttachmentLocation == IN_OVERLAY ||
if (currentAttachmentLocation != previousLocation ||
!previousHost.hostView.isAttachedToWindow) {
// Let's animate to the new position, starting from the current position
// We also go in here in case the view was detached, since the bounds wouldn't
@@ -341,10 +341,12 @@ class MediaHierarchyManager @Inject constructor(
animationStartBounds.set(previousHost.currentBounds)
}
adjustAnimatorForTransition(desiredLocation, previousLocation)
rootView?.let {
// Let's delay the animation start until we finished laying out
animationPending = true
it.postOnAnimation(startAnimation)
if (!animationPending) {
rootView?.let {
// Let's delay the animation start until we finished laying out
animationPending = true
it.postOnAnimation(startAnimation)
}
}
} else {
cancelAnimationAndApplyDesiredState()
@@ -403,15 +405,23 @@ class MediaHierarchyManager @Inject constructor(
}
/**
* Updates the state that the view wants to be in at the end of the animation.
* Updates the bounds that the view wants to be in at the end of the animation.
*/
private fun updateTargetState() {
if (isCurrentlyInGuidedTransformation()) {
val progress = getTransformationProgress()
val currentHost = getHost(desiredLocation)!!
val previousHost = getHost(previousLocation)!!
val newBounds = currentHost.currentBounds
val previousBounds = previousHost.currentBounds
var endHost = getHost(desiredLocation)!!
var starthost = getHost(previousLocation)!!
// If either of the hosts are invisible, let's keep them at the other host location to
// have a nicer disappear animation. Otherwise the currentBounds of the state might
// be undefined
if (!endHost.visible) {
endHost = starthost
} else if (!starthost.visible) {
starthost = endHost
}
val newBounds = endHost.currentBounds
val previousBounds = starthost.currentBounds
targetBounds = interpolateBounds(previousBounds, newBounds, progress)
} else {
val bounds = getHost(desiredLocation)?.currentBounds ?: return
@@ -462,7 +472,9 @@ class MediaHierarchyManager @Inject constructor(
val previousHost = getHost(previousLocation)
if (currentHost?.location == LOCATION_QS) {
if (previousHost?.location == LOCATION_QQS) {
return qsExpansion
if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
return qsExpansion
}
}
}
return -1.0f

View File

@@ -5,6 +5,7 @@ import android.graphics.Rect
import android.util.ArraySet
import android.view.View
import android.view.View.OnAttachStateChangeListener
import com.android.systemui.util.animation.DisappearParameters
import com.android.systemui.util.animation.MeasurementInput
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.UniqueObjectHostView
@@ -128,8 +129,6 @@ class MediaHost @Inject constructor(
}
class MediaHostStateHolder @Inject constructor() : MediaHostState {
private var gonePivot: PointF = PointF()
override var measurementInput: MeasurementInput? = null
set(value) {
if (value?.equals(field) != true) {
@@ -172,15 +171,18 @@ class MediaHost @Inject constructor(
changedListener?.invoke()
}
override fun getPivotX(): Float = gonePivot.x
override fun getPivotY(): Float = gonePivot.y
override fun setGonePivot(x: Float, y: Float) {
if (gonePivot.equals(x, y)) {
return
override var disappearParameters: DisappearParameters = DisappearParameters()
set(value) {
val newHash = value.hashCode()
if (lastDisappearHash.equals(newHash)) {
return
}
field = value
lastDisappearHash = newHash
changedListener?.invoke()
}
gonePivot.set(x, y)
changedListener?.invoke()
}
private var lastDisappearHash = disappearParameters.hashCode()
/**
* A listener for all changes. This won't be copied over when invoking [copy]
@@ -196,7 +198,7 @@ class MediaHost @Inject constructor(
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible
mediaHostState.gonePivot.set(gonePivot)
mediaHostState.disappearParameters = disappearParameters.deepCopy()
mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
return mediaHostState
}
@@ -220,7 +222,7 @@ class MediaHost @Inject constructor(
if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
return false
}
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
if (!disappearParameters.equals(other.disappearParameters)) {
return false
}
return true
@@ -232,12 +234,23 @@ class MediaHost @Inject constructor(
result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2
result = 31 * result + gonePivot.hashCode()
result = 31 * result + disappearParameters.hashCode()
return result
}
}
}
/**
* A description of a media host state that describes the behavior whenever the media carousel
* is hosted. The HostState notifies the media players of changes to their properties, who
* in turn will create view states from it.
* When adding a new property to this, make sure to update the listener and notify them
* about the changes.
* In case you need to have a different rendering based on the state, you can add a new
* constraintState to the [MediaViewController]. Otherwise, similar host states will resolve
* to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update
* that key if the underlying view needs to have a different measurement.
*/
interface MediaHostState {
/**
@@ -268,23 +281,11 @@ interface MediaHostState {
var falsingProtectionNeeded: Boolean
/**
* Sets the pivot point when clipping the height or width.
* Clipping happens when animating visibility when we're visible in QS but not on QQS,
* for example.
* The parameters how the view disappears from this location when going to a host that's not
* visible. If modified, make sure to set this value again on the host to ensure the values
* are propagated
*/
fun setGonePivot(x: Float, y: Float)
/**
* x position of pivot, from 0 to 1
* @see [setGonePivot]
*/
fun getPivotX(): Float
/**
* y position of pivot, from 0 to 1
* @see [setGonePivot]
*/
fun getPivotY(): Float
var disappearParameters: DisappearParameters
/**
* Get a copy of this view state, deepcopying all appropriate members

View File

@@ -18,7 +18,6 @@ package com.android.systemui.media
import android.content.Context
import android.content.res.Configuration
import android.graphics.PointF
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.R
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -53,7 +52,7 @@ class MediaViewController @Inject constructor(
/**
* A map containing all viewStates for all locations of this mediaState
*/
private val viewStates: MutableMap<MediaHostState, TransitionViewState?> = mutableMapOf()
private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
/**
* The ending location of the view where it ends when all animations and transitions have
@@ -80,9 +79,9 @@ class MediaViewController @Inject constructor(
private val tmpState = TransitionViewState()
/**
* Temporary variable to avoid unnecessary allocations.
* A temporary cache key to be used to look up cache entries
*/
private val tmpPoint = PointF()
private val tmpKey = CacheKey()
/**
* The current width of the player. This might not factor in case the player is animating
@@ -95,6 +94,24 @@ class MediaViewController @Inject constructor(
*/
var currentHeight: Int = 0
/**
* Get the translationX of the layout
*/
var translationX: Float = 0.0f
private set
get() {
return transitionLayout?.translationX ?: 0.0f
}
/**
* Get the translationY of the layout
*/
var translationY: Float = 0.0f
private set
get() {
return transitionLayout?.translationY ?: 0.0f
}
/**
* A callback for RTL config changes
*/
@@ -179,15 +196,21 @@ class MediaViewController @Inject constructor(
* Obtain a new viewState for a given media state. This usually returns a cached state, but if
* it's not available, it will recreate one by measuring, which may be expensive.
*/
private fun obtainViewState(state: MediaHostState): TransitionViewState? {
val viewState = viewStates[state]
private fun obtainViewState(state: MediaHostState?): TransitionViewState? {
if (state == null || state.measurementInput == null) {
return null
}
// Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
var cacheKey = getKey(state, tmpKey)
val viewState = viewStates[cacheKey]
if (viewState != null) {
// we already have cached this measurement, let's continue
return viewState
}
// Copy the key since this might call recursively into it and we're using tmpKey
cacheKey = cacheKey.copy()
val result: TransitionViewState?
if (transitionLayout != null && state.measurementInput != null) {
if (transitionLayout != null) {
// Let's create a new measurement
if (state.expansion == 0.0f || state.expansion == 1.0f) {
result = transitionLayout!!.calculateViewState(
@@ -198,7 +221,7 @@ class MediaViewController @Inject constructor(
// We don't want to cache interpolated or null states as this could quickly fill up
// our cache. We only cache the start and the end states since the interpolation
// is cheap
viewStates[state.copy()] = result
viewStates[cacheKey] = result
} else {
// This is an interpolated state
val startState = state.copy().also { it.expansion = 0.0f }
@@ -208,14 +231,10 @@ class MediaViewController @Inject constructor(
val startViewState = obtainViewState(startState) as TransitionViewState
val endState = state.copy().also { it.expansion = 1.0f }
val endViewState = obtainViewState(endState) as TransitionViewState
tmpPoint.set(startState.getPivotX(), startState.getPivotY())
result = TransitionViewState()
layoutController.getInterpolatedState(
result = layoutController.getInterpolatedState(
startViewState,
endViewState,
state.expansion,
tmpPoint,
result)
state.expansion)
}
} else {
result = null
@@ -223,6 +242,15 @@ class MediaViewController @Inject constructor(
return result
}
private fun getKey(state: MediaHostState, result: CacheKey): CacheKey {
result.apply {
heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
expansion = state.expansion
}
return result
}
/**
* Attach a view to this controller. This may perform measurements if it's not available yet
* and should therefore be done carefully.
@@ -270,65 +298,54 @@ class MediaViewController @Inject constructor(
val shouldAnimate = animateNextStateChange && !applyImmediately
var startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
var endHostState = mediaHostStatesManager.mediaHostStates[endLocation]
var swappedStartState = false
var swappedEndState = false
// if we're going from or to a non visible state, let's grab the visible one and animate
// the view being clipped instead.
if (endHostState?.visible != true) {
endHostState = startHostState
swappedEndState = true
}
if (startHostState?.visible != true) {
startHostState = endHostState
swappedStartState = true
}
if (startHostState == null || endHostState == null) {
return
}
var endViewState = obtainViewState(endHostState) ?: return
if (swappedEndState) {
endViewState = endViewState.copy()
endViewState.height = 0
}
val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
// Obtain the view state that we'd want to be at the end
// The view might not be bound yet or has never been measured and in that case will be
// reset once the state is fully available
val endViewState = obtainViewState(endHostState) ?: return
layoutController.setMeasureState(endViewState)
// If the view isn't bound, we can drop the animation, otherwise we'll executute it
// If the view isn't bound, we can drop the animation, otherwise we'll execute it
animateNextStateChange = false
if (transitionLayout == null) {
return
}
var startViewState = obtainViewState(startHostState)
if (swappedStartState) {
startViewState = startViewState?.copy()
startViewState?.height = 0
}
val result: TransitionViewState
val startViewState = obtainViewState(startHostState)
val result: TransitionViewState?
result = if (transitionProgress == 1.0f || startViewState == null) {
endViewState
} else if (transitionProgress == 0.0f) {
startViewState
} else {
if (swappedEndState || swappedStartState) {
tmpPoint.set(startHostState.getPivotX(), startHostState.getPivotY())
if (!endHostState.visible) {
// Let's handle the case where the end is gone first. In this case we take the
// start viewState and will make it gone
if (startViewState == null || startHostState == null || !startHostState.visible) {
// the start isn't a valid state, let's use the endstate directly
result = endViewState
} else {
tmpPoint.set(0.0f, 0.0f)
// Let's get the gone presentation from the start state
result = layoutController.getGoneState(startViewState,
startHostState.disappearParameters,
transitionProgress,
tmpState)
}
layoutController.getInterpolatedState(startViewState, endViewState, transitionProgress,
tmpPoint, tmpState)
tmpState
} else if (startHostState != null && !startHostState.visible) {
// We have a start state and it is gone.
// Let's get presentation from the endState
result = layoutController.getGoneState(endViewState, endHostState.disappearParameters,
1.0f - transitionProgress,
tmpState)
} else if (transitionProgress == 1.0f || startViewState == null) {
// We're at the end. Let's use that state
result = endViewState
} else if (transitionProgress == 0.0f) {
// We're at the start. Let's use that state
result = startViewState
} else {
result = layoutController.getInterpolatedState(startViewState, endViewState,
transitionProgress, tmpState)
}
currentWidth = result.width
currentHeight = result.height
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
animationDelay)
}
@@ -379,3 +396,12 @@ class MediaViewController @Inject constructor(
firstRefresh = false
}
}
/**
* An internal key for the cache of mediaViewStates. This is a subset of the full host state.
*/
private data class CacheKey(
var widthMeasureSpec: Int = -1,
var heightMeasureSpec: Int = -1,
var expansion: Float = 0.0f
)

View File

@@ -25,6 +25,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.PointF;
import android.metrics.LogMaker;
import android.os.Bundle;
import android.os.Handler;
@@ -60,6 +61,7 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.animation.DisappearParameters;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -246,11 +248,40 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
protected void initMediaHostState() {
mMediaHost.setExpansion(1.0f);
mMediaHost.setShowsOnlyActiveMedia(false);
// Reveal player with some parallax (1.0f would also work)
mMediaHost.setGonePivot(0.0f, 0.8f);
updateMediaDisappearParameters();
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
}
/**
* Update the way the media disappears based on if we're using the horizontal layout
*/
private void updateMediaDisappearParameters() {
if (!mUsingMediaPlayer) {
return;
}
DisappearParameters parameters = mMediaHost.getDisappearParameters();
if (mUsingHorizontalLayout) {
// Only height remaining
parameters.getDisappearSize().set(0.0f, 0.4f);
// Disappearing on the right side on the bottom
parameters.getGonePivot().set(1.0f, 1.0f);
// translating a bit horizontal
parameters.getContentTranslationFraction().set(0.25f, 1.0f);
parameters.setDisappearEnd(0.6f);
} else {
// Only width remaining
parameters.getDisappearSize().set(1.0f, 0.0f);
// Disappearing on the bottom
parameters.getGonePivot().set(0.0f, 1.0f);
// translating a bit vertical
parameters.getContentTranslationFraction().set(0.0f, 1.05f);
parameters.setDisappearEnd(0.95f);
}
parameters.setFadeStartPosition(0.95f);
parameters.setDisappearStart(0.0f);
mMediaHost.setDisappearParameters(parameters);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mTileLayout instanceof PagedTileLayout) {
@@ -542,6 +573,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
updateTileLayoutMargins();
updateFooterMargin();
updateDividerMargin();
updateMediaDisappearParameters();
updateMediaHostContentMargins();
updateHorizontalLinearLayoutMargins();
updatePadding();

View File

@@ -80,6 +80,8 @@ class TransitionLayout @JvmOverloads constructor(
*/
private fun applyCurrentState() {
val childCount = childCount
val contentTranslationX = currentState.contentTranslation.x.toInt()
val contentTranslationY = currentState.contentTranslation.y.toInt()
for (i in 0 until childCount) {
val child = getChildAt(i)
val widgetState = currentState.widgetStates.get(child.id) ?: continue
@@ -92,8 +94,8 @@ class TransitionLayout @JvmOverloads constructor(
child.measure(measureWidthSpec, measureHeightSpec)
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
}
val left = widgetState.x.toInt()
val top = widgetState.y.toInt()
val left = widgetState.x.toInt() + contentTranslationX
val top = widgetState.y.toInt() + contentTranslationY
child.setLeftTopRightBottom(left, top, left + widgetState.width,
top + widgetState.height)
child.scaleX = widgetState.scale
@@ -109,6 +111,9 @@ class TransitionLayout @JvmOverloads constructor(
}
}
updateBounds()
translationX = currentState.translation.x
translationY = currentState.translation.y
CrossFadeHelper.fadeIn(this, currentState.alpha)
}
private fun applyCurrentStateOnPredraw() {
@@ -161,9 +166,7 @@ class TransitionLayout @JvmOverloads constructor(
val layoutTop = top
setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
layoutTop + currentState.height)
translationX = currentState.translation.x
translationY = currentState.translation.y
boundsRect.set(0, 0, (width + translationX).toInt(), (height + translationY).toInt())
boundsRect.set(0, 0, width.toInt(), height.toInt())
}
/**
@@ -247,13 +250,17 @@ class TransitionViewState {
var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
var width: Int = 0
var height: Int = 0
var alpha: Float = 1.0f
val translation = PointF()
val contentTranslation = PointF()
fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
// we need a deep copy of this, so we can't use a data class
val copy = reusedState ?: TransitionViewState()
copy.width = width
copy.height = height
copy.alpha = alpha
copy.translation.set(translation.x, translation.y)
copy.contentTranslation.set(contentTranslation.x, contentTranslation.y)
for (entry in widgetStates) {
copy.widgetStates[entry.key] = entry.value.copy()
}
@@ -272,6 +279,8 @@ class TransitionViewState {
width = transitionLayout.measuredWidth
height = transitionLayout.measuredHeight
translation.set(0.0f, 0.0f)
contentTranslation.set(0.0f, 0.0f)
alpha = 1.0f
}
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.util.animation
import android.animation.ValueAnimator
import android.graphics.PointF
import android.util.MathUtils
import com.android.internal.R.attr.width
import com.android.systemui.Interpolators
/**
@@ -44,7 +45,6 @@ open class TransitionLayoutController {
private var currentState = TransitionViewState()
private var animationStartState: TransitionViewState? = null
private var state = TransitionViewState()
private var pivot = PointF()
private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
private var currentHeight: Int = 0
private var currentWidth: Int = 0
@@ -63,13 +63,11 @@ open class TransitionLayoutController {
if (animationStartState == null || !animator.isRunning) {
return
}
val view = transitionLayout ?: return
getInterpolatedState(
currentState = getInterpolatedState(
startState = animationStartState!!,
endState = state,
progress = animator.animatedFraction,
pivot = pivot,
resultState = currentState)
reusedState = currentState)
applyStateToLayout(currentState)
}
@@ -82,6 +80,49 @@ open class TransitionLayoutController {
}
}
/**
* Obtain a state that is gone, based on parameters given.
*
* @param viewState the viewState to make gone
* @param disappearParameters parameters that determine how the view should disappear
* @param goneProgress how much is the view gone? 0 for not gone at all and 1 for fully
* disappeared
* @param reusedState optional parameter for state to be reused to avoid allocations
*/
fun getGoneState(
viewState: TransitionViewState,
disappearParameters: DisappearParameters,
goneProgress: Float,
reusedState: TransitionViewState? = null
): TransitionViewState {
var remappedProgress = MathUtils.map(
disappearParameters.disappearStart,
disappearParameters.disappearEnd,
0.0f, 1.0f,
goneProgress)
remappedProgress = MathUtils.constrain(remappedProgress, 0.0f, 1.0f)
val result = viewState.copy(reusedState).apply {
width = MathUtils.lerp(
viewState.width.toFloat(),
viewState.width * disappearParameters.disappearSize.x,
remappedProgress).toInt()
height = MathUtils.lerp(
viewState.height.toFloat(),
viewState.height * disappearParameters.disappearSize.y,
remappedProgress).toInt()
translation.x = (viewState.width - width) * disappearParameters.gonePivot.x
translation.y = (viewState.height - height) * disappearParameters.gonePivot.y
contentTranslation.x = (disappearParameters.contentTranslationFraction.x - 1.0f) *
translation.x
contentTranslation.y = (disappearParameters.contentTranslationFraction.y - 1.0f) *
translation.y
val alphaProgress = MathUtils.map(
disappearParameters.fadeStartPosition, 1.0f, 1.0f, 0.0f, remappedProgress)
alpha = MathUtils.constrain(alphaProgress, 0.0f, 1.0f)
}
return result
}
/**
* Get an interpolated state between two viewstates. This interpolates all positions for all
* widgets as well as it's bounds based on the given input.
@@ -90,11 +131,10 @@ open class TransitionLayoutController {
startState: TransitionViewState,
endState: TransitionViewState,
progress: Float,
pivot: PointF,
resultState: TransitionViewState
) {
this.pivot.set(pivot)
val view = transitionLayout ?: return
reusedState: TransitionViewState? = null
): TransitionViewState {
val resultState = reusedState ?: TransitionViewState()
val view = transitionLayout ?: return resultState
val childCount = view.childCount
for (i in 0 until childCount) {
val id = view.getChildAt(i).id
@@ -195,9 +235,21 @@ open class TransitionLayoutController {
progress).toInt()
height = MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(),
progress).toInt()
translation.x = (endState.width - width) * pivot.x
translation.y = (endState.height - height) * pivot.y
translation.x = MathUtils.lerp(startState.translation.x, endState.translation.x,
progress)
translation.y = MathUtils.lerp(startState.translation.y, endState.translation.y,
progress)
alpha = MathUtils.lerp(startState.alpha, endState.alpha, progress)
contentTranslation.x = MathUtils.lerp(
startState.contentTranslation.x,
endState.contentTranslation.x,
progress)
contentTranslation.y = MathUtils.lerp(
startState.contentTranslation.y,
endState.contentTranslation.y,
progress)
}
return resultState
}
fun attach(transitionLayout: TransitionLayout) {
@@ -250,3 +302,97 @@ open class TransitionLayoutController {
transitionLayout?.measureState = state
}
}
class DisappearParameters() {
/**
* The pivot point when clipping view when disappearing, which describes how the content will
* be translated.
* The default value of (0.0f, 1.0f) means that the view will not be translated in horizontally
* and the vertical disappearing will be aligned on the bottom of the view,
*/
var gonePivot = PointF(0.0f, 1.0f)
/**
* The fraction of the width and height that will remain when disappearing. The default of
* (1.0f, 0.0f) means that 100% of the width, but 0% of the height will remain at the end of
* the transition.
*/
var disappearSize = PointF(1.0f, 0.0f)
/**
* The fraction of the normal translation, by which the content will be moved during the
* disappearing. The values here can be both negative as well as positive. The default value
* of (0.0f, 0.2f) means that the content doesn't move horizontally but moves 20% of the
* translation imposed by the pivot downwards. 1.0f means that the content will be translated
* in sync with the translation of the bounds
*/
var contentTranslationFraction = PointF(0.0f, 0.8f)
/**
* The point during the progress from [0.0, 1.0f] where the view is fully appeared. 0.0f
* means that the content will start disappearing immediately, while 0.5f means that it
* starts disappearing half way through the progress.
*/
var disappearStart = 0.0f
/**
* The point during the progress from [0.0, 1.0f] where the view has fully disappeared. 1.0f
* means that the view will disappear in sync with the progress, while 0.5f means that it
* is fully gone half way through the progress.
*/
var disappearEnd = 1.0f
/**
* The point during the mapped progress from [0.0, 1.0f] where the view starts fading out. 1.0f
* means that the view doesn't fade at all, while 0.5 means that the content fades starts
* fading at the midpoint between [disappearStart] and [disappearEnd]
*/
var fadeStartPosition = 0.9f
override fun equals(other: Any?): Boolean {
if (!(other is DisappearParameters)) {
return false
}
if (!disappearSize.equals(other.disappearSize)) {
return false
}
if (!gonePivot.equals(other.gonePivot)) {
return false
}
if (!contentTranslationFraction.equals(other.contentTranslationFraction)) {
return false
}
if (disappearStart != other.disappearStart) {
return false
}
if (disappearEnd != other.disappearEnd) {
return false
}
if (fadeStartPosition != other.fadeStartPosition) {
return false
}
return true
}
override fun hashCode(): Int {
var result = disappearSize.hashCode()
result = 31 * result + gonePivot.hashCode()
result = 31 * result + contentTranslationFraction.hashCode()
result = 31 * result + disappearStart.hashCode()
result = 31 * result + disappearEnd.hashCode()
result = 31 * result + fadeStartPosition.hashCode()
return result
}
fun deepCopy(): DisappearParameters {
val result = DisappearParameters()
result.disappearSize.set(disappearSize)
result.gonePivot.set(gonePivot)
result.contentTranslationFraction.set(contentTranslationFraction)
result.disappearStart = disappearStart
result.disappearEnd = disappearEnd
result.fadeStartPosition = fadeStartPosition
return result
}
}

View File

@@ -53,20 +53,20 @@ class UniqueObjectHostView(
// size.
val (cachedWidth, cachedHeight) = measurementManager.onMeasure(measurementInput)
if (!isCurrentHost()) {
// We're not currently the host, let's use the dimension from our cache
// The goal here is that the view will always have a consistent measuring, regardless
// if it's attached or not.
// The behavior is therefore very similar to the view being persistently attached to
// this host, which can prevent flickers. It also makes sure that we always know
// the size of the view during transitions even if it has never been attached here
// before.
setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
} else {
if (isCurrentHost()) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Let's update our cache
getChildAt(0)?.requiresRemeasuring = false
}
// The goal here is that the view will always have a consistent measuring, regardless
// if it's attached or not.
// The behavior is therefore very similar to the view being persistently attached to
// this host, which can prevent flickers. It also makes sure that we always know
// the size of the view during transitions even if it has never been attached here
// before.
// We previously still measured the size when the view was attached, but this doesn't
// work properly because we can set the measuredState while still attached to the
// old host, which will trigger an inconsistency in height
setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {

View File

@@ -51,6 +51,7 @@ import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.policy.SecurityController;
import com.android.systemui.util.animation.DisappearParameters;
import com.android.systemui.util.animation.UniqueObjectHostView;
import org.junit.Before;
@@ -110,6 +111,7 @@ public class QSPanelTest extends SysuiTestCase {
mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
mContext.addMockSystemService(Context.USER_SERVICE, mock(UserManager.class));
when(mMediaHost.getHostView()).thenReturn(new UniqueObjectHostView(getContext()));
when(mMediaHost.getDisappearParameters()).thenReturn(new DisappearParameters());
mUiEventLogger = new UiEventLoggerFake();
mTestableLooper.runWithLooper(() -> {