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:
@@ -48,5 +48,6 @@
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:tint="@color/media_primary_text"
|
||||
android:forceHasOverlappingRendering="false"
|
||||
/>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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(() -> {
|
||||
|
||||
Reference in New Issue
Block a user