update uiunfold

This commit is contained in:
MrSluffy committed 2024-10-06 13:34:50 +08:00
1 parent 7571d54a1a
commit f751457b95
41 files changed
+4460 -1919

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace "androidx.dynamicanimation.animation"
sourceSets {
main {
java.srcDirs = ['src']
}
}
}
addFrameworkJar('framework-14.jar')
compileOnlyCommonJars()
@@ -0,0 +1,296 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import android.animation.ValueAnimator;
import android.os.Build;
import android.os.Looper;
import android.os.SystemClock;
import android.view.Choreographer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.collection.SimpleArrayMap;
import java.util.ArrayList;
/**
* This custom handler handles the timing pulse that is shared by all active ValueAnimators.
* This approach ensures that the setting of animation values will happen on the
* same thread that animations start on, and that all animations will share the same times for
* calculating their values, which makes synchronizing animations possible.
*
* The handler uses the Choreographer by default for doing periodic callbacks. A custom
* AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
* may be independent of UI frame update. This could be useful in testing.
*/
public class AnimationHandler {
/**
* Callbacks that receives notifications for animation timing
*/
interface AnimationFrameCallback {
/**
* Run animation based on the frame time.
*
* @param frameTime The frame start time
*/
boolean doAnimationFrame(long frameTime);
}
/**
* This class is responsible for interacting with the available frame provider by either
* registering frame callback or posting runnable, and receiving a callback for when a
* new frame has arrived. This dispatcher class then notifies all the on-going animations of
* the new frame, so that they can update animation values as needed.
*/
private class AnimationCallbackDispatcher {
/**
* Notifies all the on-going animations of the new frame.
*/
void dispatchAnimationFrame() {
mCurrentFrameTime = SystemClock.uptimeMillis();
AnimationHandler.this.doAnimationFrame(mCurrentFrameTime);
if (mAnimationCallbacks.size() > 0) {
mScheduler.postFrameCallback(mRunnable);
}
}
}
private static final ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
/**
* Internal per-thread collections used to avoid set collisions as animations start and end
* while being processed.
*/
private final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
new SimpleArrayMap<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final ArrayList<AnimationFrameCallback> mAnimationCallbacks = new ArrayList<>();
private final AnimationCallbackDispatcher mCallbackDispatcher =
new AnimationCallbackDispatcher();
private final Runnable mRunnable = () -> mCallbackDispatcher.dispatchAnimationFrame();
private FrameCallbackScheduler mScheduler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
long mCurrentFrameTime = 0;
private boolean mListDirty = false;
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting
public float mDurationScale = 1.0f;
@Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting
public DurationScaleChangeListener mDurationScaleChangeListener;
static AnimationHandler getInstance() {
if (sAnimatorHandler.get() == null) {
AnimationHandler handler = new AnimationHandler(
new FrameCallbackScheduler16());
sAnimatorHandler.set(handler);
}
return sAnimatorHandler.get();
}
/**
* The constructor of the AnimationHandler with {@link FrameCallbackScheduler} which is handle
* running the given Runnable on the next frame.
*
* @param scheduler The scheduler for this handler to run the given runnable.
*/
public AnimationHandler(@NonNull FrameCallbackScheduler scheduler) {
mScheduler = scheduler;
}
/**
* Register to get a callback on the next frame after the delay.
*/
void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
mScheduler.postFrameCallback(mRunnable);
if (Build.VERSION.SDK_INT >= 33) {
mDurationScale = ValueAnimator.getDurationScale();
if (mDurationScaleChangeListener == null) {
mDurationScaleChangeListener = new DurationScaleChangeListener33();
}
mDurationScaleChangeListener.register();
}
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
/**
* Removes the given callback from the list, so it will no longer be called for frame related
* timing.
*/
void removeCallback(AnimationFrameCallback callback) {
mDelayedCallbackStartTime.remove(callback);
int id = mAnimationCallbacks.indexOf(callback);
if (id >= 0) {
mAnimationCallbacks.set(id, null);
mListDirty = true;
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
for (int i = 0; i < mAnimationCallbacks.size(); i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
}
}
cleanUpList();
}
/**
* Returns whether the current thread is the same thread as the animation handler
* frame scheduler.
*
* @return true the current thread is the same thread as the animation handler frame scheduler.
*/
boolean isCurrentThread() {
return mScheduler.isCurrentThread();
}
/**
* Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
* so that they can start getting frame callbacks.
*
* @return true if they have passed the initial delay or have no delay, false otherwise.
*/
private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
Long startTime = mDelayedCallbackStartTime.get(callback);
if (startTime == null) {
return true;
}
if (startTime < currentTime) {
mDelayedCallbackStartTime.remove(callback);
return true;
}
return false;
}
private void cleanUpList() {
if (mListDirty) {
for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
if (mAnimationCallbacks.get(i) == null) {
mAnimationCallbacks.remove(i);
}
}
// Unregister duration scale listener if there are no current animations.
if (mAnimationCallbacks.size() == 0) {
if (Build.VERSION.SDK_INT >= 33) {
mDurationScaleChangeListener.unregister();
}
}
mListDirty = false;
}
}
/**
* Gets the FrameCallbackScheduler in this handler.
*
* @return The FrameCallbackScheduler in this handler
*/
@NonNull
FrameCallbackScheduler getScheduler() {
return mScheduler;
}
/**
* Default provider of timing pulse that uses Choreographer for frame callbacks.
*/
static final class FrameCallbackScheduler16 implements FrameCallbackScheduler {
private final Choreographer mChoreographer = Choreographer.getInstance();
private final Looper mLooper = Looper.myLooper();
@Override
public void postFrameCallback(@NonNull Runnable frameCallback) {
mChoreographer.postFrameCallback(time -> frameCallback.run());
}
@Override
public boolean isCurrentThread() {
return Thread.currentThread() == mLooper.getThread();
}
}
/**
* Returns the system-wide scaling factor for animations.
*/
@VisibleForTesting
public float getDurationScale() {
return mDurationScale;
}
/**
* T+ listener for changes to the system-wide scaling factor for Animator-based animations.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(api = 33)
@VisibleForTesting
public class DurationScaleChangeListener33 implements DurationScaleChangeListener {
ValueAnimator.DurationScaleChangeListener mListener;
@Override
public boolean register() {
if (mListener == null) {
mListener = scale -> AnimationHandler.this.mDurationScale = scale;
return ValueAnimator.registerDurationScaleChangeListener(mListener);
}
return true;
}
@Override
public boolean unregister() {
boolean unregistered = ValueAnimator.unregisterDurationScaleChangeListener(mListener);
mListener = null;
return unregistered;
}
}
/**
* listener for changes to the system-wide scaling factor for Animator-based animations.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting
public interface DurationScaleChangeListener {
/**
* Registers a duration scale change listener.
* @return true if a listener is registered or one is already registered.
*/
boolean register();
/**
* Unregisters a duration scale change listener.
* @return true if a listener is unregistered.
*/
boolean unregister();
}
}
@@ -0,0 +1,846 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import android.annotation.SuppressLint;
import android.util.AndroidRuntimeException;
import android.view.Choreographer;
import android.view.View;
import androidx.annotation.FloatRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import java.util.ArrayList;
/**
* This class is the base class of physics-based animations. It manages the animation's
* lifecycle such as {@link #start()} and {@link #cancel()}. This base class also handles the common
* setup for all the subclass animations. For example, DynamicAnimation supports adding
* {@link OnAnimationEndListener} and {@link OnAnimationUpdateListener} so that the important
* animation events can be observed through the callbacks. The start conditions for any subclass of
* DynamicAnimation can be set using {@link #setStartValue(float)} and
* {@link #setStartVelocity(float)}.
*
* @param <T> subclass of DynamicAnimation
*/
public abstract class DynamicAnimation<T extends DynamicAnimation<T>>
implements AnimationHandler.AnimationFrameCallback {
/**
* ViewProperty holds the access of a property of a {@link View}. When an animation is
* created with a {@link ViewProperty} instance, the corresponding property value of the view
* will be updated through this ViewProperty instance.
*/
public abstract static class ViewProperty extends FloatPropertyCompat<View> {
private ViewProperty(String name) {
super(name);
}
}
/**
* View's translationX property.
*/
public static final ViewProperty TRANSLATION_X = new ViewProperty("translationX") {
@Override
public void setValue(View view, float value) {
view.setTranslationX(value);
}
@Override
public float getValue(View view) {
return view.getTranslationX();
}
};
/**
* View's translationY property.
*/
public static final ViewProperty TRANSLATION_Y = new ViewProperty("translationY") {
@Override
public void setValue(View view, float value) {
view.setTranslationY(value);
}
@Override
public float getValue(View view) {
return view.getTranslationY();
}
};
/**
* View's translationZ property.
*/
public static final ViewProperty TRANSLATION_Z = new ViewProperty("translationZ") {
@Override
public void setValue(View view, float value) {
ViewCompat.setTranslationZ(view, value);
}
@Override
public float getValue(View view) {
return ViewCompat.getTranslationZ(view);
}
};
/**
* View's scaleX property.
*/
public static final ViewProperty SCALE_X = new ViewProperty("scaleX") {
@Override
public void setValue(View view, float value) {
view.setScaleX(value);
}
@Override
public float getValue(View view) {
return view.getScaleX();
}
};
/**
* View's scaleY property.
*/
public static final ViewProperty SCALE_Y = new ViewProperty("scaleY") {
@Override
public void setValue(View view, float value) {
view.setScaleY(value);
}
@Override
public float getValue(View view) {
return view.getScaleY();
}
};
/**
* View's rotation property.
*/
public static final ViewProperty ROTATION = new ViewProperty("rotation") {
@Override
public void setValue(View view, float value) {
view.setRotation(value);
}
@Override
public float getValue(View view) {
return view.getRotation();
}
};
/**
* View's rotationX property.
*/
public static final ViewProperty ROTATION_X = new ViewProperty("rotationX") {
@Override
public void setValue(View view, float value) {
view.setRotationX(value);
}
@Override
public float getValue(View view) {
return view.getRotationX();
}
};
/**
* View's rotationY property.
*/
public static final ViewProperty ROTATION_Y = new ViewProperty("rotationY") {
@Override
public void setValue(View view, float value) {
view.setRotationY(value);
}
@Override
public float getValue(View view) {
return view.getRotationY();
}
};
/**
* View's x property.
*/
public static final ViewProperty X = new ViewProperty("x") {
@Override
public void setValue(View view, float value) {
view.setX(value);
}
@Override
public float getValue(View view) {
return view.getX();
}
};
/**
* View's y property.
*/
public static final ViewProperty Y = new ViewProperty("y") {
@Override
public void setValue(View view, float value) {
view.setY(value);
}
@Override
public float getValue(View view) {
return view.getY();
}
};
/**
* View's z property.
*/
public static final ViewProperty Z = new ViewProperty("z") {
@Override
public void setValue(View view, float value) {
ViewCompat.setZ(view, value);
}
@Override
public float getValue(View view) {
return ViewCompat.getZ(view);
}
};
/**
* View's alpha property.
*/
public static final ViewProperty ALPHA = new ViewProperty("alpha") {
@Override
public void setValue(View view, float value) {
view.setAlpha(value);
}
@Override
public float getValue(View view) {
return view.getAlpha();
}
};
// Properties below are not RenderThread compatible
/**
* View's scrollX property.
*/
public static final ViewProperty SCROLL_X = new ViewProperty("scrollX") {
@Override
public void setValue(View view, float value) {
view.setScrollX((int) value);
}
@Override
public float getValue(View view) {
return view.getScrollX();
}
};
/**
* View's scrollY property.
*/
public static final ViewProperty SCROLL_Y = new ViewProperty("scrollY") {
@Override
public void setValue(View view, float value) {
view.setScrollY((int) value);
}
@Override
public float getValue(View view) {
return view.getScrollY();
}
};
/**
* The minimum visible change in pixels that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_PIXELS = 1f;
/**
* The minimum visible change in degrees that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_ROTATION_DEGREES = 1f / 10f;
/**
* The minimum visible change in alpha that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_ALPHA = 1f / 256f;
/**
* The minimum visible change in scale that can be visible to users.
*/
@SuppressLint("MinMaxConstant")
public static final float MIN_VISIBLE_CHANGE_SCALE = 1f / 500f;
// Use the max value of float to indicate an unset state.
private static final float UNSET = Float.MAX_VALUE;
// Multiplier to the min visible change value for value threshold
private static final float THRESHOLD_MULTIPLIER = 0.75f;
// Internal tracking for velocity.
float mVelocity = 0;
// Internal tracking for value.
float mValue = UNSET;
// Tracks whether start value is set. If not, the animation will obtain the value at the time
// of starting through the getter and use that as the starting value of the animation.
boolean mStartValueIsSet = false;
// Target to be animated.
final Object mTarget;
// View property id.
final FloatPropertyCompat mProperty;
// Package private tracking of animation lifecycle state. Visible to subclass animations.
boolean mRunning = false;
// Min and max values that defines the range of the animation values.
float mMaxValue = Float.MAX_VALUE;
float mMinValue = -mMaxValue;
// Last frame time. Always gets reset to -1 at the end of the animation.
private long mLastFrameTime = 0;
private float mMinVisibleChange;
// List of end listeners
private final ArrayList<OnAnimationEndListener> mEndListeners = new ArrayList<>();
// List of update listeners
private final ArrayList<OnAnimationUpdateListener> mUpdateListeners = new ArrayList<>();
// Animation handler used to schedule updates for this animation.
private AnimationHandler mAnimationHandler;
// Internal state for value/velocity pair.
static class MassState {
float mValue;
float mVelocity;
}
/**
* Creates a dynamic animation with the given FloatValueHolder instance.
*
* @param floatValueHolder the FloatValueHolder instance to be animated.
*/
DynamicAnimation(final FloatValueHolder floatValueHolder) {
mTarget = null;
mProperty = new FloatPropertyCompat("FloatValueHolder") {
@Override
public float getValue(Object object) {
return floatValueHolder.getValue();
}
@Override
public void setValue(Object object, float value) {
floatValueHolder.setValue(value);
}
};
mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS;
}
/**
* Creates a dynamic animation to animate the given property for the given {@link View}
*
* @param object the Object whose property is to be animated
* @param property the property to be animated
*/
<K> DynamicAnimation(K object, FloatPropertyCompat<K> property) {
mTarget = object;
mProperty = property;
if (mProperty == ROTATION || mProperty == ROTATION_X
|| mProperty == ROTATION_Y) {
mMinVisibleChange = MIN_VISIBLE_CHANGE_ROTATION_DEGREES;
} else if (mProperty == ALPHA) {
mMinVisibleChange = MIN_VISIBLE_CHANGE_ALPHA;
} else if (mProperty == SCALE_X || mProperty == SCALE_Y) {
mMinVisibleChange = MIN_VISIBLE_CHANGE_SCALE;
} else {
mMinVisibleChange = MIN_VISIBLE_CHANGE_PIXELS;
}
}
/**
* Sets the start value of the animation. If start value is not set, the animation will get
* the current value for the view's property, and use that as the start value.
*
* @param startValue start value for the animation
* @return the Animation whose start value is being set
*/
@SuppressWarnings("unchecked")
public T setStartValue(float startValue) {
mValue = startValue;
mStartValueIsSet = true;
return (T) this;
}
/**
* Start velocity of the animation. Default velocity is 0. Unit: change in property per
* second (e.g. pixels per second, scale/alpha value change per second).
*
* <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity
* through touch events), it is recommended to define such a value in dp/second and convert it
* to pixel/second based on the density of the screen to achieve a consistent look across
* different screens.
*
* <p>To convert from dp/second to pixel/second:
* <pre class="prettyprint">
* float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond,
* getResources().getDisplayMetrics());
* </pre>
*
* @param startVelocity start velocity of the animation
* @return the Animation whose start velocity is being set
*/
@SuppressWarnings("unchecked")
public T setStartVelocity(float startVelocity) {
mVelocity = startVelocity;
return (T) this;
}
/**
* Sets the max value of the animation. Animations will not animate beyond their max value.
* Whether or not animation will come to an end when max value is reached is dependent on the
* child animation's implementation.
*
* @param max maximum value of the property to be animated
* @return the Animation whose max value is being set
*/
@SuppressWarnings("unchecked")
public T setMaxValue(float max) {
// This max value should be checked and handled in the subclass animations, instead of
// assuming the end of the animations when the max/min value is hit in the base class.
// The reason is that hitting max/min value may just be a transient state, such as during
// the spring oscillation.
mMaxValue = max;
return (T) this;
}
/**
* Sets the min value of the animation. Animations will not animate beyond their min value.
* Whether or not animation will come to an end when min value is reached is dependent on the
* child animation's implementation.
*
* @param min minimum value of the property to be animated
* @return the Animation whose min value is being set
*/
@SuppressWarnings("unchecked")
public T setMinValue(float min) {
mMinValue = min;
return (T) this;
}
/**
* Adds an end listener to the animation for receiving onAnimationEnd callbacks. If the listener
* is {@code null} or has already been added to the list of listeners for the animation, no op.
*
* @param listener the listener to be added
* @return the animation to which the listener is added
*/
@SuppressWarnings("unchecked")
public T addEndListener(OnAnimationEndListener listener) {
if (!mEndListeners.contains(listener)) {
mEndListeners.add(listener);
}
return (T) this;
}
/**
* Removes the end listener from the animation, so as to stop receiving animation end callbacks.
*
* @param listener the listener to be removed
*/
public void removeEndListener(OnAnimationEndListener listener) {
removeEntry(mEndListeners, listener);
}
/**
* Adds an update listener to the animation for receiving per-frame animation update callbacks.
* If the listener is {@code null} or has already been added to the list of listeners for the
* animation, no op.
*
* <p>Note that update listener should only be added before the start of the animation.
*
* @param listener the listener to be added
* @return the animation to which the listener is added
* @throws UnsupportedOperationException if the update listener is added after the animation has
* started
*/
@SuppressWarnings("unchecked")
public T addUpdateListener(OnAnimationUpdateListener listener) {
if (isRunning()) {
// Require update listener to be added before the animation, such as when we start
// the animation, we know whether the animation is RenderThread compatible.
throw new UnsupportedOperationException("Error: Update listeners must be added before"
+ "the animation.");
}
if (!mUpdateListeners.contains(listener)) {
mUpdateListeners.add(listener);
}
return (T) this;
}
/**
* Removes the update listener from the animation, so as to stop receiving animation update
* callbacks.
*
* @param listener the listener to be removed
*/
public void removeUpdateListener(OnAnimationUpdateListener listener) {
removeEntry(mUpdateListeners, listener);
}
/**
* This method sets the minimal change of animation value that is visible to users, which helps
* determine a reasonable threshold for the animation's termination condition. It is critical
* to set the minimal visible change for custom properties (i.e. non-<code>ViewProperty</code>s)
* unless the custom property is in pixels.
*
* <p>For custom properties, this minimum visible change defaults to change in pixel
* (i.e. {@link #MIN_VISIBLE_CHANGE_PIXELS}. It is recommended to adjust this value that is
* reasonable for the property to be animated. A general rule of thumb to calculate such a value
* is: minimum visible change = range of custom property value / corresponding pixel range. For
* example, if the property to be animated is a progress (from 0 to 100) that corresponds to a
* 200-pixel change. Then the min visible change should be 100 / 200. (i.e. 0.5).
*
* <p>It's not necessary to call this method when animating {@link ViewProperty}s, as the
* minimum visible change will be derived from the property. For example, if the property to be
* animated is in pixels (i.e. {@link #TRANSLATION_X}, {@link #TRANSLATION_Y},
* {@link #TRANSLATION_Z}, @{@link #SCROLL_X} or {@link #SCROLL_Y}), the default minimum visible
* change is 1 (pixel). For {@link #ROTATION}, {@link #ROTATION_X} or {@link #ROTATION_Y}, the
* animation will use {@link #MIN_VISIBLE_CHANGE_ROTATION_DEGREES} as the min visible change,
* which is 1/10. Similarly, the minimum visible change for alpha (
* i.e. {@link #MIN_VISIBLE_CHANGE_ALPHA} is defined as 1 / 256.
*
* @param minimumVisibleChange minimum change in property value that is visible to users
* @return the animation whose min visible change is being set
* @throws IllegalArgumentException if the given threshold is not positive
*/
@SuppressWarnings("unchecked")
public T setMinimumVisibleChange(@FloatRange(from = 0.0, fromInclusive = false)
float minimumVisibleChange) {
if (minimumVisibleChange <= 0) {
throw new IllegalArgumentException("Minimum visible change must be positive.");
}
mMinVisibleChange = minimumVisibleChange;
setValueThreshold(minimumVisibleChange * THRESHOLD_MULTIPLIER);
return (T) this;
}
/**
* Returns the minimum change in the animation property that could be visibly different to
* users.
*
* @return minimum change in property value that is visible to users
*/
public float getMinimumVisibleChange() {
return mMinVisibleChange;
}
/**
* Remove {@code null} entries from the list.
*/
private static <T> void removeNullEntries(ArrayList<T> list) {
// Clean up null entries
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i) == null) {
list.remove(i);
}
}
}
/**
* Remove an entry from the list by marking it {@code null} and clean up later.
*/
private static <T> void removeEntry(ArrayList<T> list, T entry) {
int id = list.indexOf(entry);
if (id >= 0) {
list.set(id, null);
}
}
/****************Animation Lifecycle Management***************/
/**
* Starts an animation. If the animation has already been started, no op. Note that calling
* {@link #start()} will not immediately set the property value to start value of the animation.
* The property values will be changed at each animation pulse, which happens before the draw
* pass. As a result, the changes will be reflected in the next frame, the same as if the values
* were set immediately. This method should only be called on main thread.
*
* Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
* is created on the same thread as the first call to start/cancel an animation. All the
* subsequent animation lifecycle manipulations need to be on that same thread, until the
* AnimationHandler is reset (using [setAnimationHandler]).
*
* @throws AndroidRuntimeException if this method is not called on the same thread as the
* animation handler
*/
@MainThread
public void start() {
if (!getAnimationHandler().isCurrentThread()) {
throw new AndroidRuntimeException("Animations may only be started on the same thread "
+ "as the animation handler");
}
if (!mRunning) {
startAnimationInternal();
}
}
/**
* Cancels the on-going animation. If the animation hasn't started, no op.
*
* Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
* is created on the same thread as the first call to start/cancel an animation. All the
* subsequent animation lifecycle manipulations need to be on that same thread, until the
* AnimationHandler is reset (using [setAnimationHandler]).
*
* @throws AndroidRuntimeException if this method is not called on the same thread as the
* animation handler
*/
@MainThread
public void cancel() {
if (!getAnimationHandler().isCurrentThread()) {
throw new AndroidRuntimeException("Animations may only be canceled from the same "
+ "thread as the animation handler");
}
if (mRunning) {
endAnimationInternal(true);
}
}
/**
* Returns whether the animation is currently running.
*
* @return {@code true} if the animation is currently running, {@code false} otherwise
*/
public boolean isRunning() {
return mRunning;
}
/************************** Private APIs below ********************************/
// This gets called when the animation is started, to finish the setup of the animation
// before the animation pulsing starts.
private void startAnimationInternal() {
if (!mRunning) {
mRunning = true;
if (!mStartValueIsSet) {
mValue = getPropertyValue();
}
// Sanity check:
if (mValue > mMaxValue || mValue < mMinValue) {
throw new IllegalArgumentException("Starting value need to be in between min"
+ " value and max value");
}
getAnimationHandler().addAnimationFrameCallback(this, 0);
}
}
/**
* This gets call on each frame of the animation. Animation value and velocity are updated
* in this method based on the new frame time. The property value of the view being animated
* is then updated. The animation's ending conditions are also checked in this method. Once
* the animation reaches equilibrium, the animation will come to its end, and end listeners
* will be notified, if any.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean doAnimationFrame(long frameTime) {
if (mLastFrameTime == 0) {
// First frame.
mLastFrameTime = frameTime;
setPropertyValue(mValue);
return false;
}
long deltaT = frameTime - mLastFrameTime;
mLastFrameTime = frameTime;
float durationScale = getAnimationHandler().getDurationScale();
deltaT = durationScale == 0.0f ? Integer.MAX_VALUE : (long) (deltaT / durationScale);
boolean finished = updateValueAndVelocity(deltaT);
// Clamp value & velocity.
mValue = Math.min(mValue, mMaxValue);
mValue = Math.max(mValue, mMinValue);
setPropertyValue(mValue);
if (finished) {
endAnimationInternal(false);
}
return finished;
}
/**
* Updates the animation state (i.e. value and velocity). This method is package private, so
* subclasses can override this method to calculate the new value and velocity in their custom
* way.
*
* @param deltaT time elapsed in millisecond since last frame
* @return whether the animation has finished
*/
abstract boolean updateValueAndVelocity(long deltaT);
/**
* Internal method to reset the animation states when animation is finished/canceled.
*/
private void endAnimationInternal(boolean canceled) {
mRunning = false;
getAnimationHandler().removeCallback(this);
mLastFrameTime = 0;
mStartValueIsSet = false;
for (int i = 0; i < mEndListeners.size(); i++) {
if (mEndListeners.get(i) != null) {
mEndListeners.get(i).onAnimationEnd(this, canceled, mValue, mVelocity);
}
}
removeNullEntries(mEndListeners);
}
/**
* Updates the property value through the corresponding setter.
*/
@SuppressWarnings("unchecked")
void setPropertyValue(float value) {
mProperty.setValue(mTarget, value);
for (int i = 0; i < mUpdateListeners.size(); i++) {
if (mUpdateListeners.get(i) != null) {
mUpdateListeners.get(i).onAnimationUpdate(this, mValue, mVelocity);
}
}
removeNullEntries(mUpdateListeners);
}
/**
* Returns the default threshold.
*/
float getValueThreshold() {
return mMinVisibleChange * THRESHOLD_MULTIPLIER;
}
/**
* Obtain the property value through the corresponding getter.
*/
@SuppressWarnings("unchecked")
private float getPropertyValue() {
return mProperty.getValue(mTarget);
}
/**
* Returns the {@link AnimationHandler} used to schedule updates for this animator.
*
* @return the {@link AnimationHandler} for this animator.
*/
@NonNull
@VisibleForTesting
public AnimationHandler getAnimationHandler() {
return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance();
}
/**
* Returns the {@link FrameCallbackScheduler} used to schedule updates for this animator.
*
* If not already set using {@link #setScheduler(FrameCallbackScheduler)}, this is initialized
* to an {@link FrameCallbackScheduler} that uses the caller thread's {@link Choreographer}.
* Otherwise, return the scheduler set via the previous
* {@link #setScheduler(FrameCallbackScheduler)} call.
*
* @return the {@link FrameCallbackScheduler} for this animator.
*/
@NonNull
public FrameCallbackScheduler getScheduler() {
return mAnimationHandler != null ? mAnimationHandler.getScheduler()
: AnimationHandler.getInstance().getScheduler();
}
/**
* Sets the frame scheduler used to schedule updates for this animator. Note this should
* be called before animation running, as it would lead to discontinuity in animations.
*
* @param scheduler The {@link FrameCallbackScheduler} that will be used to schedule updates
* for this animator.
* @throws AndroidRuntimeException if this method called when animation running
*/
public void setScheduler(@NonNull FrameCallbackScheduler scheduler) {
if (mAnimationHandler != null && mAnimationHandler.getScheduler() == scheduler) {
return;
}
if (mRunning) {
throw new AndroidRuntimeException("Animations are still running and the animation"
+ "handler should not be set at this timming");
}
mAnimationHandler = new AnimationHandler(scheduler);
}
/****************Sub class animations**************/
/**
* Returns the acceleration at the given value with the given velocity.
*/
abstract float getAcceleration(float value, float velocity);
/**
* Returns whether the animation has reached equilibrium.
*/
abstract boolean isAtEquilibrium(float value, float velocity);
/**
* Updates the default value threshold for the animation based on the property to be animated.
*/
abstract void setValueThreshold(float threshold);
/**
* An animation listener that receives end notifications from an animation.
*/
public interface OnAnimationEndListener {
/**
* Notifies the end of an animation. Note that this callback will be invoked not only when
* an animation reach equilibrium, but also when the animation is canceled.
*
* @param animation animation that has ended or was canceled
* @param canceled whether the animation has been canceled
* @param value the final value when the animation stopped
* @param velocity the final velocity when the animation stopped
*/
void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity);
}
/**
* Implementors of this interface can add themselves as update listeners
* to an <code>DynamicAnimation</code> instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* <code>DynamicAnimation</code>.
*/
public interface OnAnimationUpdateListener {
/**
* Notifies the occurrence of another frame of the animation.
*
* @param animation animation that the update listener is added to
* @param value the current value of the animation
* @param velocity the current velocity of the animation
*/
void onAnimationUpdate(DynamicAnimation animation, float value, float velocity);
}
}
@@ -0,0 +1,240 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import androidx.annotation.FloatRange;
/**
* <p>Fling animation is an animation that continues an initial momentum (most often from gesture
* velocity) and gradually slows down. The fling animation will come to a stop when the velocity of
* the animation is below the threshold derived from {@link #setMinimumVisibleChange(float)},
* or when the value of the animation has gone beyond the min or max value defined via
* {@link DynamicAnimation#setMinValue(float)} or {@link DynamicAnimation#setMaxValue(float)}.
* It is recommended to restrict the fling animation with min and/or max value, such that the
* animation can end when it goes beyond screen bounds, thus preserving CPU cycles and resources.
*
* <p>For example, you can create a fling animation that animates the translationX of a view:
* <pre class="prettyprint">
* FlingAnimation flingAnim = new FlingAnimation(view, DynamicAnimation.TRANSLATION_X)
* // Sets the start velocity to -2000 (pixel/s)
* .setStartVelocity(-2000)
* // Optional but recommended to set a reasonable min and max range for the animation.
* // In this particular case, we set the min and max to -200 and 2000 respectively.
* .setMinValue(-200).setMaxValue(2000);
* flingAnim.start();
* </pre>
*/
public final class FlingAnimation extends DynamicAnimation<FlingAnimation> {
private final DragForce mFlingForce = new DragForce();
/**
* <p>This creates a FlingAnimation that animates a {@link FloatValueHolder} instance. During
* the animation, the {@link FloatValueHolder} instance will be updated via
* {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
* animation value via {@link FloatValueHolder#getValue()}.
*
* <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
* {@link FloatValueHolder#setValue(float)} outside of the animation during an
* animation run will not have any effect on the on-going animation.
*
* @param floatValueHolder the property to be animated
*/
public FlingAnimation(FloatValueHolder floatValueHolder) {
super(floatValueHolder);
mFlingForce.setValueThreshold(getValueThreshold());
}
/**
* This creates a FlingAnimation that animates the property of the given object.
*
* @param object the Object whose property will be animated
* @param property the property to be animated
* @param <K> the class on which the property is declared
*/
public <K> FlingAnimation(K object, FloatPropertyCompat<K> property) {
super(object, property);
mFlingForce.setValueThreshold(getValueThreshold());
}
/**
* Sets the friction for the fling animation. The greater the friction is, the sooner the
* animation will slow down. When not set, the friction defaults to 1.
*
* @param friction the friction used in the animation
* @return the animation whose friction will be scaled
* @throws IllegalArgumentException if the input friction is not positive
*/
public FlingAnimation setFriction(
@FloatRange(from = 0.0, fromInclusive = false) float friction) {
if (friction <= 0) {
throw new IllegalArgumentException("Friction must be positive");
}
mFlingForce.setFrictionScalar(friction);
return this;
}
/**
* Returns the friction being set on the animation via {@link #setFriction(float)}. If the
* friction has not been set, the default friction of 1 will be returned.
*
* @return friction being used in the animation
*/
public float getFriction() {
return mFlingForce.getFrictionScalar();
}
/**
* Sets the min value of the animation. When a fling animation reaches the min value, the
* animation will end immediately. Animations will not animate beyond the min value.
*
* @param minValue minimum value of the property to be animated
* @return the Animation whose min value is being set
*/
@Override
public FlingAnimation setMinValue(float minValue) {
super.setMinValue(minValue);
return this;
}
/**
* Sets the max value of the animation. When a fling animation reaches the max value, the
* animation will end immediately. Animations will not animate beyond the max value.
*
* @param maxValue maximum value of the property to be animated
* @return the Animation whose max value is being set
*/
@Override
public FlingAnimation setMaxValue(float maxValue) {
super.setMaxValue(maxValue);
return this;
}
/**
* Start velocity of the animation. Default velocity is 0. Unit: pixel/second
*
* <p>A <b>non-zero</b> start velocity is required for a FlingAnimation. If no start velocity is
* set through {@link #setStartVelocity(float)}, the start velocity defaults to 0. In that
* case, the fling animation will consider itself done in the next frame.
*
* <p>Note when using a fixed value as the start velocity (as opposed to getting the velocity
* through touch events), it is recommended to define such a value in dp/second and convert it
* to pixel/second based on the density of the screen to achieve a consistent look across
* different screens.
*
* <p>To convert from dp/second to pixel/second:
* <pre class="prettyprint">
* float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond,
* getResources().getDisplayMetrics());
* </pre>
*
* @param startVelocity start velocity of the animation in pixel/second
* @return the Animation whose start velocity is being set
*/
@Override
public FlingAnimation setStartVelocity(float startVelocity) {
super.setStartVelocity(startVelocity);
return this;
}
@Override
boolean updateValueAndVelocity(long deltaT) {
MassState state = mFlingForce.updateValueAndVelocity(mValue, mVelocity, deltaT);
mValue = state.mValue;
mVelocity = state.mVelocity;
// When the animation hits the max/min value, consider animation done.
if (mValue < mMinValue) {
mValue = mMinValue;
return true;
}
if (mValue > mMaxValue) {
mValue = mMaxValue;
return true;
}
if (isAtEquilibrium(mValue, mVelocity)) {
return true;
}
return false;
}
@Override
float getAcceleration(float value, float velocity) {
return mFlingForce.getAcceleration(value, velocity);
}
@Override
boolean isAtEquilibrium(float value, float velocity) {
return value >= mMaxValue
|| value <= mMinValue
|| mFlingForce.isAtEquilibrium(value, velocity);
}
@Override
void setValueThreshold(float threshold) {
mFlingForce.setValueThreshold(threshold);
}
static final class DragForce implements Force {
private static final float DEFAULT_FRICTION = -4.2f;
// This multiplier is used to calculate the velocity threshold given a certain value
// threshold. The idea is that if it takes >= 1 frame to move the value threshold amount,
// then the velocity is a reasonable threshold.
private static final float VELOCITY_THRESHOLD_MULTIPLIER = 1000f / 16f;
private float mFriction = DEFAULT_FRICTION;
private float mVelocityThreshold;
// Internal state to hold a value/velocity pair.
private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState();
void setFrictionScalar(float frictionScalar) {
mFriction = frictionScalar * DEFAULT_FRICTION;
}
float getFrictionScalar() {
return mFriction / DEFAULT_FRICTION;
}
MassState updateValueAndVelocity(float value, float velocity, long deltaT) {
mMassState.mVelocity = (float) (velocity * Math.exp((deltaT / 1000f) * mFriction));
mMassState.mValue = (float) (value + (mMassState.mVelocity - velocity) / mFriction);
if (isAtEquilibrium(mMassState.mValue, mMassState.mVelocity)) {
mMassState.mVelocity = 0f;
}
return mMassState;
}
@Override
public float getAcceleration(float position, float velocity) {
return velocity * mFriction;
}
@Override
public boolean isAtEquilibrium(float value, float velocity) {
return Math.abs(velocity) < mVelocityThreshold;
}
void setValueThreshold(float threshold) {
mVelocityThreshold = threshold * VELOCITY_THRESHOLD_MULTIPLIER;
}
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import android.util.FloatProperty;
import androidx.annotation.RequiresApi;
/**
* <p>FloatPropertyCompat is an abstraction that can be used to represent a mutable float value that
* is held in a host object. To access this float value, {@link #setValue(Object, float)} and getter
* {@link #getValue(Object)} need to be implemented. Both the setter and the getter take the
* primitive <code>float</code> type and avoids autoboxing and other overhead associated with the
* <code>Float</code> class.
*
* <p>For API 24 and later, {@link FloatProperty} instances can be converted to
* {@link FloatPropertyCompat} through
* {@link FloatPropertyCompat#createFloatPropertyCompat(FloatProperty)}.
*
* @param <T> the class on which the Property is declared
*/
public abstract class FloatPropertyCompat<T> {
final String mPropertyName;
/**
* A constructor that takes an identifying name.
*/
public FloatPropertyCompat(String name) {
mPropertyName = name;
}
/**
* Create a {@link FloatPropertyCompat} wrapper for a {@link FloatProperty} object. The new
* {@link FloatPropertyCompat} instance will access and modify the property value of
* {@link FloatProperty} through the {@link FloatProperty} instance's setter and getter.
*
* @param property FloatProperty instance to be wrapped
* @param <T> the class on which the Property is declared
* @return a new {@link FloatPropertyCompat} wrapper for the given {@link FloatProperty} object
*/
@RequiresApi(24)
public static <T> FloatPropertyCompat<T> createFloatPropertyCompat(
final FloatProperty<T> property) {
return new FloatPropertyCompat<T>(property.getName()) {
@Override
public float getValue(T object) {
return property.get(object);
}
@Override
public void setValue(T object, float value) {
property.setValue(object, value);
}
};
}
/**
* Returns the current value that this property represents on the given <code>object</code>.
*
* @param object object which this property represents
* @return the current property value of the given object
*/
public abstract float getValue(T object);
/**
* Sets the value on <code>object</code> which this property represents.
*
* @param object object which this property represents
* @param value new value of the property
*/
public abstract void setValue(T object, float value);
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
/**
* <p>FloatValueHolder holds a float value. FloatValueHolder provides a setter and a getter (
* i.e. {@link #setValue(float)} and {@link #getValue()}) to access this float value. Animations can
* be performed on a FloatValueHolder instance. During each frame of the animation, the
* FloatValueHolder will have its value updated via {@link #setValue(float)}. The caller can
* obtain the up-to-date animation value via {@link FloatValueHolder#getValue()}.
*
* <p> Here is an example for creating a {@link FlingAnimation} with a FloatValueHolder:
* <pre class="prettyprint">
* // Create a fling animation with an initial velocity of 5000 (pixel/s) and an initial position
* // of 20f.
* FloatValueHolder floatValueHolder = new FloatValueHolder(20f);
* FlingAnimation anim = new FlingAnimation(floatValueHolder).setStartVelocity(5000);
* anim.start();
* </pre>
*
* @see SpringAnimation#SpringAnimation(FloatValueHolder)
* @see FlingAnimation#FlingAnimation(FloatValueHolder)
*/
public class FloatValueHolder {
private float mValue = 0.0f;
/**
* Constructs a holder for a float value that is initialized to 0.
*/
public FloatValueHolder() {
}
/**
* Constructs a holder for a float value that is initialized to the input value.
*
* @param value the value to initialize the value held in the FloatValueHolder
*/
public FloatValueHolder(float value) {
setValue(value);
}
/**
* Sets the value held in the FloatValueHolder instance.
*
* @param value float value held in the FloatValueHolder instance
*/
public void setValue(float value) {
mValue = value;
}
/**
* Returns the float value held in the FloatValueHolder instance.
*
* @return float value held in the FloatValueHolder instance
*/
public float getValue() {
return mValue;
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
/**
* Hide this for now, in case we want to change the API.
*/
interface Force {
// Acceleration based on position.
float getAcceleration(float position, float velocity);
boolean isAtEquilibrium(float value, float velocity);
}
@@ -0,0 +1,40 @@
/*
* Copyright 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 androidx.dynamicanimation.animation;
import androidx.annotation.NonNull;
/**
* A scheduler that runs the given Runnable on the next frame.
*/
public interface FrameCallbackScheduler {
/**
* Callbacks on new frame arrived.
*
* @param frameCallback The runnable of new frame should be posted
*/
void postFrameCallback(@NonNull Runnable frameCallback);
/**
* Returns whether the current thread is the same as the thread that the scheduler is
* running on.
*
* @return true if the scheduler is running on the same thread as the current thread.
*/
boolean isCurrentThread();
}
@@ -0,0 +1,317 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import android.util.AndroidRuntimeException;
import androidx.annotation.MainThread;
/**
* SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines
* the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is
* started, on each frame the spring force will update the animation's value and velocity.
* The animation will continue to run until the spring force reaches equilibrium. If the spring used
* in the animation is undamped, the animation will never reach equilibrium. Instead, it will
* oscillate forever.
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* </div>
*
* <p>To create a simple {@link SpringAnimation} that uses the default {@link SpringForce}:</p>
* <pre class="prettyprint">
* // Create an animation to animate view's X property, set the rest position of the
* // default spring to 0, and start the animation with a starting velocity of 5000 (pixel/s).
* final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.X, 0)
* .setStartVelocity(5000);
* anim.start();
* </pre>
*
* <p>Alternatively, a {@link SpringAnimation} can take a pre-configured {@link SpringForce}, and
* use that to drive the animation. </p>
* <pre class="prettyprint">
* // Create a low stiffness, low bounce spring at position 0.
* SpringForce spring = new SpringForce(0)
* .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
* .setStiffness(SpringForce.STIFFNESS_LOW);
* // Create an animation to animate view's scaleY property, and start the animation using
* // the spring above and a starting value of 0.5. Additionally, constrain the range of value for
* // the animation to be non-negative, effectively preventing any spring overshoot.
* final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.SCALE_Y)
* .setMinValue(0).setSpring(spring).setStartValue(1);
* anim.start();
* </pre>
*/
public final class SpringAnimation extends DynamicAnimation<SpringAnimation> {
private SpringForce mSpring = null;
private float mPendingPosition = UNSET;
private static final float UNSET = Float.MAX_VALUE;
private boolean mEndRequested = false;
/**
* <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
* the animation, the {@link FloatValueHolder} instance will be updated via
* {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
* animation value via {@link FloatValueHolder#getValue()}.
*
* <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
* {@link FloatValueHolder#setValue(float)} outside of the animation during an
* animation run will not have any effect on the on-going animation.
*
* @param floatValueHolder the property to be animated
*/
public SpringAnimation(FloatValueHolder floatValueHolder) {
super(floatValueHolder);
}
/**
* <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During
* the animation, the {@link FloatValueHolder} instance will be updated via
* {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date
* animation value via {@link FloatValueHolder#getValue()}.
*
* A Spring will be created with the given final position and default stiffness and damping
* ratio. This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
*
* <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via
* {@link FloatValueHolder#setValue(float)} outside of the animation during an
* animation run will not have any effect on the on-going animation.
*
* @param floatValueHolder the property to be animated
* @param finalPosition the final position of the spring to be created.
*/
public SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition) {
super(floatValueHolder);
mSpring = new SpringForce(finalPosition);
}
/**
* This creates a SpringAnimation that animates the property of the given object.
* Note, a spring will need to setup through {@link #setSpring(SpringForce)} before
* the animation starts.
*
* @param object the Object whose property will be animated
* @param property the property to be animated
* @param <K> the class on which the Property is declared
*/
public <K> SpringAnimation(K object, FloatPropertyCompat<K> property) {
super(object, property);
}
/**
* This creates a SpringAnimation that animates the property of the given object. A Spring will
* be created with the given final position and default stiffness and damping ratio.
* This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}.
*
* @param object the Object whose property will be animated
* @param property the property to be animated
* @param finalPosition the final position of the spring to be created.
* @param <K> the class on which the Property is declared
*/
public <K> SpringAnimation(K object, FloatPropertyCompat<K> property,
float finalPosition) {
super(object, property);
mSpring = new SpringForce(finalPosition);
}
/**
* Returns the spring that the animation uses for animations.
*
* @return the spring that the animation uses for animations
*/
public SpringForce getSpring() {
return mSpring;
}
/**
* Uses the given spring as the force that drives this animation. If this spring force has its
* parameters re-configured during the animation, the new configuration will be reflected in the
* animation immediately.
*
* @param force a pre-defined spring force that drives the animation
* @return the animation that the spring force is set on
*/
public SpringAnimation setSpring(SpringForce force) {
mSpring = force;
return this;
}
@MainThread
@Override
public void start() {
sanityCheck();
mSpring.setValueThreshold(getValueThreshold());
super.start();
}
/**
* Updates the final position of the spring.
* <p/>
* When the animation is running, calling this method would assume the position change of the
* spring as a continuous movement since last frame, which yields more accurate results than
* changing the spring position directly through {@link SpringForce#setFinalPosition(float)}.
* <p/>
* If the animation hasn't started, calling this method will change the spring position, and
* immediately start the animation.
*
* @param finalPosition rest position of the spring
*/
public void animateToFinalPosition(float finalPosition) {
if (isRunning()) {
mPendingPosition = finalPosition;
} else {
if (mSpring == null) {
mSpring = new SpringForce(finalPosition);
}
mSpring.setFinalPosition(finalPosition);
start();
}
}
/**
* Cancels the on-going animation. If the animation hasn't started, no op. Note that this method
* should only be called on main thread.
*
* @throws AndroidRuntimeException if this method is not called on the main thread
*/
@MainThread
@Override
public void cancel() {
super.cancel();
if (mPendingPosition != UNSET) {
if (mSpring == null) {
mSpring = new SpringForce(mPendingPosition);
} else {
mSpring.setFinalPosition(mPendingPosition);
}
mPendingPosition = UNSET;
}
}
/**
* Skips to the end of the animation. If the spring is undamped, an
* {@link IllegalStateException} will be thrown, as the animation would never reach to an end.
* It is recommended to check {@link #canSkipToEnd()} before calling this method. If animation
* is not running, no-op.
*
* Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler
* is created on the same thread as the first call to start/cancel an animation. All the
* subsequent animation lifecycle manipulations need to be on that same thread, until the
* AnimationHandler is reset (using [setAnimationHandler]).
*
* @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0)
* @throws AndroidRuntimeException if this method is not called on the same thread as the
* animation handler
*/
public void skipToEnd() {
if (!canSkipToEnd()) {
throw new UnsupportedOperationException("Spring animations can only come to an end"
+ " when there is damping");
}
if (!getAnimationHandler().isCurrentThread()) {
throw new AndroidRuntimeException("Animations may only be started on the same thread "
+ "as the animation handler");
}
if (mRunning) {
mEndRequested = true;
}
}
/**
* Queries whether the spring can eventually come to the rest position.
*
* @return {@code true} if the spring is damped, otherwise {@code false}
*/
public boolean canSkipToEnd() {
return mSpring.mDampingRatio > 0;
}
/************************ Below are private APIs *************************/
private void sanityCheck() {
if (mSpring == null) {
throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final"
+ " position or a spring force needs to be set.");
}
double finalPosition = mSpring.getFinalPosition();
if (finalPosition > mMaxValue) {
throw new UnsupportedOperationException("Final position of the spring cannot be greater"
+ " than the max value.");
} else if (finalPosition < mMinValue) {
throw new UnsupportedOperationException("Final position of the spring cannot be less"
+ " than the min value.");
}
}
@Override
boolean updateValueAndVelocity(long deltaT) {
// If user had requested end, then update the value and velocity to end state and consider
// animation done.
if (mEndRequested) {
if (mPendingPosition != UNSET) {
mSpring.setFinalPosition(mPendingPosition);
mPendingPosition = UNSET;
}
mValue = mSpring.getFinalPosition();
mVelocity = 0;
mEndRequested = false;
return true;
}
if (mPendingPosition != UNSET) {
// Approximate by considering half of the time spring position stayed at the old
// position, half of the time it's at the new position.
MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2);
mSpring.setFinalPosition(mPendingPosition);
mPendingPosition = UNSET;
massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2);
mValue = massState.mValue;
mVelocity = massState.mVelocity;
} else {
MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT);
mValue = massState.mValue;
mVelocity = massState.mVelocity;
}
mValue = Math.max(mValue, mMinValue);
mValue = Math.min(mValue, mMaxValue);
if (isAtEquilibrium(mValue, mVelocity)) {
mValue = mSpring.getFinalPosition();
mVelocity = 0f;
return true;
}
return false;
}
@Override
float getAcceleration(float value, float velocity) {
return mSpring.getAcceleration(value, velocity);
}
@Override
boolean isAtEquilibrium(float value, float velocity) {
return mSpring.isAtEquilibrium(value, velocity);
}
@Override
void setValueThreshold(float threshold) {
}
}
@@ -0,0 +1,330 @@
/*
* Copyright (C) 2017 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 androidx.dynamicanimation.animation;
import androidx.annotation.FloatRange;
import androidx.annotation.RestrictTo;
/**
* Spring Force defines the characteristics of the spring being used in the animation.
* <p>
* By configuring the stiffness and damping ratio, callers can create a spring with the look and
* feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring
* is, the harder it is to stretch it, the faster it undergoes dampening.
* <p>
* Spring damping ratio describes how oscillations in a system decay after a disturbance.
* When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position
* without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
* return to equilibrium within the shortest amount of time. When damping ratio is less than 1
* (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any
* damping (i.e. damping ratio = 0), the mass will oscillate forever.
*/
public final class SpringForce implements Force {
/**
* Stiffness constant for extremely stiff spring.
*/
public static final float STIFFNESS_HIGH = 10_000f;
/**
* Stiffness constant for medium stiff spring. This is the default stiffness for spring force.
*/
public static final float STIFFNESS_MEDIUM = 1500f;
/**
* Stiffness constant for a spring with low stiffness.
*/
public static final float STIFFNESS_LOW = 200f;
/**
* Stiffness constant for a spring with very low stiffness.
*/
public static final float STIFFNESS_VERY_LOW = 50f;
/**
* Damping ratio for a very bouncy spring. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
*/
public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f;
/**
* Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring
* force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio,
* the more bouncy the spring.
*/
public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f;
/**
* Damping ratio for a spring with low bounciness. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
*/
public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f;
/**
* Damping ratio for a spring with no bounciness. This damping ratio will create a critically
* damped spring that returns to equilibrium within the shortest amount of time without
* oscillating.
*/
public static final float DAMPING_RATIO_NO_BOUNCY = 1f;
// This multiplier is used to calculate the velocity threshold given a certain value threshold.
// The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity
// is a reasonable threshold.
private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0;
// Natural frequency
double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM);
// Damping ratio.
double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY;
// Value to indicate an unset state.
private static final double UNSET = Double.MAX_VALUE;
// Indicates whether the spring has been initialized
private boolean mInitialized = false;
// Threshold for velocity and value to determine when it's reasonable to assume that the spring
// is approximately at rest.
private double mValueThreshold;
private double mVelocityThreshold;
// Intermediate values to simplify the spring function calculation per frame.
private double mGammaPlus;
private double mGammaMinus;
private double mDampedFreq;
// Final position of the spring. This must be set before the start of the animation.
private double mFinalPosition = UNSET;
// Internal state to hold a value/velocity pair.
private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState();
/**
* Creates a spring force. Note that final position of the spring must be set through
* {@link #setFinalPosition(float)} before the spring animation starts.
*/
public SpringForce() {
// No op.
}
/**
* Creates a spring with a given final rest position.
*
* @param finalPosition final position of the spring when it reaches equilibrium
*/
public SpringForce(float finalPosition) {
mFinalPosition = finalPosition;
}
/**
* Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to
* the object attached when the spring is not at the final position. Default stiffness is
* {@link #STIFFNESS_MEDIUM}.
*
* @param stiffness non-negative stiffness constant of a spring
* @return the spring force that the given stiffness is set on
* @throws IllegalArgumentException if the given spring stiffness is not positive
*/
public SpringForce setStiffness(
@FloatRange(from = 0.0, fromInclusive = false) float stiffness) {
if (stiffness <= 0) {
throw new IllegalArgumentException("Spring stiffness constant must be positive.");
}
mNaturalFreq = Math.sqrt(stiffness);
// All the intermediate values need to be recalculated.
mInitialized = false;
return this;
}
/**
* Gets the stiffness of the spring.
*
* @return the stiffness of the spring
*/
public float getStiffness() {
return (float) (mNaturalFreq * mNaturalFreq);
}
/**
* Spring damping ratio describes how oscillations in a system decay after a disturbance.
* <p>
* When damping ratio > 1 (over-damped), the object will quickly return to the rest position
* without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will
* return to equilibrium within the shortest amount of time. When damping ratio is less than 1
* (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without
* any damping (i.e. damping ratio = 0), the mass will oscillate forever.
* <p>
* Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}.
*
* @param dampingRatio damping ratio of the spring, it should be non-negative
* @return the spring force that the given damping ratio is set on
* @throws IllegalArgumentException if the {@param dampingRatio} is negative.
*/
public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) {
if (dampingRatio < 0) {
throw new IllegalArgumentException("Damping ratio must be non-negative");
}
mDampingRatio = dampingRatio;
// All the intermediate values need to be recalculated.
mInitialized = false;
return this;
}
/**
* Returns the damping ratio of the spring.
*
* @return damping ratio of the spring
*/
public float getDampingRatio() {
return (float) mDampingRatio;
}
/**
* Sets the rest position of the spring.
*
* @param finalPosition rest position of the spring
* @return the spring force that the given final position is set on
*/
public SpringForce setFinalPosition(float finalPosition) {
mFinalPosition = finalPosition;
return this;
}
/**
* Returns the rest position of the spring.
*
* @return rest position of the spring
*/
public float getFinalPosition() {
return (float) mFinalPosition;
}
/*********************** Below are private APIs *********************/
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public float getAcceleration(float lastDisplacement, float lastVelocity) {
lastDisplacement -= getFinalPosition();
double k = mNaturalFreq * mNaturalFreq;
double c = 2 * mNaturalFreq * mDampingRatio;
return (float) (-k * lastDisplacement - c * lastVelocity);
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean isAtEquilibrium(float value, float velocity) {
if (Math.abs(velocity) < mVelocityThreshold
&& Math.abs(value - getFinalPosition()) < mValueThreshold) {
return true;
}
return false;
}
/**
* Initialize the string by doing the necessary pre-calculation as well as some sanity check
* on the setup.
*
* @throws IllegalStateException if the final position is not yet set by the time the spring
* animation has started
*/
private void init() {
if (mInitialized) {
return;
}
if (mFinalPosition == UNSET) {
throw new IllegalStateException("Error: Final position of the spring must be"
+ " set before the animation starts");
}
if (mDampingRatio > 1) {
// Over damping
mGammaPlus = -mDampingRatio * mNaturalFreq
+ mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
mGammaMinus = -mDampingRatio * mNaturalFreq
- mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1);
} else if (mDampingRatio >= 0 && mDampingRatio < 1) {
// Under damping
mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio);
}
mInitialized = true;
}
/**
* Internal only call for Spring to calculate the spring position/velocity using
* an analytical approach.
*/
DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity,
long timeElapsed) {
init();
double deltaT = timeElapsed / 1000d; // unit: seconds
lastDisplacement -= mFinalPosition;
double displacement;
double currentVelocity;
if (mDampingRatio > 1) {
// Overdamped
double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity)
/ (mGammaMinus - mGammaPlus);
double coeffB = (mGammaMinus * lastDisplacement - lastVelocity)
/ (mGammaMinus - mGammaPlus);
displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT)
+ coeffB * Math.pow(Math.E, mGammaPlus * deltaT);
currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT)
+ coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT);
} else if (mDampingRatio == 1) {
// Critically damped
double coeffA = lastDisplacement;
double coeffB = lastVelocity + mNaturalFreq * lastDisplacement;
displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT);
currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT)
* -mNaturalFreq + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT);
} else {
// Underdamped
double cosCoeff = lastDisplacement;
double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq
* lastDisplacement + lastVelocity);
displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
* (cosCoeff * Math.cos(mDampedFreq * deltaT)
+ sinCoeff * Math.sin(mDampedFreq * deltaT));
currentVelocity = displacement * -mNaturalFreq * mDampingRatio
+ Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT)
* (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT)
+ mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT));
}
mMassState.mValue = (float) (displacement + mFinalPosition);
mMassState.mVelocity = (float) currentVelocity;
return mMassState;
}
/**
* This threshold defines how close the animation value needs to be before the animation can
* finish. This default value is based on the property being animated, e.g. animations on alpha,
* scale, translation or rotation would have different thresholds. This value should be small
* enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that
* animations take seconds to finish.
*
* @param threshold the difference between the animation value and final spring position that
* is allowed to end the animation when velocity is very low
*/
void setValueThreshold(double threshold) {
mValueThreshold = Math.abs(threshold);
mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER;
}
}
+18 -7
View File
@@ -7,11 +7,11 @@ plugins {
id 'com.android.library' version "8.7.0" apply false
id 'com.android.test' version '8.7.0' apply false
id 'androidx.baselineprofile' version '1.3.2'
id 'org.jetbrains.kotlin.kapt' version "1.9.20" apply false
id 'org.jetbrains.kotlin.android' version "2.0.20"
id 'org.jetbrains.kotlin.plugin.compose' version "2.0.20"
id 'org.jetbrains.kotlin.plugin.parcelize' version "2.0.20"
id 'org.jetbrains.kotlin.plugin.serialization' version "2.0.20"
id "com.google.devtools.ksp" version "2.0.20-1.0.25"
id 'com.google.protobuf' version "0.9.4"
id 'app.cash.licensee' version "1.11.0"
id 'dev.rikka.tools.refine' version "4.4.0"
@@ -19,6 +19,9 @@ plugins {
id 'com.diffplug.spotless' version '6.25.0'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
allprojects {
plugins.withType(AndroidBasePlugin).configureEach {
apply plugin: 'org.gradle.android.cache-fix'
@@ -359,7 +362,7 @@ dependencies {
implementation 'androidx.profileinstaller:profileinstaller:1.4.1'
baselineProfile projects.baselineProfile
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
implementation projects.androidxLib
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.preference:preference-ktx:1.2.1"
@@ -405,9 +408,14 @@ dependencies {
implementation "com.squareup.retrofit2:converter-kotlinx-serialization:$retrofitVersion"
def roomVersion = '2.6.1'
implementation "androidx.room:room-runtime:$roomVersion"
annotationProcessor "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-runtime:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
ksp "androidx.room:room-compiler:$roomVersion"
//noinspection KaptUsageInsteadOfKsp
kapt "androidx.room:room-compiler:$roomVersion"
def core_version = "1.13.1"
implementation "androidx.core:core:$core_version"
@@ -430,12 +438,15 @@ dependencies {
implementation("com.github.android:renderscript-intrinsics-replacement-toolkit:b6363490c3")
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
arg("room.incremental", "true")
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
arg("room.incremental", "true")
}
}
spotless {
java {
target("compatLib/**/src/**/*.java")
File diff suppressed because it is too large. Load diff
@@ -61,8 +61,7 @@ import java.util.HashMap;
import java.util.StringJoiner;
/**
* Track LauncherState, RecentsAnimation, resumed state for task bar in one
* place here and animate
* Track LauncherState, RecentsAnimation, resumed state for task bar in one place here and animate
* the task bar accordingly.
*/
public class TaskbarLauncherStateController {
@@ -74,14 +73,12 @@ public class TaskbarLauncherStateController {
public static final int FLAG_VISIBLE = 1 << 0;
/**
* A external transition / animation is running that will result in FLAG_VISIBLE
* being set.
* A external transition / animation is running that will result in FLAG_VISIBLE being set.
**/
public static final int FLAG_TRANSITION_TO_VISIBLE = 1 << 1;
/**
* Set while the launcher state machine is performing a state transition, see
* {@link
* Set while the launcher state machine is performing a state transition, see {@link
* StateManager.StateListener}.
*/
public static final int FLAG_LAUNCHER_IN_STATE_TRANSITION = 1 << 2;
@@ -94,14 +91,11 @@ public class TaskbarLauncherStateController {
private static final int FLAG_AWAKE = 1 << 3;
/**
* Captures whether the launcher was active at the time the FLAG_AWAKE was
* cleared.
* Captures whether the launcher was active at the time the FLAG_AWAKE was cleared.
* Always cleared when FLAG_AWAKE is set.
* <p>
* FLAG_RESUMED will be cleared when the device is asleep, since all apps get
* paused at this
* point. Thus, this flag indicates whether the launcher will be shown when the
* device wakes up
* FLAG_RESUMED will be cleared when the device is asleep, since all apps get paused at this
* point. Thus, this flag indicates whether the launcher will be shown when the device wakes up
* again.
*/
private static final int FLAG_LAUNCHER_WAS_ACTIVE_WHILE_AWAKE = 1 << 4;
@@ -109,23 +103,18 @@ public class TaskbarLauncherStateController {
/**
* Whether the device is currently locked.
* <ul>
* <li>While locked, the taskbar is always stashed.
* <li/>
* <li>Navbar animations on FLAG_DEVICE_LOCKED transitions will get special
* treatment.</li>
* <li>While locked, the taskbar is always stashed.<li/>
* <li>Navbar animations on FLAG_DEVICE_LOCKED transitions will get special treatment.</li>
* </ul>
*/
private static final int FLAG_DEVICE_LOCKED = 1 << 5;
/**
* Whether the complete taskbar is completely hidden (neither visible stashed or
* unstashed).
* This is tracked to allow a nice transition of the taskbar before SysUI forces
* it away by
* Whether the complete taskbar is completely hidden (neither visible stashed or unstashed).
* This is tracked to allow a nice transition of the taskbar before SysUI forces it away by
* hiding the inset.
*
* This flag is predominanlty set while FLAG_DEVICE_LOCKED is set, thus the
* taskbar's invisible
* This flag is predominanlty set while FLAG_DEVICE_LOCKED is set, thus the taskbar's invisible
* resting state while hidden is stashed.
*/
private static final int FLAG_TASKBAR_HIDDEN = 1 << 6;
@@ -141,15 +130,14 @@ public class TaskbarLauncherStateController {
/**
* Delay for the taskbar fade-in.
*
* Helps to avoid visual noise when unlocking successfully via SFPS, and the
* device transitions
* to launcher directly. The delay avoids the navbar to become briefly visible.
* The duration
* Helps to avoid visual noise when unlocking successfully via SFPS, and the device transitions
* to launcher directly. The delay avoids the navbar to become briefly visible. The duration
* is the same as in SysUI, see http://shortn/_uNSbDoRUSr.
*/
private static final long TASKBAR_SHOW_DELAY_MS = 250;
private final AnimatedFloat mIconAlignment = new AnimatedFloat(this::onIconAlignmentRatioChanged);
private final AnimatedFloat mIconAlignment =
new AnimatedFloat(this::onIconAlignmentRatioChanged);
private TaskbarControllers mControllers;
private AnimatedFloat mTaskbarBackgroundAlpha;
@@ -163,8 +151,7 @@ public class TaskbarLauncherStateController {
private LauncherState mLauncherState = LauncherState.NORMAL;
private boolean mSkipNextRecentsAnimEnd;
// Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime
// (milliseconds).
// Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds).
private long mLastUnlockTimeMs = 0;
private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener;
@@ -178,12 +165,23 @@ public class TaskbarLauncherStateController {
private boolean mIsQsbInline;
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener=new DeviceProfile.OnDeviceProfileChangeListener(){@Override public void onDeviceProfileChanged(DeviceProfile dp){if(mIsQsbInline&&!dp.isQsbInline){
// We only modify QSB alpha if isQsbInline = true. If we switch to a DP
// where isQsbInline = false, then we need to reset the alpha.
mLauncher.getHotseat().setQsbAlpha(1f);}mIsQsbInline=dp.isQsbInline;TaskbarLauncherStateController.this.updateIconAlphaForHome(mIconAlphaForHome.getValue());}};
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
new DeviceProfile.OnDeviceProfileChangeListener() {
@Override
public void onDeviceProfileChanged(DeviceProfile dp) {
if (mIsQsbInline && !dp.isQsbInline) {
// We only modify QSB alpha if isQsbInline = true. If we switch to a DP
// where isQsbInline = false, then we need to reset the alpha.
mLauncher.getHotseat().setQsbAlpha(1f);
}
mIsQsbInline = dp.isQsbInline;
TaskbarLauncherStateController.this.updateIconAlphaForHome(
mIconAlphaForHome.getValue());
}
};
private final StateManager.StateListener<LauncherState> mStateListener = new StateManager.StateListener<LauncherState>() {
private final StateManager.StateListener<LauncherState> mStateListener =
new StateManager.StateListener<LauncherState>() {
@Override
public void onStateTransitionStart(LauncherState toState) {
@@ -203,23 +201,18 @@ public class TaskbarLauncherStateController {
}
}
}
}
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
mLauncherState = finalState;
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
applyState();
updateOverviewDragState(finalState);
}
};
@Override
public void onStateTransitionComplete(LauncherState finalState) {
mLauncherState = finalState;
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
applyState();
updateOverviewDragState(finalState);
}
};
/**
* Callback for when launcher state transition completes after user swipes to
* home.
*
* Callback for when launcher state transition completes after user swipes to home.
* @param finalState The final state of the transition.
*/
public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
@@ -232,10 +225,7 @@ public class TaskbarLauncherStateController {
}
}
/**
* Initializes the controller instance, and applies the initial state
* immediately.
*/
/** Initializes the controller instance, and applies the initial state immediately. */
public void init(TaskbarControllers controllers, QuickstepLauncher launcher,
@SystemUiStateFlags long sysuiStateFlags) {
mCanSyncViews = false;
@@ -256,7 +246,7 @@ public class TaskbarLauncherStateController {
mLauncher.getStateManager().addStateListener(mStateListener);
mLauncherState = launcher.getStateManager().getState();
updateStateForSysuiFlags(sysuiStateFlags, /* applyState */ false);
updateStateForSysuiFlags(sysuiStateFlags, /*applyState*/ false);
applyState(0);
@@ -280,8 +270,7 @@ public class TaskbarLauncherStateController {
/**
* Creates a transition animation to the launcher activity.
*
* Warning: the resulting animation must be played, since this method has side
* effects on this
* Warning: the resulting animation must be played, since this method has side effects on this
* controller's state.
*/
public Animator createAnimToLauncher(@NonNull LauncherState toState,
@@ -290,8 +279,7 @@ public class TaskbarLauncherStateController {
// If going home, align the icons to hotseat
AnimatorSet animatorSet = new AnimatorSet();
// Update stashed flags first to ensure goingToUnstashedLauncherState() returns
// correctly.
// Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly.
TaskbarStashController stashController = mControllers.taskbarStashController;
stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE,
toState.isTaskbarStashed(mLauncher));
@@ -306,12 +294,12 @@ public class TaskbarLauncherStateController {
if (mTaskBarRecentsAnimationListener != null) {
mTaskBarRecentsAnimationListener.endGestureStateOverride(
!mLauncher.isInState(LauncherState.OVERVIEW), false /* canceled */);
!mLauncher.isInState(LauncherState.OVERVIEW), false /*canceled*/);
}
mTaskBarRecentsAnimationListener = new TaskBarRecentsAnimationListener(callbacks);
callbacks.addListener(mTaskBarRecentsAnimationListener);
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(
() -> mTaskBarRecentsAnimationListener.endGestureStateOverride(true, false /* canceled */));
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(() ->
mTaskBarRecentsAnimationListener.endGestureStateOverride(true, false /*canceled*/));
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchCancelledRunnable(() -> {
updateStateForUserFinishedToApp(false /* finishedToApp */);
@@ -325,8 +313,7 @@ public class TaskbarLauncherStateController {
public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) {
if (!shouldDelayLauncherStateAnim && mShouldDelayLauncherStateAnim) {
// Animate the animation we have delayed immediately. This is usually triggered
// when
// Animate the animation we have delayed immediately. This is usually triggered when
// the user has released their finger.
applyState();
}
@@ -358,14 +345,10 @@ public class TaskbarLauncherStateController {
updateStateForFlag(FLAG_DEVICE_LOCKED, SystemUiFlagUtils.isLocked(systemUiStateFlags));
// Taskbar is hidden whenever the device is dreaming. The dreaming state
// includes the
// interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING
// only kicks in
// when the device is asleep, the second condition extends ensures that the
// transition from
// and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the
// taskbar
// Taskbar is hidden whenever the device is dreaming. The dreaming state includes the
// interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in
// when the device is asleep, the second condition extends ensures that the transition from
// and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar
// hide/reveal animation timings.
boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING)
|| (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE;
@@ -377,15 +360,15 @@ public class TaskbarLauncherStateController {
}
/**
* Updates overview drag state on various controllers based on
* {@link #mLauncherState}.
* Updates overview drag state on various controllers based on {@link #mLauncherState}.
*
* @param launcherState The current state launcher is in
*/
private void updateOverviewDragState(LauncherState launcherState) {
boolean disallowLongClick = FeatureFlags.enableSplitContextually()
? mLauncher.isSplitSelectionActive()
: launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
boolean disallowLongClick =
FeatureFlags.enableSplitContextually()
? mLauncher.isSplitSelectionActive()
: launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
com.android.launcher3.taskbar.Utilities.setOverviewDragState(
mControllers, launcherState.disallowTaskbarGlobalDrag(),
disallowLongClick, launcherState.allowTaskbarInitialSplitSelection());
@@ -394,8 +377,7 @@ public class TaskbarLauncherStateController {
/**
* Updates the proper flag to change the state of the task bar.
*
* Note that this only updates the flag. {@link #applyState()} needs to be
* called separately.
* Note that this only updates the flag. {@link #applyState()} needs to be called separately.
*
* @param flag The flag to update.
* @param enabled Whether to enable the flag
@@ -489,8 +471,7 @@ public class TaskbarLauncherStateController {
handleOpenFloatingViews = true;
}
if (mLauncherState == LauncherState.OVERVIEW) {
// Calling to update the insets in
// TaskbarInsetController#updateInsetsTouchability
// Calling to update the insets in TaskbarInsetController#updateInsetsTouchability
mControllers.taskbarActivityContext.notifyUpdateLayoutParams();
}
}
@@ -501,7 +482,8 @@ public class TaskbarLauncherStateController {
public void onAnimationStart(Animator animation) {
mIsAnimatingToLauncher = isInLauncher;
TaskbarStashController stashController = mControllers.taskbarStashController;
TaskbarStashController stashController =
mControllers.taskbarStashController;
if (DEBUG) {
Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !isInLauncher);
}
@@ -538,83 +520,124 @@ public class TaskbarLauncherStateController {
// Stash the transient taskbar once the taskbar is not visible. This reduces
// visual noise when unlocking the device afterwards.
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
TaskbarStashController stashController =
mControllers.taskbarStashController;
stashController.updateAndAnimateTransientTaskbar(
/* stash */ true, /* bubblesShouldFollow */ true);
}
});
} else {
// delay the fade in animation a bit to reduce visual noise when waking up a device
// with a fingerprint reader. This should only be done when the device was woken
// up via fingerprint reader, however since this information is currently not
// available, opting to always delay the fade-in a bit.
long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs;
taskbarVisibility.setStartDelay(
Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs));
}
animatorSet.play(taskbarVisibility);
}
@Override
public void onAnimationEnd(Animator animation) {
TaskbarStashController stashController = mControllers.taskbarStashController;
stashController.updateAndAnimateTransientTaskbar(
/* stash */ true, /* bubblesShouldFollow */ true);
}});}else{
float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
// delay the fade in animation a bit to reduce visual noise when waking up a
// device
// with a fingerprint reader. This should only be done when the device was woken
// up via fingerprint reader, however since this information is currently not
// available, opting to always delay the fade-in a bit.
long durationSinceLastUnlockMs = SystemClock.elapsedRealtime()
- mLastUnlockTimeMs;taskbarVisibility.setStartDelay(Math.max(0,TASKBAR_SHOW_DELAY_MS-durationSinceLastUnlockMs));}animatorSet.play(taskbarVisibility);}
// Don't animate if background has reached desired value.
if (mTaskbarBackgroundAlpha.isAnimating()
|| mTaskbarBackgroundAlpha.value != backgroundAlpha) {
mTaskbarBackgroundAlpha.cancelAnimation();
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - "
+ mTaskbarBackgroundAlpha.value
+ " -> " + backgroundAlpha + ": " + duration);
}
float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat;
boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat;
boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat;
// Don't animate if background has reached desired value.
if(mTaskbarBackgroundAlpha.isAnimating()||mTaskbarBackgroundAlpha.value!=backgroundAlpha){mTaskbarBackgroundAlpha.cancelAnimation();if(DEBUG){Log.d(TAG,"onStateChangeApplied - taskbarBackgroundAlpha - "+mTaskbarBackgroundAlpha.value+" -> "+backgroundAlpha+": "+duration);}
float startDelay = 0;
// We want to delay the background from fading in so that the icons have time to move
// into the bounds of the background before it appears.
if (isInLauncherIconNotAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
} else if (notInLauncherIconNotAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
}
float newDuration = duration - startDelay;
if (isInLauncherIconIsAligned) {
// Make the background fade out faster so that it is gone by the time the
// icons move outside of the bounds of the background.
newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT;
}
Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
.animateToValue(backgroundAlpha)
.setDuration((long) newDuration);
taskbarBackgroundAlpha.setStartDelay((long) startDelay);
animatorSet.play(taskbarBackgroundAlpha);
}
boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat;
boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat;
boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat;
float cornerRoundness = isInLauncher ? 0 : 1;
float startDelay = 0;
// We want to delay the background from fading in so that the icons have time to
// move
// into the bounds of the background before it appears.
if(isInLauncherIconNotAligned){startDelay=duration*TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT;}else if(notInLauncherIconNotAligned){startDelay=duration*TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT;}
float newDuration = duration - startDelay;if(isInLauncherIconIsAligned){
// Make the background fade out faster so that it is gone by the time the
// icons move outside of the bounds of the background.
newDuration=duration*TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT;}
Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
.animateToValue(backgroundAlpha)
.setDuration(
(long) newDuration);taskbarBackgroundAlpha.setStartDelay((long)startDelay);animatorSet.play(taskbarBackgroundAlpha);}
// Don't animate if corner roundness has reached desired value.
if (mTaskbarCornerRoundness.isAnimating()
|| mTaskbarCornerRoundness.value != cornerRoundness) {
mTaskbarCornerRoundness.cancelAnimation();
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - taskbarCornerRoundness - "
+ mTaskbarCornerRoundness.value
+ " -> " + cornerRoundness + ": " + duration);
}
animatorSet.play(mTaskbarCornerRoundness.animateToValue(cornerRoundness));
}
float cornerRoundness = isInLauncher ? 0 : 1;
// Keep isUnlockTransition in sync with its counterpart in
// TaskbarStashController#createAnimToIsStashed.
boolean isUnlockTransition =
hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED);
if (isUnlockTransition) {
// When transitioning to unlocked, ensure the hotseat is fully visible from the
// beginning. The hotseat itself is animated by LauncherUnlockAnimationController.
mIconAlignment.cancelAnimation();
// updateValue ensures onIconAlignmentRatioChanged will be called if there is an actual
// change in value
mIconAlignment.updateValue(toAlignment);
// Don't animate if corner roundness has reached desired value.
if(mTaskbarCornerRoundness.isAnimating()||mTaskbarCornerRoundness.value!=cornerRoundness){mTaskbarCornerRoundness.cancelAnimation();if(DEBUG){Log.d(TAG,"onStateChangeApplied - taskbarCornerRoundness - "+mTaskbarCornerRoundness.value+" -> "+cornerRoundness+": "+duration);}animatorSet.play(mTaskbarCornerRoundness.animateToValue(cornerRoundness));}
// Make sure FLAG_IN_APP is set when launching applications from keyguard.
if (!isInLauncher) {
mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true);
mControllers.taskbarStashController.applyState(0);
}
} else if (mIconAlignment.isAnimatingToValue(toAlignment)
|| mIconAlignment.isSettledOnValue(toAlignment)) {
// Already at desired value, but make sure we run the callback at the end.
animatorSet.addListener(AnimatorListeners.forEndCallback(
this::onIconAlignmentRatioChanged));
} else {
mIconAlignment.cancelAnimation();
ObjectAnimator iconAlignAnim = mIconAlignment
.animateToValue(toAlignment)
.setDuration(duration);
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - iconAlignment - "
+ mIconAlignment.value
+ " -> " + toAlignment + ": " + duration);
}
animatorSet.play(iconAlignAnim);
}
// Keep isUnlockTransition in sync with its counterpart in
// TaskbarStashController#createAnimToIsStashed.
boolean isUnlockTransition = hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED)
&& !hasAnyFlag(FLAG_DEVICE_LOCKED);if(isUnlockTransition){
// When transitioning to unlocked, ensure the hotseat is fully visible from the
// beginning. The hotseat itself is animated by
// LauncherUnlockAnimationController.
mIconAlignment.cancelAnimation();
// updateValue ensures onIconAlignmentRatioChanged will be called if there is an
// actual
// change in value
mIconAlignment.updateValue(toAlignment);
animatorSet.setInterpolator(EMPHASIZED);
// Make sure FLAG_IN_APP is set when launching applications from keyguard.
if(!isInLauncher){mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP,true);mControllers.taskbarStashController.applyState(0);}}else if(mIconAlignment.isAnimatingToValue(toAlignment)||mIconAlignment.isSettledOnValue(toAlignment)){
// Already at desired value, but make sure we run the callback at the end.
animatorSet.addListener(AnimatorListeners.forEndCallback(this::onIconAlignmentRatioChanged));}else{mIconAlignment.cancelAnimation();
ObjectAnimator iconAlignAnim = mIconAlignment
.animateToValue(toAlignment)
.setDuration(
duration);if(DEBUG){Log.d(TAG,"onStateChangeApplied - iconAlignment - "+mIconAlignment.value+" -> "+toAlignment+": "+duration);
}animatorSet.play(iconAlignAnim);}
animatorSet.setInterpolator(EMPHASIZED);
if(start){animatorSet.start();}return animatorSet;}
if (start) {
animatorSet.start();
}
return animatorSet;
}
/**
* Whether the taskbar is aligned with the hotseat in the current/target
* launcher state.
* Whether the taskbar is aligned with the hotseat in the current/target launcher state.
*
* This refers to the intended state - a transition to this state might be in
* progress.
* This refers to the intended state - a transition to this state might be in progress.
*/
public boolean isTaskbarAlignedWithHotseat() {
return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
@@ -628,7 +651,8 @@ public class TaskbarLauncherStateController {
boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
boolean willStashVisually = isInStashedState
&& mControllers.taskbarStashController.supportsVisualStashing();
boolean isTaskbarAlignedWithHotseat = mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
boolean isTaskbarAlignedWithHotseat =
mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
return isTaskbarAlignedWithHotseat && !willStashVisually;
} else {
return false;
@@ -673,12 +697,9 @@ public class TaskbarLauncherStateController {
animatorSet.play(stashAnimator);
}
// Translate back to 0 at a shorter or same duration as the icon alignment
// animation.
// This ensures there is no jump after switching to hotseat, e.g. when swiping
// up from
// overview to home. When not in app, we do duration / 2 just to make it feel
// snappier.
// Translate back to 0 at a shorter or same duration as the icon alignment animation.
// This ensures there is no jump after switching to hotseat, e.g. when swiping up from
// overview to home. When not in app, we do duration / 2 just to make it feel snappier.
long resetDuration = mControllers.taskbarStashController.isInApp()
? duration
: duration / 2;
@@ -735,7 +756,7 @@ public class TaskbarLauncherStateController {
boolean hotseatVisible = alpha == 0
|| mControllers.taskbarActivityContext.isPhoneMode()
|| (!mControllers.uiController.isHotseatIconOnTopWhenAligned()
&& mIconAlignment.value > 0);
&& mIconAlignment.value > 0);
/*
* Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets
* should not be visible at the same time.
@@ -757,26 +778,23 @@ public class TaskbarLauncherStateController {
@Override
public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
boolean isInOverview = mLauncher.isInState(LauncherState.OVERVIEW);
endGestureStateOverride(!isInOverview, true /* canceled */);
endGestureStateOverride(!isInOverview, true /*canceled*/);
}
@Override
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /* canceled */);
endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/);
}
/**
* Handles whatever cleanup is needed after the recents animation is completed.
* NOTE: If {@link #mSkipNextRecentsAnimEnd} is set and we're coming from a
* non-cancelled
* NOTE: If {@link #mSkipNextRecentsAnimEnd} is set and we're coming from a non-cancelled
* path, this will not call {@link #updateStateForUserFinishedToApp(boolean)}
*
* @param finishedToApp {@code true} if the recents animation finished to
* showing an app and
* @param finishedToApp {@code true} if the recents animation finished to showing an app and
* not workspace or overview
* @param canceled {@code true} if the recents animation was canceled
* instead of finishing
* to completion
* @param canceled {@code true} if the recents animation was canceled instead of finishing
* to completion
*/
private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
mCallbacks.removeListener(this);
@@ -793,7 +811,6 @@ public class TaskbarLauncherStateController {
/**
* Updates the visible state immediately to ensure a seamless handoff.
*
* @param finishedToApp True iff user is in an app.
*/
private void updateStateForUserFinishedToApp(boolean finishedToApp) {
@@ -31,7 +31,6 @@ import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
@@ -62,13 +61,13 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Implements interfaces required to show and allow interacting with a
* PopupContainerWithArrow.
* Implements interfaces required to show and allow interacting with a PopupContainerWithArrow.
* Controls the long-press menu on Taskbar and AllApps icons.
*/
public class TaskbarPopupController implements TaskbarControllers.LoggableTaskbarController {
private static final SystemShortcut.Factory<BaseTaskbarContext> APP_INFO = SystemShortcut.AppInfo::new;
private static final SystemShortcut.Factory<BaseTaskbarContext>
APP_INFO = SystemShortcut.AppInfo::new;
private final TaskbarActivityContext mContext;
private final PopupDataProvider mPopupDataProvider;
@@ -162,12 +161,7 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (FeatureFlags.showMaterialUPopup(icon.getContext())) {
container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
R.layout.popup_container_material_u, context.getDragLayer(), false);
container.populateAndShowRowsMaterialU(icon, deepShortcutCount, systemShortcuts);
} else {
container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
R.layout.popup_container, context.getDragLayer(), false);
container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
@@ -187,11 +181,11 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
// Create a Stream of all applicable system shortcuts
private Stream<SystemShortcut.Factory> getSystemShortcuts() {
// append split options to APP_INFO shortcut, the order here will reflect in the
// popup
// append split options to APP_INFO shortcut, the order here will reflect in the popup
return Stream.concat(
Stream.of(APP_INFO),
mControllers.uiController.getSplitMenuOptions());
mControllers.uiController.getSplitMenuOptions()
);
}
@Override
@@ -206,8 +200,7 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
protected final Point mIconLastTouchPos = new Point();
TaskbarPopupItemDragHandler() {
}
TaskbarPopupItemDragHandler() {}
@Override
public boolean onTouch(View view, MotionEvent ev) {
@@ -225,8 +218,7 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
@Override
public boolean onLongClick(View v) {
// Return early if not the correct view
if (!(v.getParent() instanceof DeepShortcutView))
return false;
if (!(v.getParent() instanceof DeepShortcutView)) return false;
DeepShortcutView sv = (DeepShortcutView) v.getParent();
sv.setWillDrawIcon(false);
@@ -244,12 +236,9 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
}
/**
* Creates a factory function representing a single "split position" menu item
* ("Split left,"
* Creates a factory function representing a single "split position" menu item ("Split left,"
* "Split right," or "Split top").
*
* @param position A SplitPositionOption representing whether we are splitting
* top, left, or
* @param position A SplitPositionOption representing whether we are splitting top, left, or
* right.
* @return A factory function to be used in populating the long-press menu.
*/
@@ -259,47 +248,43 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
originalView, position, mAllowInitialSplitSelection);
}
/**
* A single menu item ("Split left," "Split right," or "Split top") that
* executes a split
/**
* A single menu item ("Split left," "Split right," or "Split top") that executes a split
* from the taskbar, as if the user performed a drag and drop split.
* Includes an onClick method that initiates the actual split.
*/
private static class TaskbarSplitShortcut extends
SplitShortcut<BaseTaskbarContext> {
/**
* If {@code true}, clicking this shortcut will not attempt to start a split app
* directly,
* but be the first app in split selection mode
*/
private final boolean mAllowInitialSplitSelection;
SplitShortcut<BaseTaskbarContext> {
/**
* If {@code true}, clicking this shortcut will not attempt to start a split app directly,
* but be the first app in split selection mode
*/
private final boolean mAllowInitialSplitSelection;
TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView,
TaskbarSplitShortcut(BaseTaskbarContext context, ItemInfo itemInfo, View originalView,
SplitPositionOption position, boolean allowInitialSplitSelection) {
super(position.iconResId, position.textResId, context, itemInfo, originalView,
position);
mAllowInitialSplitSelection = allowInitialSplitSelection;
}
super(position.iconResId, position.textResId, context, itemInfo, originalView,
position);
mAllowInitialSplitSelection = allowInitialSplitSelection;
}
@Override
public void onClick(View view) {
// Add callbacks depending on what type of Taskbar context we're in (Taskbar or
// AllApps)
// Add callbacks depending on what type of Taskbar context we're in (Taskbar or AllApps)
mTarget.onSplitScreenMenuButtonClicked();
AbstractFloatingView.closeAllOpenViews(mTarget);
// Depending on what app state we're in, we either want to initiate the split
// screen
// Depending on what app state we're in, we either want to initiate the split screen
// staging process or immediately launch a split with an existing app.
// - Initiate the split screen staging process
if (mAllowInitialSplitSelection) {
super.onClick(view);
return;
}
if (mAllowInitialSplitSelection) {
super.onClick(view);
return;
}
// - Immediately launch split with the running app
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds = LogUtils
.getShellShareableInstanceId();
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
mTarget.getStatsLogManager().logger()
.withItemInfo(mItemInfo)
.withInstanceId(instanceIds.second)
@@ -329,3 +314,4 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
}
}
}
@@ -16,6 +16,7 @@
package com.android.launcher3.uioverrides;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.os.Trace.TRACE_TAG_APP;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
@@ -78,10 +79,12 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.display.DisplayManager;
import android.media.permission.SafeCloseable;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.SystemProperties;
import android.os.Trace;
import android.util.AttributeSet;
import android.view.Display;
import android.view.HapticFeedbackConstants;
@@ -94,9 +97,11 @@ import android.window.OnBackAnimationCallback;
import android.window.OnBackInvokedDispatcher;
import android.window.RemoteTransition;
import android.window.SplashScreen;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.launcher3.AbstractFloatingView;
@@ -106,7 +111,6 @@ import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherState;
import com.android.launcher3.OnBackPressedHandler;
import com.android.launcher3.QuickstepAccessibilityDelegate;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
@@ -207,12 +211,12 @@ import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import app.lawnchair.compat.LawnchairQuickstepCompat;
public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
private static final boolean TRACE_LAYOUTS = SystemProperties.getBoolean("persist.debug.trace_layouts", false);
private static final String TRACE_RELAYOUT_CLASS = SystemProperties.get("persist.debug.trace_request_layout_class",
null);
private static final boolean TRACE_LAYOUTS =
SystemProperties.getBoolean("persist.debug.trace_layouts", false);
private static final String TRACE_RELAYOUT_CLASS =
SystemProperties.get("persist.debug.trace_request_layout_class", null);
public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t";
@@ -229,13 +233,13 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
// Will be updated when dragging from taskbar.
private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
private SplitSelectStateController mSplitSelectStateController;
private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
private SplitToWorkspaceController mSplitToWorkspaceController;
/**
* If Launcher restarted while in the middle of an Overview split select, it
* needs this data to
* If Launcher restarted while in the middle of an Overview split select, it needs this data to
* recover. In all other cases this will remain null.
*/
private PendingSplitSelectInfo mPendingSplitSelectInfo = null;
@@ -244,6 +248,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
private SafeCloseable mViewCapture;
private boolean mEnableWidgetDepth;
private boolean mIsPredictiveBackToHomeInProgress;
@@ -255,16 +260,17 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
protected void setupViews() {
super.setupViews();
mActionsView = findViewById(R.id.overview_actions_view);
RecentsView<?, ?> overviewPanel = getOverviewPanel();
RecentsView<?,?> overviewPanel = getOverviewPanel();
SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
mSplitSelectStateController = new SplitSelectStateController(this, mHandler, getStateManager(),
getDepthController(), getStatsLogManager(),
systemUiProxy, RecentsModel.INSTANCE.get(this),
() -> onStateBack());
mSplitSelectStateController =
new SplitSelectStateController(this, mHandler, getStateManager(),
getDepthController(), getStatsLogManager(),
systemUiProxy, RecentsModel.INSTANCE.get(this),
() -> onStateBack());
RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(asContext());
// TODO(b/337863494): Explore use of the same OverviewComponentObserver across
// launcher
// TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
asContext(), deviceState);
if (enableDesktopWindowingMode()) {
@@ -280,9 +286,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
mSplitSelectStateController);
mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
mActionsView.updateVerticalMargin(DisplayController.getNavigationMode(this));
mAppTransitionManager = buildAppTransitionManager();
mAppTransitionManager.registerRemoteAnimations();
mAppTransitionManager.registerRemoteTransitions();
mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
mDepthController = new DepthController(this);
if (enableDesktopWindowingMode()) {
@@ -292,26 +300,28 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
overviewComponentObserver);
}
mHotseatPredictionController = new HotseatPredictionController(this);
mEnableWidgetDepth = SystemProperties.getBoolean("ro.launcher.depth.widget", true);
getWorkspace().addOverlayCallback(
progress -> onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX));
getWorkspace().addOverlayCallback(progress ->
onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX));
addBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler());
}
@Override
public void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info,
InstanceId instanceId) {
// If the app launch is from any of the surfaces in AllApps then add the
// InstanceId from
// If the app launch is from any of the surfaces in AllApps then add the InstanceId from
// LiveSearchManager to recreate the AllApps session on the server side.
if (mAllAppsSessionLogId != null && ALL_APPS.equals(
getStateManager().getCurrentStableState())) {
instanceId = mAllAppsSessionLogId;
}
StatsLogger logger = statsLogManager.logger().withItemInfo(info).withInstanceId(instanceId);
if (mAllAppsPredictions != null
&& (info.itemType == ITEM_TYPE_APPLICATION
|| info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) {
|| info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) {
int count = mAllAppsPredictions.items.size();
for (int i = 0; i < count; i++) {
ItemInfo targetInfo = mAllAppsPredictions.items.get(i);
@@ -321,9 +331,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
logger.withRank(i);
break;
}
}
}
logger.log(LAUNCHER_APP_LAUNCH_TAP);
mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
}
@@ -355,8 +367,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
/**
* Builds the {@link QuickstepTransitionManager} instance to use for managing
* transitions.
* Builds the {@link QuickstepTransitionManager} instance to use for managing transitions.
*/
protected QuickstepTransitionManager buildAppTransitionManager() {
return new QuickstepTransitionManager(this);
@@ -370,11 +381,10 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public RunnableList startActivitySafely(View v, Intent intent, ItemInfo item) {
// Only pause is taskbar controller is not present until the transition (if it
// exists) ends
// Only pause is taskbar controller is not present until the transition (if it exists) ends
mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null);
PredictionRowView<?> predictionRowView = getAppsView().getFloatingHeaderView()
.findFixedRowByType(PredictionRowView.class);
PredictionRowView<?> predictionRowView =
getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class);
// Pause the prediction row updates until the transition (if it exists) ends.
predictionRowView.setPredictionUiUpdatePaused(true);
RunnableList result = super.startActivitySafely(v, intent, item);
@@ -401,11 +411,13 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
if ((changeBits & ACTIVITY_STATE_STARTED) != 0) {
mDepthController.setActivityStarted(isStarted());
}
if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) {
if (!FeatureFlags.enableHomeTransitionListener() && mTaskbarUIController != null) {
mTaskbarUIController.onLauncherVisibilityChanged(hasBeenResumed());
}
}
super.onActivityFlagsChanged(changeBits);
if ((changeBits & (ACTIVITY_STATE_DEFERRED_RESUMED | ACTIVITY_STATE_STARTED
| ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
@@ -454,11 +466,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
if (!mDeviceProfile.isTablet || mSplitSelectStateController.isSplitSelectActive()) {
return Collections.emptyList();
}
RecentsView recentsView = (RecentsView) getOverviewPanel();
// TODO(b/266482558): Pull it out of PagedOrentationHandler for split from
// workspace.
List<SplitPositionOption> positions = recentsView.getPagedOrientationHandler().getSplitPositionOptions(
mDeviceProfile);
RecentsView recentsView = getOverviewPanel();
// TODO(b/266482558): Pull it out of PagedOrentationHandler for split from workspace.
List<SplitPositionOption> positions =
recentsView.getPagedOrientationHandler().getSplitPositionOptions(
mDeviceProfile);
List<SystemShortcut.Factory<QuickstepLauncher>> splitShortcuts = new ArrayList<>();
for (SplitPositionOption position : positions) {
splitShortcuts.add(getSplitSelectShortcutByPosition(position));
@@ -467,15 +479,15 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
/**
* Recents logic that triggers when launcher state changes or launcher activity
* stops/resumes.
* Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
*/
private void onStateOrResumeChanging(boolean inTransition) {
LauncherState state = getStateManager().getState();
boolean started = ((getActivityFlags() & ACTIVITY_STATE_STARTED)) != 0;
if (started) {
DeviceProfile profile = getDeviceProfile();
boolean willUserBeActive = (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
boolean willUserBeActive =
(getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
boolean visible = (state == NORMAL || state == OVERVIEW)
&& (willUserBeActive || isUserActive())
&& !profile.isVerticalBarLayout();
@@ -491,8 +503,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
public void bindExtraContainerItems(FixedContainerItems item) {
if (item.containerId == Favorites.CONTAINER_PREDICTION) {
mAllAppsPredictions = item;
PredictionRowView<?> predictionRowView = getAppsView().getFloatingHeaderView().findFixedRowByType(
PredictionRowView.class);
PredictionRowView<?> predictionRowView =
getAppsView().getFloatingHeaderView().findFixedRowByType(
PredictionRowView.class);
predictionRowView.setPredictedApps(item.items);
} else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
mHotseatPredictionController.setPredictedItems(item);
@@ -519,6 +532,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
SystemUiProxy.INSTANCE.get(this).setUnfoldAnimationListener(null);
mUnfoldTransitionProgressProvider.destroy();
}
mTISBindHelper.onDestroy();
if (mLauncherUnfoldAnimationController != null) {
@@ -544,6 +558,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
public void onStateSetEnd(LauncherState state) {
super.onStateSetEnd(state);
handlePendingActivityRequest();
switch (state.ordinal) {
case HINT_STATE_ORDINAL: {
Workspace<?> workspace = getWorkspace();
@@ -559,7 +574,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
break;
}
case OVERVIEW_STATE_ORDINAL: {
RecentsView rv = (RecentsView) getOverviewPanel();
RecentsView rv = getOverviewPanel();
sendCustomAccessibilityEvent(
rv.getPageAt(rv.getCurrentPage()), TYPE_VIEW_FOCUSED, null);
break;
@@ -581,12 +596,14 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
break;
}
}
}
@Override
public TouchController[] createTouchControllers() {
NavigationMode mode = DisplayController.getNavigationMode(this);
ArrayList<TouchController> list = new ArrayList<>();
list.add(getDragController());
BiConsumer<AnimatorSet, Long> splitAnimator = (animatorSet, duration) ->
@@ -616,9 +633,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
list.add(new PortraitStatesTouchController(this));
break;
}
if (!getDeviceProfile().isMultiWindowMode) {
list.add(new StatusBarTouchController(this));
}
list.add(new LauncherTaskViewController(this));
return list.toArray(new TouchController[list.size()]);
}
@@ -630,8 +649,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
protected LauncherWidgetHolder createAppWidgetHolder() {
final QuickstepHolderFactory factory = (QuickstepHolderFactory) LauncherWidgetHolder.HolderFactory
.newFactory(this);
final QuickstepHolderFactory factory =
(QuickstepHolderFactory) LauncherWidgetHolder.HolderFactory.newFactory(this);
return factory.newInstance(this,
appWidgetId -> getWorkspace().removeWidget(appWidgetId),
new QuickstepInteractionHandler(this));
@@ -657,10 +676,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
QuickstepOnboardingPrefs.setup(this);
if (Utilities.ATLEAST_U) {
View.setTraceLayoutSteps(TRACE_LAYOUTS);
View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS);
}
View.setTraceLayoutSteps(TRACE_LAYOUTS);
View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS);
}
@Override
@@ -674,8 +691,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public void startSplitSelection(SplitSelectSource splitSelectSource) {
RecentsView recentsView = getOverviewPanel();
// Check if there is already an instance of this app running, if so, initiate
// the split
// Check if there is already an instance of this app running, if so, initiate the split
// using that.
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
@@ -691,13 +707,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
} else {
recentsView.initiateSplitSelect(splitSelectSource);
}
});
}
);
}
/**
* TODO(b/266482558) Migrate into SplitSelectStateController or someplace split
* specific.
*/
/** TODO(b/266482558) Migrate into SplitSelectStateController or someplace split specific. */
private void startSplitToHome(SplitSelectSource source) {
AbstractFloatingView.closeAllOpenViews(this);
int splitPlaceholderSize = getResources().getDimensionPixelSize(
@@ -705,13 +719,16 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
int splitPlaceholderInset = getResources().getDimensionPixelSize(
R.dimen.split_placeholder_inset);
Rect tempRect = new Rect();
mSplitSelectStateController.setInitialTaskSelect(source.intent,
source.position.stagePosition, source.itemInfo, source.splitEvent,
source.alreadyRunningTaskId);
RecentsView recentsView = getOverviewPanel();
recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
splitPlaceholderSize, splitPlaceholderInset, getDeviceProfile(),
mSplitSelectStateController.getActiveSplitStagePosition(), tempRect);
PendingAnimation anim = new PendingAnimation(TABLET_HOME_TO_SPLIT.getDuration());
RectF startingTaskRect = new RectF();
final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(this,
@@ -759,6 +776,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
protected void onResume() {
super.onResume();
if (mLauncherUnfoldAnimationController != null) {
mLauncherUnfoldAnimationController.onResume();
}
@@ -769,6 +787,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
if (mLauncherUnfoldAnimationController != null) {
mLauncherUnfoldAnimationController.onPause();
}
super.onPause();
if (enableSplitContextually()) {
@@ -799,10 +818,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public void onEnterAnimationComplete() {
super.onEnterAnimationComplete();
// After the transition to home, enable the high-res thumbnail loader if it
// wasn't enabled
// as a part of quickstep, so that high-res thumbnails can load the next time we
// enter
// After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
// as a part of quickstep, so that high-res thumbnails can load the next time we enter
// overview
RecentsModel.INSTANCE.get(this).getThumbnailCache()
.getHighResLoadingState().setVisible(true);
@@ -876,50 +893,51 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
new OnBackAnimationCallback() {
@Nullable
OnBackPressedHandler mActiveOnBackPressedHandler;
@Nullable OnBackAnimationCallback mActiveOnBackAnimationCallback;
@Override
public void onBackStarted(@NonNull BackEvent backEvent) {
if (mActiveOnBackPressedHandler != null) {
mActiveOnBackPressedHandler.onBackCancelled();
if (mActiveOnBackAnimationCallback != null) {
mActiveOnBackAnimationCallback.onBackCancelled();
}
mActiveOnBackPressedHandler = getOnBackPressedHandler();
mActiveOnBackPressedHandler.onBackStarted();
mActiveOnBackAnimationCallback = getOnBackAnimationCallback();
mActiveOnBackAnimationCallback.onBackStarted(backEvent);
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Override
public void onBackInvoked() {
// Recreate mActiveOnBackPressedHandler if necessary to avoid NPE
// Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE
// because:
// 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not
// called on ACTION_DOWN before onBackInvoked() is called in ACTION_UP.
// 2. Launcher#onBackPressed() will call onBackInvoked() without calling
// onBackInvoked() beforehand.
if (mActiveOnBackPressedHandler == null) {
mActiveOnBackPressedHandler = getOnBackPressedHandler();
if (mActiveOnBackAnimationCallback == null) {
mActiveOnBackAnimationCallback = getOnBackAnimationCallback();
}
mActiveOnBackPressedHandler.onBackInvoked();
mActiveOnBackPressedHandler = null;
mActiveOnBackAnimationCallback.onBackInvoked();
mActiveOnBackAnimationCallback = null;
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onBackInvoked");
}
@Override
public void onBackProgressed(@NonNull BackEvent backEvent) {
if (!FeatureFlags.IS_STUDIO_BUILD
&& mActiveOnBackPressedHandler == null) {
&& mActiveOnBackAnimationCallback == null) {
return;
}
mActiveOnBackPressedHandler.onBackProgressed(backEvent.getProgress());
mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
}
@Override
public void onBackCancelled() {
if (!FeatureFlags.IS_STUDIO_BUILD
&& mActiveOnBackPressedHandler == null) {
&& mActiveOnBackAnimationCallback == null) {
return;
}
mActiveOnBackPressedHandler.onBackCancelled();
mActiveOnBackPressedHandler = null;
mActiveOnBackAnimationCallback.onBackCancelled();
mActiveOnBackAnimationCallback = null;
}
});
}
@@ -988,11 +1006,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
private void handlePendingActivityRequest() {
if (mPendingActivityRequestCode != -1 && isInState(NORMAL)
&& ((getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
// Remove any active ProxyActivityStarter task and send RESULT_CANCELED to
// Launcher.
// Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
// ProxyActivityStarter is started with clear task to reset the task after which
// it
// ProxyActivityStarter is started with clear task to reset the task after which it
// removes the task itself.
startActivity(ProxyActivityStarter.getLaunchIntent(this, null));
}
@@ -1046,8 +1062,10 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
"Trying to create getRemoteTransitionProgress when the transition "
+ "is disabled"));
mUnfoldTransitionProgressProvider = remoteUnfoldTransitionProgressProvider;
SystemUiProxy.INSTANCE.get(this).setUnfoldAnimationListener(
remoteUnfoldTransitionProgressProvider);
initUnfoldAnimationController(mUnfoldTransitionProgressProvider,
unfoldComponent.getRotationChangeProvider());
}
@@ -1058,7 +1076,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
/* launcher= */ this,
getWindowManager(),
progressProvider,
rotationChangeProvider);
rotationChangeProvider
);
}
public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
@@ -1127,15 +1146,13 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public float[] getNormalOverviewScaleAndOffset() {
return DisplayController.getNavigationMode(this).hasGestures
? new float[] { 1, 1 }
: new float[] { 1.1f, NO_OFFSET };
? new float[] {1, 1} : new float[] {1.1f, NO_OFFSET};
}
@Override
public void finishBindingItems(IntSet pagesBoundFirst) {
super.finishBindingItems(pagesBoundFirst);
// Instantiate and initialize WellbeingModel now that its loading won't
// interfere with
// Instantiate and initialize WellbeingModel now that its loading won't interfere with
// populating workspace.
// TODO: Find a better place for this
WellbeingModel.INSTANCE.get(this);
@@ -1148,29 +1165,24 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
ActivityOptionsWrapper activityOptions = mAppTransitionManager.hasControlRemoteAppTransitionPermission()
? mAppTransitionManager.getActivityLaunchOptions(v)
: super.getActivityLaunchOptions(v, item);
if (mLastTouchUpTime > 0 && LawnchairQuickstepCompat.ATLEAST_S) {
ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(v);
if (mLastTouchUpTime > 0) {
activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER,
mLastTouchUpTime);
}
if (item != null && (item.animationType == DEFAULT_NO_ICON
|| item.animationType == VIEW_BACKGROUND) && LawnchairQuickstepCompat.ATLEAST_T) {
|| item.animationType == VIEW_BACKGROUND)) {
activityOptions.options.setSplashScreenStyle(
SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR);
} else {
if (LawnchairQuickstepCompat.ATLEAST_T) {
activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
}
activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
}
activityOptions.options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
: Display.DEFAULT_DISPLAY);
Utilities.allowBGLaunch(activityOptions.options);
if (LawnchairQuickstepCompat.ATLEAST_T) {
addLaunchCookie(item, activityOptions.options);
}
activityOptions.options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
addLaunchCookie(item, activityOptions.options);
return activityOptions;
}
@@ -1251,19 +1263,16 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
super.onDisplayInfoChanged(context, info, flags);
// When changing screens, force moving to rest state similar to
// StatefulActivity.onStop, as
// When changing screens, force moving to rest state similar to StatefulActivity.onStop, as
// StatefulActivity isn't called consistently.
if ((flags & CHANGE_ACTIVE_SCREEN) != 0) {
// Do not animate moving to rest state, as it can clash with
// Launcher#onIdpChanged
// where reapplyUi calls StateManager's reapplyState during the state change
// animation,
// and cancel the state change unexpectedly. The screen will be off during
// screen
// Do not animate moving to rest state, as it can clash with Launcher#onIdpChanged
// where reapplyUi calls StateManager's reapplyState during the state change animation,
// and cancel the state change unexpectedly. The screen will be off during screen
// transition, hiding the unanimated transition.
getStateManager().moveToRestState(/* isAnimated = */false);
}
if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
getDragLayer().recreateControllers();
if (mActionsView != null) {
@@ -1275,16 +1284,12 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// If Launcher shuts downs during split select, we save some extra data in the
// recovery
// bundle to allow graceful recovery. The normal LauncherState restore mechanism
// doesn't
// work in this case because restoring straight to OverviewSplitSelect without
// staging data,
// or before the tasks themselves have loaded into Overview, causes a crash. So
// we tell
// Launcher to first restore into Overview state, wait for the relevant tasks
// and icons to
// If Launcher shuts downs during split select, we save some extra data in the recovery
// bundle to allow graceful recovery. The normal LauncherState restore mechanism doesn't
// work in this case because restoring straight to OverviewSplitSelect without staging data,
// or before the tasks themselves have loaded into Overview, causes a crash. So we tell
// Launcher to first restore into Overview state, wait for the relevant tasks and icons to
// load in, and then proceed to OverviewSplitSelect.
if (isInState(OVERVIEW_SPLIT_SELECT)) {
// Launcher will restart in Overview and then transition to OverviewSplitSelect.
@@ -1292,18 +1297,17 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
new PendingSplitSelectInfo(
mSplitSelectStateController.getInitialTaskId(),
mSplitSelectStateController.getActiveSplitStagePosition(),
mSplitSelectStateController.getSplitEvent())));
mSplitSelectStateController.getSplitEvent())
));
outState.putInt(RUNTIME_STATE, OVERVIEW.ordinal);
}
}
/**
* When Launcher restarts, it sometimes needs to recover to a split selection
* state.
* When Launcher restarts, it sometimes needs to recover to a split selection state.
* This function checks if such a recovery is needed.
*
* @return a boolean representing whether the launcher is waiting to recover to
* OverviewSplitSelect state.
* OverviewSplitSelect state.
*/
public boolean hasPendingSplitSelectInfo() {
return mPendingSplitSelectInfo != null;
@@ -1317,8 +1321,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
/**
* When the launcher has successfully recovered to OverviewSplitSelect state,
* this function
* When the launcher has successfully recovered to OverviewSplitSelect state, this function
* deletes the recovery data, returning it to a null state.
*/
public void finishSplitSelectRecovery() {
@@ -1350,6 +1353,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
@Override
public void dispatchDeviceProfileChanged() {
super.dispatchDeviceProfileChanged();
Trace.instantForTrack(TRACE_TAG_APP, "QuickstepLauncher#DeviceProfileChanged",
getDeviceProfile().toSmallString());
SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
if (taskbarManager != null) {
@@ -1397,6 +1402,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
return (mTaskbarUIController != null && mTaskbarUIController.isBubbleBarEnabled());
}
@Override
public boolean hasBubbles() {
return (mTaskbarUIController != null && mTaskbarUIController.hasBubbles());
}
@NonNull
public TISBindHelper getTISBindHelper() {
return mTISBindHelper;
@@ -1424,6 +1434,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
@Override
protected boolean isRecentsModal() {
return mContainer.isInState(OVERVIEW_MODAL_TASK);
}
@@ -1441,8 +1452,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer
}
RecentsView recentsView = getOverviewPanel();
writer.println("\nQuickstepLauncher:");
writer.println(prefix + "\tmOrientationState: "
+ (recentsView == null ? "recentsNull" : recentsView.getPagedViewOrientedState()));
writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" :
recentsView.getPagedViewOrientedState()));
if (recentsView != null) {
recentsView.getSplitSelectController().dump(prefix, writer);
}
@@ -71,20 +71,16 @@ import java.lang.ref.WeakReference;
/**
* Controls the animation of swiping back and returning to launcher.
*
* This is a two part animation. The first part is an animation that tracks
* gesture location to
* scale and move the leaving app window. Once the gesture is committed, the
* second part takes over
* This is a two part animation. The first part is an animation that tracks gesture location to
* scale and move the leaving app window. Once the gesture is committed, the second part takes over
* the app window and plays the rest of app close transitions in one go.
*
* This animation is used only for apps that enable back dispatching via
* {@link android.window.OnBackInvokedDispatcher}. The controller registers
* an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches
* when a back
* an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back
* navigation to launcher starts.
*
* Apps using the legacy back dispatching will keep triggering the
* WALLPAPER_OPEN remote
* Apps using the legacy back dispatching will keep triggering the WALLPAPER_OPEN remote
* transition registered in {@link QuickstepTransitionManager}.
*
*/
@@ -119,15 +115,21 @@ public class LauncherBackAnimationController {
private boolean mBackInProgress = false;
private OnBackInvokedCallbackStub mBackCallback;
private IRemoteAnimationFinishedCallback mAnimationFinishedCallback;
private BackProgressAnimator mProgressAnimator;
private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
private SurfaceControl mScrimLayer;
private ValueAnimator mScrimAlphaAnimator;
private float mScrimAlpha;
private boolean mOverridingStatusBarFlags;
private final ComponentCallbacks mComponentCallbacks=new ComponentCallbacks(){@Override public void onConfigurationChanged(Configuration newConfig){loadResources();}
private final ComponentCallbacks mComponentCallbacks = new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
loadResources();
}
@Override public void onLowMemory(){}};
@Override
public void onLowMemory() {}
};
public LauncherBackAnimationController(
QuickstepLauncher launcher,
@@ -137,107 +139,24 @@ public class LauncherBackAnimationController {
loadResources();
mWindowScaleMarginX = mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_scale_x_margin);
try {
mProgressAnimator = new BackProgressAnimator();
} catch (Throwable throwable) {
// ignore
}
}
/**
* Registers {@link IOnBackInvokedCallback} to receive back dispatches from
* shell.
*
* Registers {@link IOnBackInvokedCallback} to receive back dispatches from shell.
* @param handler Handler to the thread to run the animations on.
*/
public void registerBackCallbacks(Handler handler) {
try {
mBackCallback = new IOnBackInvokedCallback.Stub() {
@Override
public void onBackStarted(BackMotionEvent backMotionEvent) {
startBack(backMotionEvent);
handler.post(() -> {
if (mProgressAnimator == null) {
return;
}
mProgressAnimator.onBackStarted(backMotionEvent, event -> {
mBackProgress = event.getProgress();
// TODO: Update once the interpolation curve spec is finalized.
mBackProgress = 1 - (1 - mBackProgress) * (1 - mBackProgress) * (1
- mBackProgress);
updateBackProgress(mBackProgress, event);
});
});
}
@Override
public void onBackProgressed(BackMotionEvent backMotionEvent) {
handler.post(() -> {
if (mProgressAnimator == null) {
return;
}
mProgressAnimator.onBackProgressed(backMotionEvent);
});
}
@Override
public void onBackCancelled() {
handler.post(() -> {
if (mProgressAnimator == null) {
return;
}
mProgressAnimator.onBackCancelled(
LauncherBackAnimationController.this::resetPositionAnimated);
});
}
@Override
public void onBackInvoked() {
handler.post(() -> {
startTransition();
if (mProgressAnimator == null) {
return;
}
mProgressAnimator.reset();
});
}
};
final IRemoteAnimationRunner runner = new IRemoteAnimationRunner.Stub() {
@Override
public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback) {
for (final RemoteAnimationTarget target : apps) {
if (MODE_CLOSING == target.mode) {
mBackTarget = target;
break;
}
}
mAnimationFinishedCallback = finishedCallback;
}
void onAnimationCancelled(boolean isKeyguardOccluded) {
}
public void onAnimationCancelled() {
}
};
SystemUiProxy.INSTANCE.get(mLauncher).setBackToLauncherCallback(mBackCallback, runner);
} catch (Throwable t) {
// Ignore
}
mBackCallback = new OnBackInvokedCallbackStub(handler, mProgressAnimator,
mProgressInterpolator, this);
SystemUiProxy.INSTANCE.get(mLauncher).setBackToLauncherCallback(mBackCallback,
new RemoteAnimationRunnerStub(this));
}
private static class OnBackInvokedCallbackStub extends IOnBackInvokedCallback.Stub {
private Handler mHandler;
private BackProgressAnimator mProgressAnimator;
private final Interpolator mProgressInterpolator;
// LauncherBackAnimationController has strong reference to Launcher activity,
// the binder
// LauncherBackAnimationController has strong reference to Launcher activity, the binder
// callback should not hold strong reference to it to avoid memory leak.
private WeakReference<LauncherBackAnimationController> mControllerRef;
@@ -295,7 +214,8 @@ public class LauncherBackAnimationController {
controller.startBack(backEvent);
mProgressAnimator.onBackStarted(backEvent, event -> {
float backProgress = event.getProgress();
controller.mBackProgress = mProgressInterpolator.getInterpolation(backProgress);
controller.mBackProgress =
mProgressInterpolator.getInterpolation(backProgress);
controller.updateBackProgress(controller.mBackProgress, event);
});
}
@@ -310,8 +230,7 @@ public class LauncherBackAnimationController {
private static class RemoteAnimationRunnerStub extends IRemoteAnimationRunner.Stub {
// LauncherBackAnimationController has strong reference to Launcher activity,
// the binder
// LauncherBackAnimationController has strong reference to Launcher activity, the binder
// callback should not hold strong reference to it to avoid memory leak.
private WeakReference<LauncherBackAnimationController> mControllerRef;
@@ -339,8 +258,7 @@ public class LauncherBackAnimationController {
}
@Override
public void onAnimationCancelled() {
}
public void onAnimationCancelled() {}
}
private void onCancelFinished() {
@@ -353,22 +271,16 @@ public class LauncherBackAnimationController {
if (mBackCallback != null) {
SystemUiProxy.INSTANCE.get(mLauncher).clearBackToLauncherCallback(mBackCallback);
}
if (mProgressAnimator != null) {
mProgressAnimator.reset();
}
mProgressAnimator.reset();
mBackCallback = null;
}
private void startBack(BackMotionEvent backEvent) {
// in case we're still animating an onBackCancelled event, let's remove the
// finish-
// callback from the progress animator to prevent calling finishAnimation()
// before
// in case we're still animating an onBackCancelled event, let's remove the finish-
// callback from the progress animator to prevent calling finishAnimation() before
// restarting a new animation
// Side note: startBack is never called during the post-commit phase if the back
// gesture
// was committed (not cancelled). BackAnimationController prevents that.
// Therefore we
// Side note: startBack is never called during the post-commit phase if the back gesture
// was committed (not cancelled). BackAnimationController prevents that. Therefore we
// don't have to handle that case.
mProgressAnimator.removeOnBackCancelledFinishCallback();
@@ -391,7 +303,7 @@ public class LauncherBackAnimationController {
mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
new RemoteAnimationTarget[] { mBackTarget });
new RemoteAnimationTarget[]{ mBackTarget });
setLauncherTargetViewVisible(false);
mCurrentRect.set(mStartRect);
if (mScrimLayer == null) {
@@ -425,8 +337,7 @@ public class LauncherBackAnimationController {
.build();
final float[] colorComponents = new float[] { 0f, 0f, 0f };
mScrimAlpha = (isDarkTheme)
? MAX_SCRIM_ALPHA_DARK
: MAX_SCRIM_ALPHA_LIGHT;
? MAX_SCRIM_ALPHA_DARK : MAX_SCRIM_ALPHA_LIGHT;
mTransaction
.setColor(mScrimLayer, colorComponents)
.setAlpha(mScrimLayer, mScrimAlpha)
@@ -515,11 +426,9 @@ public class LauncherBackAnimationController {
if (taskbarUIController != null) {
taskbarUIController.onLauncherVisibilityChanged(true);
}
// TODO: Catch the moment when launcher becomes visible after the top app
// un-occludes
// launcher and start animating afterwards. Currently we occasionally get a
// flicker from
// animating when launcher is still invisible.
// TODO: Catch the moment when launcher becomes visible after the top app un-occludes
// launcher and start animating afterwards. Currently we occasionally get a flicker from
// animating when launcher is still invisible.
if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) {
mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS);
mLauncher.getStateManager().moveToRestState();
@@ -528,8 +437,7 @@ public class LauncherBackAnimationController {
setLauncherTargetViewVisible(true);
// Explicitly close opened floating views (which is typically called from
// Launcher#onResumed, but in the predictive back flow launcher is not resumed
// until
// Launcher#onResumed, but in the predictive back flow launcher is not resumed until
// the transition is fully finished.)
AbstractFloatingView.closeAllOpenViewsExcept(mLauncher, false, TYPE_REBIND_SAFE);
float cornerRadius = Utilities.mapRange(
@@ -538,13 +446,14 @@ public class LauncherBackAnimationController {
mQuickstepTransitionManager.transferRectToTargetCoordinate(
mBackTarget, mCurrentRect, true, resolveRectF);
Pair<RectFSpringAnim, AnimatorSet> pair = mQuickstepTransitionManager.createWallpaperOpenAnimations(
new RemoteAnimationTarget[] { mBackTarget },
new RemoteAnimationTarget[0],
false /* fromUnlock */,
resolveRectF,
cornerRadius,
mBackInProgress /* fromPredictiveBack */);
Pair<RectFSpringAnim, AnimatorSet> pair =
mQuickstepTransitionManager.createWallpaperOpenAnimations(
new RemoteAnimationTarget[]{mBackTarget},
new RemoteAnimationTarget[0],
false /* fromUnlock */,
resolveRectF,
cornerRadius,
mBackInProgress /* fromPredictiveBack */);
startTransitionAnimations(pair.first, pair.second);
mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
customizeStatusBarAppearance(true);
@@ -567,16 +476,21 @@ public class LauncherBackAnimationController {
// We don't call customizeStatusBarAppearance here to prevent the status bar update with
// the legacy appearance. It should be refreshed after the transition done.
mOverridingStatusBarFlags = false;
if (mAnimationFinishedCallback != null) {
try {
mAnimationFinishedCallback.onAnimationFinished();
} catch (RemoteException e) {
Log.w("ShellBackPreview", "Failed call onBackAnimationFinished", e);
}mAnimationFinishedCallback=null;if(mScrimAlphaAnimator!=null&&mScrimAlphaAnimator.isRunning())
{
mScrimAlphaAnimator.cancel();
mScrimAlphaAnimator = null;
}if(mScrimLayer!=null)
{
removeScrimLayer();
}
}
mAnimationFinishedCallback = null;
}
if (mScrimAlphaAnimator != null && mScrimAlphaAnimator.isRunning()) {
mScrimAlphaAnimator.cancel();
mScrimAlphaAnimator = null;
}
if (mScrimLayer != null) {
removeScrimLayer();
}
}
private void startTransitionAnimations(RectFSpringAnim springAnim, AnimatorSet anim) {
@@ -590,22 +504,26 @@ public class LauncherBackAnimationController {
mSpringAnimationInProgress = false;
tryFinishBackAnimation();
}
});
}
);
}
anim.addListener(new AnimatorListenerAdapter() {
@Override
@Override
public void onAnimationEnd(Animator animation) {
mAnimatorSetInProgress = false;
tryFinishBackAnimation();
}});if(mScrimLayer==null){
// Scrim hasn't been attached yet. Let's attach it.
addScrimLayer();}mScrimAlphaAnimator=new ValueAnimator().ofFloat(1,0);mScrimAlphaAnimator.addUpdateListener(animation->{
float value = (Float) animation
.getAnimatedValue();if(mScrimLayer!=null&&mScrimLayer.isValid()){mTransaction.setAlpha(mScrimLayer,value*mScrimAlpha);
applyTransaction();
}
});
if (mScrimLayer == null) {
// Scrim hasn't been attached yet. Let's attach it.
addScrimLayer();
}
mScrimAlphaAnimator = new ValueAnimator().ofFloat(1, 0);
mScrimAlphaAnimator.addUpdateListener(animation -> {
float value = (Float) animation.getAnimatedValue();
if (mScrimLayer != null && mScrimLayer.isValid()) {
mTransaction.setAlpha(mScrimLayer, value * mScrimAlpha);
applyTransaction();
}
});
mScrimAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@@ -621,29 +539,28 @@ public class LauncherBackAnimationController {
private void loadResources() {
mWindowScaleEndCornerRadius = QuickStepContract.supportsRoundedCornersOnWindows(
mLauncher.getResources())
? mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_corner_radius)
: 0;
? mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_corner_radius)
: 0;
mWindowScaleStartCornerRadius = QuickStepContract.getWindowCornerRadius(mLauncher);
mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mLauncher);
}
/**
* Called when launcher is destroyed. Unregisters component callbacks to avoid
* memory leaks.
* Called when launcher is destroyed. Unregisters component callbacks to avoid memory leaks.
*/
public void unregisterComponentCallbacks() {
mLauncher.unregisterComponentCallbacks(mComponentCallbacks);
}
/**
* Registers component callbacks with the launcher to receive configuration
* change events.
* Registers component callbacks with the launcher to receive configuration change events.
*/
public void registerComponentCallbacks() {
mLauncher.registerComponentCallbacks(mComponentCallbacks);
}
private void resetScrim() {
removeScrimLayer();
mScrimAlpha = 0;
@@ -661,8 +578,9 @@ public class LauncherBackAnimationController {
}
mOverridingStatusBarFlags = overridingStatusBarFlags;
final boolean isBackgroundDark = (mLauncher.getWindow().getDecorView().getSystemUiVisibility()
& View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) == 0;
final boolean isBackgroundDark =
(mLauncher.getWindow().getDecorView().getSystemUiVisibility()
& View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) == 0;
final AppearanceRegion region = mOverridingStatusBarFlags
? new AppearanceRegion(!isBackgroundDark ? APPEARANCE_LIGHT_STATUS_BARS : 0,
mBackTarget.windowConfiguration.getBounds())
File diff suppressed because it is too large. Load diff
+2
View File
@@ -73,3 +73,5 @@ include ':compatLib:compatLibVT'
include ':compatLib:compatLibVU'
include ':baseline-profile'
include ':androidx-lib'
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.testing;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.android.launcher3.Utilities;
public class TestInformationProvider extends ContentProvider {
private static final String TAG = "TestInformationProvider";
@Override
public boolean onCreate() {
return true;
}
@Override
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
return 0;
}
@Override
public int delete(Uri uri, String s, String[] strings) {
return 0;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
return null;
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
if (Utilities.isRunningInTestHarness()) {
TestInformationHandler handler = TestInformationHandler.newInstance(getContext());
handler.init(getContext());
Bundle response = handler.call(method, arg, extras);
if (response == null) {
Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
+ handler.getClass().getSimpleName());
}
return response;
}
return null;
}
}
@@ -55,128 +55,114 @@ import com.android.launcher3.states.StateAnimationConfig;
*/
public class AllAppsSwipeController extends AbstractStateChangeTouchController {
private static final float ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD = 0.8f;
private static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.5f;
private static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = 0.1f;
private static final float ALL_APPS_STAGGERED_FADE_THRESHOLD = 0.5f;
private static final float ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD = 0.8f;
private static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.5f;
private static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = 0.1f;
private static final float ALL_APPS_STAGGERED_FADE_THRESHOLD = 0.5f;
public static final Interpolator ALL_APPS_SCRIM_RESPONDER = Interpolators.clampToProgress(
LINEAR, ALL_APPS_SCRIM_VISIBLE_THRESHOLD, ALL_APPS_STAGGERED_FADE_THRESHOLD);
public static final Interpolator ALL_APPS_CLAMPING_RESPONDER = Interpolators.clampToProgress(
LINEAR,
1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD,
1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD);
public static final Interpolator ALL_APPS_SCRIM_RESPONDER =
Interpolators.clampToProgress(
LINEAR, ALL_APPS_SCRIM_VISIBLE_THRESHOLD, ALL_APPS_STAGGERED_FADE_THRESHOLD);
public static final Interpolator ALL_APPS_CLAMPING_RESPONDER =
Interpolators.clampToProgress(
LINEAR,
1 - ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD,
1 - ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD);
// ---- Custom interpolators for NORMAL -> ALL_APPS on phones only. ----
// ---- Custom interpolators for NORMAL -> ALL_APPS on phones only. ----
public static final float ALL_APPS_STATE_TRANSITION_ATOMIC = 0.3333f;
public static final float ALL_APPS_STATE_TRANSITION_MANUAL = 0.4f;
private static final float ALL_APPS_FADE_END_ATOMIC = 0.8333f;
private static final float ALL_APPS_FADE_END_MANUAL = 0.8f;
private static final float ALL_APPS_FULL_DEPTH_PROGRESS = 0.5f;
private static final float SCRIM_FADE_START_ATOMIC = 0.2642f;
private static final float SCRIM_FADE_START_MANUAL = 0.117f;
private static final float WORKSPACE_MOTION_START_ATOMIC = 0.1667f;
public static final float ALL_APPS_STATE_TRANSITION_ATOMIC = 0.3333f;
public static final float ALL_APPS_STATE_TRANSITION_MANUAL = 0.4f;
private static final float ALL_APPS_FADE_END_ATOMIC = 0.8333f;
private static final float ALL_APPS_FADE_END_MANUAL = 0.8f;
private static final float ALL_APPS_FULL_DEPTH_PROGRESS = 0.5f;
private static final float SCRIM_FADE_START_ATOMIC = 0.2642f;
private static final float SCRIM_FADE_START_MANUAL = 0.117f;
private static final float WORKSPACE_MOTION_START_ATOMIC = 0.1667f;
private static final Interpolator LINEAR_EARLY_MANUAL = Interpolators.clampToProgress(LINEAR, 0f,
ALL_APPS_STATE_TRANSITION_MANUAL);
private static final Interpolator STEP_TRANSITION_ATOMIC = Interpolators.clampToProgress(FINAL_FRAME, 0f,
ALL_APPS_STATE_TRANSITION_ATOMIC);
private static final Interpolator STEP_TRANSITION_MANUAL = Interpolators.clampToProgress(FINAL_FRAME, 0f,
ALL_APPS_STATE_TRANSITION_MANUAL);
private static final Interpolator LINEAR_EARLY_MANUAL =
Interpolators.clampToProgress(LINEAR, 0f, ALL_APPS_STATE_TRANSITION_MANUAL);
private static final Interpolator STEP_TRANSITION_ATOMIC =
Interpolators.clampToProgress(FINAL_FRAME, 0f, ALL_APPS_STATE_TRANSITION_ATOMIC);
private static final Interpolator STEP_TRANSITION_MANUAL =
Interpolators.clampToProgress(FINAL_FRAME, 0f, ALL_APPS_STATE_TRANSITION_MANUAL);
// The blur to All Apps is set to be complete when the interpolator is at 0.5.
private static final Interpolator BLUR_ADJUSTED = Interpolators.mapToProgress(LINEAR, 0f,
ALL_APPS_FULL_DEPTH_PROGRESS);
public static final Interpolator BLUR_ATOMIC = Interpolators.clampToProgress(
BLUR_ADJUSTED, WORKSPACE_MOTION_START_ATOMIC, ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator BLUR_MANUAL = Interpolators.clampToProgress(BLUR_ADJUSTED, 0f,
ALL_APPS_STATE_TRANSITION_MANUAL);
// The blur to All Apps is set to be complete when the interpolator is at 0.5.
private static final Interpolator BLUR_ADJUSTED =
Interpolators.mapToProgress(LINEAR, 0f, ALL_APPS_FULL_DEPTH_PROGRESS);
public static final Interpolator BLUR_ATOMIC =
Interpolators.clampToProgress(
BLUR_ADJUSTED, WORKSPACE_MOTION_START_ATOMIC, ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator BLUR_MANUAL =
Interpolators.clampToProgress(BLUR_ADJUSTED, 0f, ALL_APPS_STATE_TRANSITION_MANUAL);
public static final Interpolator WORKSPACE_FADE_ATOMIC = STEP_TRANSITION_ATOMIC;
public static final Interpolator WORKSPACE_FADE_MANUAL = STEP_TRANSITION_MANUAL;
public static final Interpolator WORKSPACE_FADE_ATOMIC = STEP_TRANSITION_ATOMIC;
public static final Interpolator WORKSPACE_FADE_MANUAL = STEP_TRANSITION_MANUAL;
public static final Interpolator WORKSPACE_SCALE_ATOMIC = Interpolators.clampToProgress(
EMPHASIZED_ACCELERATE, WORKSPACE_MOTION_START_ATOMIC,
ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator WORKSPACE_SCALE_MANUAL = LINEAR_EARLY_MANUAL;
public static final Interpolator WORKSPACE_SCALE_ATOMIC =
Interpolators.clampToProgress(
EMPHASIZED_ACCELERATE, WORKSPACE_MOTION_START_ATOMIC,
ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator WORKSPACE_SCALE_MANUAL = LINEAR_EARLY_MANUAL;
public static final Interpolator HOTSEAT_FADE_ATOMIC = STEP_TRANSITION_ATOMIC;
public static final Interpolator HOTSEAT_FADE_MANUAL = STEP_TRANSITION_MANUAL;
public static final Interpolator HOTSEAT_FADE_ATOMIC = STEP_TRANSITION_ATOMIC;
public static final Interpolator HOTSEAT_FADE_MANUAL = STEP_TRANSITION_MANUAL;
public static final Interpolator HOTSEAT_SCALE_ATOMIC = Interpolators.clampToProgress(
EMPHASIZED_ACCELERATE, WORKSPACE_MOTION_START_ATOMIC,
ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator HOTSEAT_SCALE_MANUAL = LINEAR_EARLY_MANUAL;
public static final Interpolator HOTSEAT_SCALE_ATOMIC =
Interpolators.clampToProgress(
EMPHASIZED_ACCELERATE, WORKSPACE_MOTION_START_ATOMIC,
ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator HOTSEAT_SCALE_MANUAL = LINEAR_EARLY_MANUAL;
public static final Interpolator HOTSEAT_TRANSLATE_ATOMIC = STEP_TRANSITION_ATOMIC;
public static final Interpolator HOTSEAT_TRANSLATE_MANUAL = STEP_TRANSITION_MANUAL;
public static final Interpolator HOTSEAT_TRANSLATE_ATOMIC = STEP_TRANSITION_ATOMIC;
public static final Interpolator HOTSEAT_TRANSLATE_MANUAL = STEP_TRANSITION_MANUAL;
public static final Interpolator SCRIM_FADE_ATOMIC = Interpolators.clampToProgress(
Interpolators.mapToProgress(LINEAR, 0f, 0.8f),
SCRIM_FADE_START_ATOMIC, ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator SCRIM_FADE_MANUAL = Interpolators.clampToProgress(
LINEAR, SCRIM_FADE_START_MANUAL, ALL_APPS_STATE_TRANSITION_MANUAL);
public static final Interpolator SCRIM_FADE_ATOMIC =
Interpolators.clampToProgress(
Interpolators.mapToProgress(LINEAR, 0f, 0.8f),
SCRIM_FADE_START_ATOMIC, ALL_APPS_STATE_TRANSITION_ATOMIC);
public static final Interpolator SCRIM_FADE_MANUAL =
Interpolators.clampToProgress(
LINEAR, SCRIM_FADE_START_MANUAL, ALL_APPS_STATE_TRANSITION_MANUAL);
public static final Interpolator ALL_APPS_FADE_ATOMIC = Interpolators.clampToProgress(
Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.2f, 1f),
ALL_APPS_STATE_TRANSITION_ATOMIC, ALL_APPS_FADE_END_ATOMIC);
public static final Interpolator ALL_APPS_FADE_MANUAL = Interpolators.clampToProgress(
LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, ALL_APPS_FADE_END_MANUAL);
public static final Interpolator ALL_APPS_FADE_ATOMIC =
Interpolators.clampToProgress(
Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.2f, 1f),
ALL_APPS_STATE_TRANSITION_ATOMIC, ALL_APPS_FADE_END_ATOMIC);
public static final Interpolator ALL_APPS_FADE_MANUAL =
Interpolators.clampToProgress(
LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, ALL_APPS_FADE_END_MANUAL);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_ATOMIC = Interpolators.clampToProgress(
Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f),
ALL_APPS_STATE_TRANSITION_ATOMIC, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR;
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_ATOMIC =
Interpolators.clampToProgress(
Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f),
ALL_APPS_STATE_TRANSITION_ATOMIC, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR;
// --------
// --------
public AllAppsSwipeController(Launcher l) {
super(l, SingleAxisSwipeDetector.VERTICAL);
public AllAppsSwipeController(Launcher l) {
super(l, SingleAxisSwipeDetector.VERTICAL);
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
if (mCurrentAnimation != null) {
// If we are already animating from a previous state, we can intercept.
return true;
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
if (mCurrentAnimation != null) {
// If we are already animating from a previous state, we can intercept.
return true;
}
if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
return false;
}
if (!mLauncher.isInState(NORMAL) && !mLauncher.isInState(ALL_APPS)) {
// Don't listen for the swipe gesture if we are already in some other state.
return false;
}
if (mLauncher.isInState(ALL_APPS) && !mLauncher.getAppsView().shouldContainerScroll(ev)) {
return false;
}
return true;
if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
return false;
}
@Override
protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
if (fromState == NORMAL && isDragTowardPositive) {
return ALL_APPS;
} else if (fromState == ALL_APPS && !isDragTowardPositive) {
return NORMAL;
}
return fromState;
if (!mLauncher.isInState(NORMAL) && !mLauncher.isInState(ALL_APPS)) {
// Don't listen for the swipe gesture if we are already in some other state.
return false;
}
@Override
protected float initCurrentAnimation() {
float range = getShiftRange();
StateAnimationConfig config = getConfigForStates(mFromState, mToState);
config.duration = (long) (2 * range);
mCurrentAnimation = mLauncher.getStateManager()
.createAnimationToNewWorkspace(mToState, config);
float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
float totalShift = endVerticalShift - startVerticalShift;
return 1 / totalShift;
if (mLauncher.isInState(ALL_APPS) && !mLauncher.getAppsView().shouldContainerScroll(ev)) {
return false;
}
return true;
}
@Override
protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
@@ -185,20 +171,22 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
} else if (fromState == ALL_APPS && !isDragTowardPositive) {
return NORMAL;
}
return fromState;
}
@Override
protected float initCurrentAnimation() {
float range = getShiftRange();
StateAnimationConfig config = getConfigForStates(mFromState, mToState);
config.duration = (long) (2 * range);
@Override
protected float initCurrentAnimation() {
float range = getShiftRange();
StateAnimationConfig config = getConfigForStates(mFromState, mToState);
config.duration = (long) (2 * range);
mCurrentAnimation = mLauncher.getStateManager()
.createAnimationToNewWorkspace(mToState, config);
float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
float totalShift = endVerticalShift - startVerticalShift;
return 1 / totalShift;
}
mCurrentAnimation = mLauncher.getStateManager()
.createAnimationToNewWorkspace(mToState, config);
float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
float totalShift = endVerticalShift - startVerticalShift;
return 1 / totalShift;
}
@Override
protected StateAnimationConfig getConfigForStates(LauncherState fromState,
@@ -210,6 +198,8 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
} else if (fromState == ALL_APPS && toState == NORMAL) {
applyAllAppsToNormalConfig(mLauncher, config);
}
return config;
}
/**
* Applies Animation config values for transition from all apps to home.
@@ -250,6 +240,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
config.setInterpolator(ANIM_VERTICAL_PROGRESS, EMPHASIZED_ACCELERATE);
}
}
}
/**
* Applies Animation config values for transition from home to all apps.
@@ -288,6 +279,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
? ALL_APPS_VERTICAL_PROGRESS_MANUAL
: ALL_APPS_VERTICAL_PROGRESS_ATOMIC);
}
}
/**
* Applies Animation config values for transition from overview to all apps.
@@ -317,4 +309,10 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
config.setInterpolator(ANIM_VERTICAL_PROGRESS,
thresholdInterpolator(threshold, mapToProgress(LINEAR, threshold, 1f)));
}
}
/** Creates an interpolator that is 0 until the threshold, then follows given interpolator. */
private static Interpolator thresholdInterpolator(float threshold, Interpolator interpolator) {
return progress -> progress <= threshold ? 0 : interpolator.getInterpolation(progress);
}
}
@@ -31,7 +31,6 @@ import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.view.ViewCompat;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAppState;
@@ -44,34 +43,25 @@ import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import app.lawnchair.font.FontManager;
import app.lawnchair.theme.drawable.DrawableTokens;
/**
* A UI represents a header of an app shown in the full widgets tray.
*
* It is a {@link LinearLayout} which contains an app icon, an app name, a
* subtitle and a checkbox
* which indicates if the widgets content view underneath this header should be
* shown.
* It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox
* which indicates if the widgets content view underneath this header should be shown.
*/
public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
private static final int[] EXPANDED_DRAWABLE_STATE = new int[] { android.R.attr.state_expanded };
private static final int[] EXPANDED_DRAWABLE_STATE = new int[] {android.R.attr.state_expanded};
private final int mIconSize;
/**
* Indicates if the header is collapsable. For example, when displayed in a two
* pane layout,
* Indicates if the header is collapsable. For example, when displayed in a two pane layout,
* widget apps aren't collapsable.
*/
*/
private final boolean mIsCollapsable;
@Nullable
private CancellableTask mIconLoadRequest;
@Nullable
private Drawable mIconDrawable;
@Nullable
private WidgetsListDrawableState mListDrawableState;
@Nullable private CancellableTask mIconLoadRequest;
@Nullable private Drawable mIconDrawable;
@Nullable private WidgetsListDrawableState mListDrawableState;
private ImageView mAppIcon;
private TextView mTitle;
private TextView mSubtitle;
@@ -104,43 +94,36 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
mAppIcon = findViewById(R.id.app_icon);
mTitle = findViewById(R.id.app_title);
mSubtitle = findViewById(R.id.app_subtitle);
// Lists that cannot collapse, don't need EXPAND or COLLAPSE accessibility actions.
if (mIsCollapsable) {
setAccessibilityDelegate(new AccessibilityDelegate() {
var mContent = ViewCompat.requireViewById(this, R.id.widgets_list_header);
mContent.setBackground(DrawableTokens.WidgetsListBackground.resolve(getContext()));
FontManager fontManager = FontManager.INSTANCE.get(getContext());
fontManager.setCustomFont(mTitle, R.id.font_body_medium);
fontManager.setCustomFont(mTitle, R.id.font_body);
setAccessibilityDelegate(new AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host,
AccessibilityNodeInfo info) {
if (mIsExpanded) {
info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
} else {
info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
@Override
public void onInitializeAccessibilityNodeInfo(View host,
AccessibilityNodeInfo info) {
if (mIsExpanded) {
info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
} else {
info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
}
super.onInitializeAccessibilityNodeInfo(host, info);
}
super.onInitializeAccessibilityNodeInfo(host, info);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
switch (action) {
case AccessibilityNodeInfo.ACTION_EXPAND:
case AccessibilityNodeInfo.ACTION_COLLAPSE:
callOnClick();
return true;
default:
return super.performAccessibilityAction(host, action, args);
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
switch (action) {
case AccessibilityNodeInfo.ACTION_EXPAND:
case AccessibilityNodeInfo.ACTION_COLLAPSE:
callOnClick();
return true;
default:
return super.performAccessibilityAction(host, action, args);
}
}
}
});
}
});
}
}
/** Sets the expand toggle to expand / collapse. */
@@ -155,22 +138,15 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
return mIsExpanded;
}
/**
* Sets the {@link WidgetsListDrawableState} and refreshes the background
* drawable.
*/
/** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
@UiThread
public void setListDrawableState(WidgetsListDrawableState state) {
if (state == mListDrawableState)
return;
if (state == mListDrawableState) return;
this.mListDrawableState = state;
refreshDrawableState();
}
/**
* Apply app icon, labels and tag using a generic
* {@link WidgetsListHeaderEntry}.
*/
/** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
@UiThread
public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
PackageItemInfo info = entry.mPkgItem;
@@ -196,7 +172,8 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
private void applyDrawables(Drawable icon) {
icon.setBounds(0, 0, mIconSize, mIconSize);
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mAppIcon.getLayoutParams();
LayoutParams layoutParams =
(LayoutParams) mAppIcon.getLayoutParams();
layoutParams.width = mIconSize;
layoutParams.height = mIconSize;
mAppIcon.setLayoutParams(layoutParams);
@@ -239,8 +216,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
@Override
protected int[] onCreateDrawableState(int extraSpace) {
// We create a drawable state with an additional two spaces to be able to fit
// expanded state
// We create a drawable state with an additional two spaces to be able to fit expanded state
// and the list drawable state.
int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
if (mIsExpanded) {
@@ -252,10 +228,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
return drawableState;
}
/**
* Verifies that the current icon is high-res otherwise posts a request to load
* the icon.
*/
/** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
public void verifyHighRes() {
if (mIconLoadRequest != null) {
mIconLoadRequest.cancel();
+7 -5
View File
@@ -1,7 +1,7 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
id 'org.jetbrains.kotlin.kapt'
}
android {
@@ -15,8 +15,10 @@ android {
aidl.srcDirs = ['src']
}
}
ksp {
arg("dagger.hilt.disableModulesHaveInstallInCheck", "true")
kapt {
arguments {
arg("dagger.hilt.disableModulesHaveInstallInCheck", "true")
}
}
}
@@ -25,9 +27,9 @@ compileOnlyCommonJars()
dependencies {
implementation "com.google.dagger:hilt-android:$daggerVersion"
ksp "com.google.dagger:hilt-compiler:$daggerVersion"
kapt "com.google.dagger:hilt-compiler:$daggerVersion"
implementation "androidx.concurrent:concurrent-futures:1.2.0"
implementation "androidx.lifecycle:lifecycle-common:2.8.6"
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
implementation projects.androidxLib
}
@@ -16,9 +16,12 @@
package com.android.systemui.unfold
import android.os.Handler
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.dagger.UseReceivingFilter
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.util.ATraceLoggerTransitionProgressListener
import dagger.Module
import dagger.Provides
@@ -33,16 +36,25 @@ class UnfoldRemoteModule {
@Singleton
fun provideTransitionProvider(
config: UnfoldTransitionConfig,
traceListener: ATraceLoggerTransitionProgressListener,
traceListener: ATraceLoggerTransitionProgressListener.Factory,
remoteReceiverProvider: Provider<RemoteUnfoldTransitionReceiver>,
): Optional<RemoteUnfoldTransitionReceiver> {
if (!config.isEnabled) {
return Optional.empty()
}
val remoteReceiver = remoteReceiverProvider.get()
remoteReceiver.addCallback(traceListener)
remoteReceiver.addCallback(traceListener.create("remoteReceiver"))
return Optional.of(remoteReceiver)
}
@Provides @UseReceivingFilter fun useReceivingFilter(): Boolean = true
@Provides
@UnfoldMain
fun provideMainRotationChangeProvider(
rotationChangeProviderFactory: RotationChangeProvider.Factory,
@UnfoldMain callbackHandler: Handler,
): RotationChangeProvider {
return rotationChangeProviderFactory.create(callbackHandler)
}
}
@@ -22,12 +22,12 @@ import android.hardware.SensorManager
import android.hardware.display.DisplayManager
import android.os.Handler
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver
import com.android.systemui.unfold.updates.FoldProvider
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.UnfoldTransitionATracePrefix
@@ -63,13 +63,12 @@ interface UnfoldSharedComponent {
@BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
@BindsInstance displayManager: DisplayManager,
@BindsInstance contentResolver: ContentResolver = context.contentResolver
@BindsInstance @UnfoldBg bgHandler: Handler,
@BindsInstance contentResolver: ContentResolver = context.contentResolver,
): UnfoldSharedComponent
}
val unfoldTransitionProvider: Optional<UnfoldTransitionProgressProvider>
val hingeAngleProvider: HingeAngleProvider
val rotationChangeProvider: RotationChangeProvider
}
/**
@@ -88,13 +87,15 @@ interface RemoteUnfoldSharedComponent {
@BindsInstance @UnfoldMain executor: Executor,
@BindsInstance @UnfoldMain handler: Handler,
@BindsInstance @UnfoldSingleThreadBg singleThreadBgExecutor: Executor,
@BindsInstance @UnfoldBg bgHandler: Handler,
@BindsInstance displayManager: DisplayManager,
@BindsInstance @UnfoldTransitionATracePrefix tracingTagPrefix: String,
): RemoteUnfoldSharedComponent
}
val remoteTransitionProgress: Optional<RemoteUnfoldTransitionReceiver>
val rotationChangeProvider: RotationChangeProvider
@UnfoldMain fun getRotationChangeProvider(): RotationChangeProvider
}
/**
@@ -16,12 +16,18 @@
package com.android.systemui.unfold
import android.os.Handler
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.dagger.UnfoldBgProgressFlag
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.progress.FixedTimingTransitionProgressProvider
import com.android.systemui.unfold.progress.MainThreadUnfoldTransitionProgressProvider
import com.android.systemui.unfold.progress.PhysicsBasedUnfoldTransitionProgressProvider
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
import com.android.systemui.unfold.updates.DeviceFoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.RotationChangeProvider
import com.android.systemui.unfold.updates.hinge.EmptyHingeAngleProvider
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.hinge.HingeSensorAngleProvider
@@ -30,20 +36,25 @@ import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManager
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityManagerImpl
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityProvider
import dagger.BindsOptionalOf
import dagger.Module
import dagger.Provides
import java.util.Optional
import javax.inject.Provider
import javax.inject.Singleton
import kotlin.jvm.optionals.getOrDefault
@Module(includes = [UnfoldSharedInternalModule::class])
@Module(
includes =
[
UnfoldFlagsModule::class,
UnfoldSharedInternalModule::class,
UnfoldRotationProviderInternalModule::class,
HingeAngleProviderInternalModule::class,
FoldStateProviderModule::class,
]
)
class UnfoldSharedModule {
@Provides
@Singleton
fun provideFoldStateProvider(
deviceFoldStateProvider: DeviceFoldStateProvider
): FoldStateProvider = deviceFoldStateProvider
@Provides
@Singleton
fun unfoldKeyguardVisibilityProvider(
@@ -57,6 +68,16 @@ class UnfoldSharedModule {
): UnfoldKeyguardVisibilityManager = impl
}
@Module
abstract class UnfoldFlagsModule {
/**
* Users of the library can bind this boolean to notify whether the progress should be
* calculated only in the background (and the main thread provider is generated by posting the
* background events in the main handler).
*/
@BindsOptionalOf @UnfoldBgProgressFlag abstract fun unfoldBgProgressFlag(): Boolean
}
/**
* Needed as methods inside must be public, but their parameters can be internal (and, a public
* method can't have internal parameters). Making the module internal and included in a public one
@@ -67,19 +88,88 @@ internal class UnfoldSharedInternalModule {
@Provides
@Singleton
fun unfoldTransitionProgressProvider(
config: UnfoldTransitionConfig,
scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
tracingListener: ATraceLoggerTransitionProgressListener.Factory,
physicsBasedUnfoldTransitionProgressProvider:
PhysicsBasedUnfoldTransitionProgressProvider.Factory,
fixedTimingTransitionProgressProvider: Provider<FixedTimingTransitionProgressProvider>,
foldStateProvider: FoldStateProvider,
@UnfoldMain mainHandler: Handler,
mainThreadUnfoldTransitionProgressProviderFactory:
MainThreadUnfoldTransitionProgressProvider.Factory,
@UnfoldBg bgProvider: Provider<Optional<UnfoldTransitionProgressProvider>>,
@UnfoldBgProgressFlag unfoldBgProgressFlag: Optional<Boolean>,
): Optional<UnfoldTransitionProgressProvider> {
if (unfoldBgProgressFlag.getOrDefault(false)) {
// In this case, we wrap the background progress provider
val mainThreadProvider: Optional<UnfoldTransitionProgressProvider> =
bgProvider.get().map {
mainThreadUnfoldTransitionProgressProviderFactory.create(it)
}
mainThreadProvider.ifPresent {
it.addCallback(tracingListener.create("MainThreadFromBgProgress"))
}
return mainThreadProvider
} else {
// TODO(b/277879146): Remove this once unfold_animation_background_progress is launched.
return createOptionalUnfoldTransitionProgressProvider(
config = config,
scaleAwareProviderFactory = scaleAwareProviderFactory,
tracingListener = tracingListener.create("MainThread"),
physicsBasedUnfoldTransitionProgressProvider =
physicsBasedUnfoldTransitionProgressProvider,
fixedTimingTransitionProgressProvider = fixedTimingTransitionProgressProvider,
foldStateProvider = foldStateProvider,
progressHandler = mainHandler,
)
}
}
@Provides
@Singleton
@UnfoldBg
fun unfoldBgTransitionProgressProvider(
config: UnfoldTransitionConfig,
scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
tracingListener: ATraceLoggerTransitionProgressListener.Factory,
physicsBasedUnfoldTransitionProgressProvider:
PhysicsBasedUnfoldTransitionProgressProvider.Factory,
fixedTimingTransitionProgressProvider: Provider<FixedTimingTransitionProgressProvider>,
@UnfoldBg bgFoldStateProvider: FoldStateProvider,
@UnfoldBg bgHandler: Handler,
): Optional<UnfoldTransitionProgressProvider> {
return createOptionalUnfoldTransitionProgressProvider(
config = config,
scaleAwareProviderFactory = scaleAwareProviderFactory,
tracingListener = tracingListener.create("BgThread"),
physicsBasedUnfoldTransitionProgressProvider =
physicsBasedUnfoldTransitionProgressProvider,
fixedTimingTransitionProgressProvider = fixedTimingTransitionProgressProvider,
foldStateProvider = bgFoldStateProvider,
progressHandler = bgHandler,
)
}
private fun createOptionalUnfoldTransitionProgressProvider(
config: UnfoldTransitionConfig,
scaleAwareProviderFactory: ScaleAwareTransitionProgressProvider.Factory,
tracingListener: ATraceLoggerTransitionProgressListener,
physicsBasedUnfoldTransitionProgressProvider:
Provider<PhysicsBasedUnfoldTransitionProgressProvider>,
PhysicsBasedUnfoldTransitionProgressProvider.Factory,
fixedTimingTransitionProgressProvider: Provider<FixedTimingTransitionProgressProvider>,
foldStateProvider: FoldStateProvider,
progressHandler: Handler,
): Optional<UnfoldTransitionProgressProvider> {
if (!config.isEnabled) {
return Optional.empty()
}
val baseProgressProvider =
if (config.isHingeAngleEnabled) {
physicsBasedUnfoldTransitionProgressProvider.get()
physicsBasedUnfoldTransitionProgressProvider.create(
foldStateProvider,
progressHandler
)
} else {
fixedTimingTransitionProgressProvider.get()
}
@@ -92,23 +182,11 @@ internal class UnfoldSharedInternalModule {
)
}
@Provides
fun hingeAngleProvider(
config: UnfoldTransitionConfig,
hingeAngleSensorProvider: Provider<HingeSensorAngleProvider>
): HingeAngleProvider {
return if (config.isHingeAngleEnabled) {
hingeAngleSensorProvider.get()
} else {
EmptyHingeAngleProvider
}
}
@Provides
@Singleton
fun provideProgressForwarder(
config: UnfoldTransitionConfig,
progressForwarder: Provider<UnfoldTransitionProgressForwarder>
config: UnfoldTransitionConfig,
progressForwarder: Provider<UnfoldTransitionProgressForwarder>
): Optional<UnfoldTransitionProgressForwarder> {
if (!config.isEnabled) {
return Optional.empty()
@@ -116,3 +194,95 @@ internal class UnfoldSharedInternalModule {
return Optional.of(progressForwarder.get())
}
}
/**
* Provides [FoldStateProvider]. The [UnfoldBg] annotated binding sends progress in the [UnfoldBg]
* handler.
*/
@Module
internal class FoldStateProviderModule {
@Provides
@Singleton
fun provideFoldStateProvider(
factory: DeviceFoldStateProvider.Factory,
@UnfoldMain hingeAngleProvider: HingeAngleProvider,
@UnfoldMain rotationChangeProvider: RotationChangeProvider,
@UnfoldMain mainHandler: Handler,
): FoldStateProvider =
factory.create(
hingeAngleProvider,
rotationChangeProvider,
progressHandler = mainHandler
)
@Provides
@Singleton
@UnfoldBg
fun provideBgFoldStateProvider(
factory: DeviceFoldStateProvider.Factory,
@UnfoldBg hingeAngleProvider: HingeAngleProvider,
@UnfoldBg rotationChangeProvider: RotationChangeProvider,
@UnfoldBg bgHandler: Handler,
): FoldStateProvider =
factory.create(
hingeAngleProvider,
rotationChangeProvider,
progressHandler = bgHandler
)
}
/** Provides bindings for both [UnfoldMain] and [UnfoldBg] [HingeAngleProvider]. */
@Module
internal class HingeAngleProviderInternalModule {
@Provides
@UnfoldMain
fun hingeAngleProvider(
config: UnfoldTransitionConfig,
@UnfoldMain handler: Handler,
hingeAngleSensorProvider: HingeSensorAngleProvider.Factory
): HingeAngleProvider {
return if (config.isHingeAngleEnabled) {
hingeAngleSensorProvider.create(handler)
} else {
EmptyHingeAngleProvider
}
}
@Provides
@UnfoldBg
fun hingeAngleProviderBg(
config: UnfoldTransitionConfig,
@UnfoldBg handler: Handler,
hingeAngleSensorProvider: HingeSensorAngleProvider.Factory
): HingeAngleProvider {
return if (config.isHingeAngleEnabled) {
hingeAngleSensorProvider.create(handler)
} else {
EmptyHingeAngleProvider
}
}
}
@Module
internal class UnfoldRotationProviderInternalModule {
@Provides
@Singleton
@UnfoldMain
fun provideRotationChangeProvider(
rotationChangeProviderFactory: RotationChangeProvider.Factory,
@UnfoldMain callbackHandler: Handler,
): RotationChangeProvider {
return rotationChangeProviderFactory.create(callbackHandler)
}
@Provides
@Singleton
@UnfoldBg
fun provideBgRotationChangeProvider(
rotationChangeProviderFactory: RotationChangeProvider.Factory,
@UnfoldBg callbackHandler: Handler,
): RotationChangeProvider {
// For UnfoldBg RotationChangeProvider we use bgHandler as callbackHandler
return rotationChangeProviderFactory.create(callbackHandler)
}
}
@@ -48,6 +48,7 @@ fun createUnfoldSharedComponent(
singleThreadBgExecutor: Executor,
tracingTagPrefix: String,
displayManager: DisplayManager,
bgHandler: Handler,
): UnfoldSharedComponent =
DaggerUnfoldSharedComponent.factory()
.create(
@@ -62,6 +63,7 @@ fun createUnfoldSharedComponent(
singleThreadBgExecutor,
tracingTagPrefix,
displayManager,
bgHandler,
)
/**
@@ -75,6 +77,7 @@ fun createRemoteUnfoldSharedComponent(
mainExecutor: Executor,
mainHandler: Handler,
singleThreadBgExecutor: Executor,
bgHandler: Handler,
tracingTagPrefix: String,
displayManager: DisplayManager,
): RemoteUnfoldSharedComponent =
@@ -85,6 +88,7 @@ fun createRemoteUnfoldSharedComponent(
mainExecutor,
mainHandler,
singleThreadBgExecutor,
bgHandler,
displayManager,
tracingTagPrefix,
)
@@ -47,6 +47,10 @@ class ResourceUnfoldTransitionConfig @Inject constructor() : UnfoldTransitionCon
getBooleanResource("config_unfoldTransitionHingeAngle")
}
override val isHapticsEnabled: Boolean by lazy {
getBooleanResource("config_unfoldTransitionHapticsEnabled")
}
override val halfFoldedTimeoutMillis: Int by lazy {
getIntResource("config_unfoldTransitionHalfFoldedTimeout")
}
@@ -18,5 +18,6 @@ package com.android.systemui.unfold.config
interface UnfoldTransitionConfig {
val isEnabled: Boolean
val isHingeAngleEnabled: Boolean
val isHapticsEnabled: Boolean
val halfFoldedTimeoutMillis: Int
}
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.unfold.dagger
import javax.inject.Qualifier
/** Qualifier annotation for a progress provider that emits animation events only when
* in natural rotation */
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class NaturalRotation
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 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.unfold.dagger
import javax.inject.Qualifier
/** Annotation for background computations related to unfold lib. */
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnfoldBg
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 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.unfold.dagger
import javax.inject.Qualifier
/**
* Annotates the boolean representing whether we are calculating progresses in the background.
*
* Used to allow clients to provide this value, without depending on the flags directly.
*/
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class UnfoldBgProgressFlag
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2023 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.unfold.progress
import android.os.Handler
import androidx.annotation.FloatRange
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.dagger.UnfoldMain
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.Collections.synchronizedMap
/**
* [UnfoldTransitionProgressProvider] that forwards all progress to the main thread handler.
*
* This is needed when progress are calculated in the background, but some listeners need the
* callbacks in the main thread.
*
* Note that this class assumes that the root provider has thread safe callback registration, as
* they might be called from any thread.
*/
class MainThreadUnfoldTransitionProgressProvider
@AssistedInject
constructor(
@UnfoldMain private val mainHandler: Handler,
@Assisted private val rootProvider: UnfoldTransitionProgressProvider
) : UnfoldTransitionProgressProvider {
private val listenerMap: MutableMap<TransitionProgressListener, TransitionProgressListener> =
synchronizedMap(mutableMapOf())
override fun addCallback(listener: TransitionProgressListener) {
val proxy = TransitionProgressListerProxy(listener)
rootProvider.addCallback(proxy)
listenerMap[listener] = proxy
}
override fun removeCallback(listener: TransitionProgressListener) {
val proxy = listenerMap.remove(listener) ?: return
rootProvider.removeCallback(proxy)
}
override fun destroy() {
rootProvider.destroy()
}
inner class TransitionProgressListerProxy(private val listener: TransitionProgressListener) :
TransitionProgressListener {
override fun onTransitionStarted() {
mainHandler.post { listener.onTransitionStarted() }
}
override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
mainHandler.post { listener.onTransitionProgress(progress) }
}
override fun onTransitionFinishing() {
mainHandler.post { listener.onTransitionFinishing() }
}
override fun onTransitionFinished() {
mainHandler.post { listener.onTransitionFinished() }
}
}
@AssistedFactory
interface Factory {
/** Creates a [MainThreadUnfoldTransitionProgressProvider] that wraps the [rootProvider]. */
fun create(
rootProvider: UnfoldTransitionProgressProvider
): MainThreadUnfoldTransitionProgressProvider
}
}
@@ -20,6 +20,7 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.os.Handler
import android.os.Trace
import android.util.FloatProperty
import android.util.Log
@@ -38,13 +39,25 @@ import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.unfold.updates.name
import javax.inject.Inject
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
/** Maps fold updates to unfold transition progress using DynamicAnimation. */
/**
* Maps fold updates to unfold transition progress using DynamicAnimation.
*
* Note that all variable accesses must be done in the [Handler] provided in the constructor, that
* might be different than [mainHandler]. When a custom handler is provided, the [SpringAnimation]
* uses a scheduler different than the default one.
*/
class PhysicsBasedUnfoldTransitionProgressProvider
@Inject
constructor(context: Context, private val foldStateProvider: FoldStateProvider) :
UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
@AssistedInject
constructor(
context: Context,
private val schedulerFactory: UnfoldFrameCallbackScheduler.Factory,
@Assisted private val foldStateProvider: FoldStateProvider,
@Assisted private val progressHandler: Handler,
) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
private val emphasizedInterpolator =
loadInterpolator(context, android.R.interpolator.fast_out_extra_slow_in)
@@ -63,6 +76,7 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
private var transitionProgress: Float = 0.0f
set(value) {
assertInProgressThread()
if (isTransitionRunning) {
listeners.forEach { it.onTransitionProgress(value) }
}
@@ -72,8 +86,14 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
init {
foldStateProvider.addCallback(this)
foldStateProvider.start()
progressHandler.post {
// The scheduler needs to be created in the progress handler in order to get the correct
// choreographer and frame callbacks. This is because the choreographer can be get only
// as a thread local.
springAnimation.scheduler = schedulerFactory.create()
foldStateProvider.addCallback(this)
foldStateProvider.start()
}
}
override fun destroy() {
@@ -81,6 +101,8 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
}
override fun onHingeAngleUpdate(angle: Float) {
assertInProgressThread()
if (!isTransitionRunning || isAnimatedCancelRunning) return
val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION)
springAnimation.animateToFinalPosition(progress)
@@ -90,6 +112,7 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
if (amount < low) low else if (amount > high) high else amount
override fun onFoldUpdate(@FoldUpdate update: Int) {
assertInProgressThread()
when (update) {
FOLD_UPDATE_FINISH_FULL_OPEN,
FOLD_UPDATE_FINISH_HALF_OPEN -> {
@@ -148,6 +171,7 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
}
private fun cancelTransition(endValue: Float, animate: Boolean) {
assertInProgressThread()
if (isTransitionRunning && animate) {
if (endValue == 1.0f && !isAnimatedCancelRunning) {
listeners.forEach { it.onTransitionFinishing() }
@@ -165,7 +189,6 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
isAnimatedCancelRunning = false
isTransitionRunning = false
springAnimation.cancel()
cannedAnimator?.removeAllListeners()
cannedAnimator?.cancel()
cannedAnimator = null
@@ -182,7 +205,7 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
animation: DynamicAnimation<out DynamicAnimation<*>>,
canceled: Boolean,
value: Float,
velocity: Float
velocity: Float,
) {
if (isAnimatedCancelRunning) {
cancelTransition(value, animate = false)
@@ -202,6 +225,7 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
}
private fun startTransition(startValue: Float) {
assertInProgressThread()
if (!isTransitionRunning) onStartTransition()
springAnimation.apply {
@@ -221,14 +245,16 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
}
override fun addCallback(listener: TransitionProgressListener) {
listeners.add(listener)
progressHandler.post { listeners.add(listener) }
}
override fun removeCallback(listener: TransitionProgressListener) {
listeners.remove(listener)
progressHandler.post { listeners.remove(listener) }
}
private fun startCannedCancelAnimation() {
assertInProgressThread()
cannedAnimator?.cancel()
cannedAnimator = null
@@ -264,7 +290,7 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
override fun setValue(
provider: PhysicsBasedUnfoldTransitionProgressProvider,
value: Float
value: Float,
) {
provider.transitionProgress = value
}
@@ -272,6 +298,25 @@ constructor(context: Context, private val foldStateProvider: FoldStateProvider)
override fun get(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
provider.transitionProgress
}
private fun assertInProgressThread() {
check(progressHandler.looper.isCurrentThread) {
val progressThread = progressHandler.looper.thread
val thisThread = Thread.currentThread()
"""should be called from the progress thread.
progressThread=$progressThread tid=${progressThread.id}
Thread.currentThread()=$thisThread tid=${thisThread.id}"""
.trimMargin()
}
}
@AssistedFactory
interface Factory {
fun create(
foldStateProvider: FoldStateProvider,
handler: Handler,
): PhysicsBasedUnfoldTransitionProgressProvider
}
}
private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider"
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 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.unfold.progress
import android.os.Looper
import android.view.Choreographer
import androidx.dynamicanimation.animation.FrameCallbackScheduler
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
/**
* Scheduler that posts animation progresses on a thread different than the ui one.
*
* The following is taken from [AnimationHandler.FrameCallbackScheduler16]. It is extracted here as
* there are no guarantees which implementation the [DynamicAnimation] class would use otherwise.
* This allows classes using [DynamicAnimation] to be created in any thread, but still use the
* scheduler for a specific thread.
*
* Technically the [AssistedInject] is not needed: it's just to have a nicer factory with a
* documentation snippet instead of using a plain dagger provider.
*/
class UnfoldFrameCallbackScheduler @AssistedInject constructor() : FrameCallbackScheduler {
private val choreographer = Choreographer.getInstance()
private val looper =
Looper.myLooper() ?: error("This should be created in a thread with a looper.")
override fun postFrameCallback(frameCallback: Runnable) {
choreographer.postFrameCallback { frameCallback.run() }
}
override fun isCurrentThread(): Boolean {
return looper.isCurrentThread
}
@AssistedFactory
interface Factory {
/**
* Creates a [FrameCallbackScheduler] that uses [Choreographer] to post frame callbacks.
*
* Note that the choreographer used depends on the thread this [create] is called on, as it
* is get from a thread static attribute.
*/
fun create(): UnfoldFrameCallbackScheduler
}
}
@@ -41,8 +41,6 @@ class UnfoldRemoteFilter(
if (inProgress) {
logCounter({ "$TAG#filtered_progress" }, newProgress)
listener.onTransitionProgress(newProgress)
} else {
Log.e(TAG, "Filtered progress received received while animation not in progress.")
}
field = newProgress
}
@@ -17,44 +17,41 @@ package com.android.systemui.unfold.updates
import android.content.Context
import android.os.Handler
import android.os.Trace
import android.util.Log
import androidx.annotation.FloatRange
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.core.util.Consumer
import com.android.systemui.unfold.compat.INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
import com.android.systemui.unfold.config.UnfoldTransitionConfig
import com.android.systemui.unfold.dagger.UnfoldMain
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES
import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
import com.android.systemui.unfold.util.CurrentActivityTypeProvider
import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import javax.inject.Inject
class DeviceFoldStateProvider
@Inject
@AssistedInject
constructor(
config: UnfoldTransitionConfig,
private val hingeAngleProvider: HingeAngleProvider,
private val context: Context,
private val screenStatusProvider: ScreenStatusProvider,
private val foldProvider: FoldProvider,
private val activityTypeProvider: CurrentActivityTypeProvider,
private val unfoldKeyguardVisibilityProvider: UnfoldKeyguardVisibilityProvider,
private val rotationChangeProvider: RotationChangeProvider,
private val context: Context,
@UnfoldMain private val mainExecutor: Executor,
@UnfoldMain private val handler: Handler
private val foldProvider: FoldProvider,
@Assisted private val hingeAngleProvider: HingeAngleProvider,
@Assisted private val rotationChangeProvider: RotationChangeProvider,
@Assisted private val progressHandler: Handler,
) : FoldStateProvider {
private val outputListeners = CopyOnWriteArrayList<FoldStateProvider.FoldUpdatesListener>()
private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
@FoldUpdate private var lastFoldUpdate: Int? = null
@FoldStateProvider.FoldUpdate private var lastFoldUpdate: Int? = null
@FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
@FloatRange(from = 0.0, to = 180.0) private var lastHingeAngleBeforeTransition: Float = 0f
@@ -62,11 +59,9 @@ constructor(
private val hingeAngleListener = HingeAngleListener()
private val screenListener = ScreenStatusListener()
private val foldStateListener = FoldStateListener()
private val mainLooper = handler.looper
private val timeoutRunnable = Runnable { cancelAnimation() }
private val rotationListener = RotationListener {
if (isTransitionInProgress) cancelAnimation()
}
private val rotationListener = FoldRotationListener()
private val progressExecutor = Executor { progressHandler.post(it) }
/**
* Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
@@ -81,9 +76,9 @@ constructor(
private var isStarted = false
override fun start() {
assertMainThread()
if (isStarted) return
foldProvider.registerCallback(foldStateListener, mainExecutor)
foldProvider.registerCallback(foldStateListener, progressExecutor)
// TODO(b/277879146): get callbacks in the background
screenStatusProvider.addCallback(screenListener)
hingeAngleProvider.addCallback(hingeAngleListener)
rotationChangeProvider.addCallback(rotationListener)
@@ -92,7 +87,6 @@ constructor(
}
override fun stop() {
assertMainThread()
screenStatusProvider.removeCallback(screenListener)
foldProvider.unregisterCallback(foldStateListener)
hingeAngleProvider.removeCallback(hingeAngleListener)
@@ -102,11 +96,11 @@ constructor(
isStarted = false
}
override fun addCallback(listener: FoldUpdatesListener) {
override fun addCallback(listener: FoldStateProvider.FoldUpdatesListener) {
outputListeners.add(listener)
}
override fun removeCallback(listener: FoldUpdatesListener) {
override fun removeCallback(listener: FoldStateProvider.FoldUpdatesListener) {
outputListeners.remove(listener)
}
@@ -122,6 +116,7 @@ constructor(
lastFoldUpdate == FOLD_UPDATE_START_CLOSING
private fun onHingeAngle(angle: Float) {
assertInProgressThread()
if (DEBUG) {
Log.d(
TAG,
@@ -130,17 +125,16 @@ constructor(
"lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition"
)
}
Trace.setCounter("DeviceFoldStateProvider#onHingeAngle", angle.toLong())
val currentDirection =
if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
if (isTransitionInProgress && currentDirection != lastFoldUpdate) {
lastHingeAngleBeforeTransition = lastHingeAngle
}
val isClosing = angle < lastHingeAngleBeforeTransition
val transitionUpdate =
if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
val angleChangeSurpassedThreshold =
Math.abs(angle - lastHingeAngleBeforeTransition) > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
@@ -152,12 +146,12 @@ constructor(
angleChangeSurpassedThreshold && // Do not react immediately to small changes in angle
eventNotAlreadyDispatched && // we haven't sent transition event already
!isFullyOpened && // do not send transition event if we are in fully opened hinge
// angle range as closing threshold could overlap this range
// angle range as closing threshold could overlap this range
screenAvailableEventSent && // do not send transition event if we are still in the
// process of turning on the inner display
// process of turning on the inner display
isClosingThresholdMet(angle) && // hinge angle is below certain threshold.
isOnLargeScreen // Avoids sending closing event when on small screen.
// Start event is sent regardless due to hall sensor.
// Start event is sent regardless due to hall sensor.
) {
notifyFoldUpdate(transitionUpdate, lastHingeAngle)
}
@@ -204,6 +198,7 @@ constructor(
private inner class FoldStateListener : FoldProvider.FoldCallback {
override fun onFoldUpdated(isFolded: Boolean) {
assertInProgressThread()
this@DeviceFoldStateProvider.isFolded = isFolded
lastHingeAngle = FULLY_CLOSED_DEGREES
@@ -220,7 +215,15 @@ constructor(
}
}
private fun notifyFoldUpdate(@FoldUpdate update: Int, angle: Float) {
private inner class FoldRotationListener : RotationChangeProvider.RotationListener {
@WorkerThread
override fun onRotationChanged(newRotation: Int) {
assertInProgressThread()
if (isTransitionInProgress) cancelAnimation()
}
}
private fun notifyFoldUpdate(@FoldStateProvider.FoldUpdate update: Int, angle: Float) {
if (DEBUG) {
Log.d(TAG, update.name())
}
@@ -238,11 +241,11 @@ constructor(
if (isTransitionInProgress) {
cancelTimeout()
}
handler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong())
progressHandler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong())
}
private fun cancelTimeout() {
handler.removeCallbacks(timeoutRunnable)
progressHandler.removeCallbacks(timeoutRunnable)
}
private fun cancelAnimation(): Unit =
@@ -251,42 +254,61 @@ constructor(
private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
override fun onScreenTurnedOn() {
// Trigger this event only if we are unfolded and this is the first screen
// turned on event since unfold started. This prevents running the animation when
// turning on the internal display using the power button.
// Initially isUnfoldHandled is true so it will be reset to false *only* when we
// receive 'folded' event. If SystemUI started when device is already folded it will
// still receive 'folded' event on startup.
if (!isFolded && !isUnfoldHandled) {
outputListeners.forEach { it.onUnfoldedScreenAvailable() }
isUnfoldHandled = true
executeInProgressThread {
// Trigger this event only if we are unfolded and this is the first screen
// turned on event since unfold started. This prevents running the animation when
// turning on the internal display using the power button.
// Initially isUnfoldHandled is true so it will be reset to false *only* when we
// receive 'folded' event. If SystemUI started when device is already folded it will
// still receive 'folded' event on startup.
if (!isFolded && !isUnfoldHandled) {
outputListeners.forEach { it.onUnfoldedScreenAvailable() }
isUnfoldHandled = true
}
}
}
override fun markScreenAsTurnedOn() {
if (!isFolded) {
isUnfoldHandled = true
executeInProgressThread {
if (!isFolded) {
isUnfoldHandled = true
}
}
}
override fun onScreenTurningOn() {
isScreenOn = true
updateHingeAngleProviderState()
executeInProgressThread {
isScreenOn = true
updateHingeAngleProviderState()
}
}
override fun onScreenTurningOff() {
isScreenOn = false
updateHingeAngleProviderState()
executeInProgressThread {
isScreenOn = false
updateHingeAngleProviderState()
}
}
/**
* Needed just for compatibility while not all data sources are providing data in the
* background.
*
* TODO(b/277879146): Remove once ScreeStatusProvider provides in the background.
*/
private fun executeInProgressThread(f: () -> Unit) {
progressHandler.post { f() }
}
}
private fun isOnLargeScreen(): Boolean {
return context.resources.configuration.smallestScreenWidthDp >
INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
return context.resources.configuration.smallestScreenWidthDp >
INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
}
/** While the screen is off or the device is folded, hinge angle updates are not needed. */
private fun updateHingeAngleProviderState() {
assertInProgressThread()
if (isScreenOn && !isFolded) {
hingeAngleProvider.start()
} else {
@@ -296,20 +318,34 @@ constructor(
private inner class HingeAngleListener : Consumer<Float> {
override fun accept(angle: Float) {
assertInProgressThread()
onHingeAngle(angle)
}
}
private fun assertMainThread() {
check(mainLooper.isCurrentThread) {
("should be called from the main thread." +
" sMainLooper.threadName=" + mainLooper.thread.name +
" Thread.currentThread()=" + Thread.currentThread().name)
private fun assertInProgressThread() {
check(progressHandler.looper.isCurrentThread) {
val progressThread = progressHandler.looper.thread
val thisThread = Thread.currentThread()
"""should be called from the progress thread.
progressThread=$progressThread tid=${progressThread.id}
Thread.currentThread()=$thisThread tid=${thisThread.id}"""
.trimMargin()
}
}
@AssistedFactory
interface Factory {
/** Creates a [DeviceFoldStateProvider] using the provided dependencies. */
fun create(
hingeAngleProvider: HingeAngleProvider,
rotationChangeProvider: RotationChangeProvider,
progressHandler: Handler,
): DeviceFoldStateProvider
}
}
fun @receiver:FoldUpdate Int.name() =
fun @receiver:FoldStateProvider.FoldUpdate Int.name() =
when (this) {
FOLD_UPDATE_START_OPENING -> "START_OPENING"
FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
@@ -20,7 +20,7 @@ interface FoldProvider {
fun registerCallback(callback: FoldCallback, executor: Executor)
fun unregisterCallback(callback: FoldCallback)
interface FoldCallback {
fun interface FoldCallback {
fun onFoldUpdated(isFolded: Boolean)
}
}
@@ -20,29 +20,38 @@ import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Handler
import android.os.RemoteException
import com.android.systemui.unfold.dagger.UnfoldMain
import android.os.Trace
import androidx.annotation.AnyThread
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.util.CallbackController
import javax.inject.Inject
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
private const val INVALID_ROTATION = -1
/**
* Allows to subscribe to rotation changes. Updates are provided for the display associated
* to [context].
* Allows to subscribe to rotation changes. Updates are provided for the display associated to
* [context].
*/
class RotationChangeProvider
@Inject
@AssistedInject
constructor(
private val displayManager: DisplayManager,
private val context: Context,
@UnfoldMain private val mainHandler: Handler,
@UnfoldBg private val bgHandler: Handler,
@Assisted private val callbackHandler: Handler,
) : CallbackController<RotationChangeProvider.RotationListener> {
private val listeners = mutableListOf<RotationListener>()
private val listeners = CopyOnWriteArrayList<RotationListener>()
private val displayListener = RotationDisplayListener()
private var lastRotation: Int? = null
private val lastRotation = AtomicInteger(INVALID_ROTATION)
override fun addCallback(listener: RotationListener) {
mainHandler.post {
bgHandler.post {
if (listeners.isEmpty()) {
subscribeToRotation()
}
@@ -51,18 +60,18 @@ constructor(
}
override fun removeCallback(listener: RotationListener) {
mainHandler.post {
bgHandler.post {
listeners -= listener
if (listeners.isEmpty()) {
unsubscribeToRotation()
lastRotation = null
lastRotation.set(INVALID_ROTATION)
}
}
}
private fun subscribeToRotation() {
try {
displayManager.registerDisplayListener(displayListener, mainHandler)
displayManager.registerDisplayListener(displayListener, callbackHandler)
} catch (e: RemoteException) {
throw e.rethrowFromSystemServer()
}
@@ -78,21 +87,28 @@ constructor(
/** Gets notified of rotation changes. */
fun interface RotationListener {
/** Called once rotation changes. */
fun onRotationChanged(newRotation: Int)
/**
* Called once rotation changes. This callback is called on the handler provided to
* [RotationChangeProvider.Factory.create].
*/
@AnyThread fun onRotationChanged(newRotation: Int)
}
private inner class RotationDisplayListener : DisplayManager.DisplayListener {
override fun onDisplayChanged(displayId: Int) {
val display = context.display ?: return
Trace.beginSection("RotationChangeProvider.RotationDisplayListener#onDisplayChanged")
try {
val display = context.display ?: return
if (displayId == display.displayId) {
val currentRotation = display.rotation
if (lastRotation == null || lastRotation != currentRotation) {
listeners.forEach { it.onRotationChanged(currentRotation) }
lastRotation = currentRotation
if (displayId == display.displayId) {
val currentRotation = display.rotation
if (lastRotation.compareAndSet(lastRotation.get(), currentRotation)) {
listeners.forEach { it.onRotationChanged(currentRotation) }
}
}
} finally {
Trace.endSection()
}
}
@@ -100,4 +116,10 @@ constructor(
override fun onDisplayRemoved(displayId: Int) {}
}
@AssistedFactory
interface Factory {
/** Creates a new [RotationChangeProvider] that provides updated using [callbackHandler]. */
fun create(callbackHandler: Handler): RotationChangeProvider
}
}
@@ -18,21 +18,26 @@ import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Handler
import android.os.Trace
import androidx.core.util.Consumer
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import javax.inject.Inject
internal class HingeSensorAngleProvider
@Inject
@AssistedInject
constructor(
private val sensorManager: SensorManager,
@UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor
@UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor,
@Assisted private val listenerHandler: Handler,
) : HingeAngleProvider {
private val sensorListener = HingeAngleSensorListener()
private val listeners: MutableList<Consumer<Float>> = arrayListOf()
private val listeners: MutableList<Consumer<Float>> = CopyOnWriteArrayList()
var started = false
override fun start() {
@@ -43,7 +48,8 @@ constructor(
sensorManager.registerListener(
sensorListener,
sensor,
SensorManager.SENSOR_DELAY_FASTEST
SensorManager.SENSOR_DELAY_FASTEST,
listenerHandler
)
Trace.endSection()
@@ -75,4 +81,10 @@ constructor(
listeners.forEach { it.accept(event.values[0]) }
}
}
@AssistedFactory
interface Factory {
/** Creates an [HingeSensorAngleProvider] that sends updates using [handler]. */
fun create(handler: Handler): HingeSensorAngleProvider
}
}
@@ -16,7 +16,9 @@ package com.android.systemui.unfold.util
import android.os.Trace
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import javax.inject.Inject
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import javax.inject.Qualifier
/**
@@ -26,11 +28,11 @@ import javax.inject.Qualifier
* for each fold/unfold: in (1) systemui and (2) launcher process.
*/
class ATraceLoggerTransitionProgressListener
@Inject
internal constructor(@UnfoldTransitionATracePrefix tracePrefix: String) :
@AssistedInject
internal constructor(@UnfoldTransitionATracePrefix tracePrefix: String, @Assisted details: String) :
TransitionProgressListener {
private val traceName = "$tracePrefix#$UNFOLD_TRANSITION_TRACE_NAME"
private val traceName = "$tracePrefix$details#$UNFOLD_TRANSITION_TRACE_NAME"
override fun onTransitionStarted() {
Trace.beginAsyncSection(traceName, /* cookie= */ 0)
@@ -43,6 +45,12 @@ internal constructor(@UnfoldTransitionATracePrefix tracePrefix: String) :
override fun onTransitionProgress(progress: Float) {
Trace.setCounter(traceName, (progress * 100).toLong())
}
@AssistedFactory
interface Factory {
/** Creates an [ATraceLoggerTransitionProgressListener] with [details] in the track name. */
fun create(details: String): ATraceLoggerTransitionProgressListener
}
}
private const val UNFOLD_TRANSITION_TRACE_NAME = "FoldUnfoldTransitionInProgress"
@@ -15,8 +15,12 @@
*/
package com.android.systemui.unfold.util
import android.os.Handler
import android.os.Looper
import androidx.annotation.GuardedBy
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import java.util.concurrent.CopyOnWriteArrayList
/**
* Manages progress listeners that can have smaller lifespan than the unfold animation.
@@ -33,12 +37,17 @@ open class ScopedUnfoldTransitionProgressProvider
constructor(source: UnfoldTransitionProgressProvider? = null) :
UnfoldTransitionProgressProvider, TransitionProgressListener {
private var progressHandler: Handler? = null
private var source: UnfoldTransitionProgressProvider? = null
private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
private val listeners = CopyOnWriteArrayList<TransitionProgressListener>()
private var isReadyToHandleTransition = false
private val lock = Object()
@GuardedBy("lock") private var isReadyToHandleTransition = false
// Accessed only from progress thread
private var isTransitionRunning = false
// Accessed only from progress thread
private var lastTransitionProgress = PROGRESS_UNSET
init {
@@ -68,9 +77,34 @@ constructor(source: UnfoldTransitionProgressProvider? = null) :
* the transition progress events.
*
* Call it with readyToHandleTransition = false when listeners can't process the events.
*
* Note that this could be called by any thread.
*/
fun setReadyToHandleTransition(isReadyToHandleTransition: Boolean) {
if (isTransitionRunning) {
synchronized(lock) {
this.isReadyToHandleTransition = isReadyToHandleTransition
val progressHandlerLocal = this.progressHandler
if (progressHandlerLocal != null) {
ensureInHandler(progressHandlerLocal) { reportLastProgressIfNeeded() }
}
}
}
/** Runs directly if called from the handler thread. Posts otherwise. */
private fun ensureInHandler(handler: Handler, f: () -> Unit) {
if (handler.looper.isCurrentThread) {
f()
} else {
handler.post(f)
}
}
private fun reportLastProgressIfNeeded() {
assertInProgressThread()
synchronized(lock) {
if (!isTransitionRunning) {
return
}
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionStarted() }
if (lastTransitionProgress != PROGRESS_UNSET) {
@@ -81,7 +115,6 @@ constructor(source: UnfoldTransitionProgressProvider? = null) :
listeners.forEach { it.onTransitionFinished() }
}
}
this.isReadyToHandleTransition = isReadyToHandleTransition
}
override fun addCallback(listener: TransitionProgressListener) {
@@ -98,34 +131,61 @@ constructor(source: UnfoldTransitionProgressProvider? = null) :
}
override fun onTransitionStarted() {
isTransitionRunning = true
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionStarted() }
assertInProgressThread()
synchronized(lock) {
isTransitionRunning = true
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionStarted() }
}
}
}
override fun onTransitionProgress(progress: Float) {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionProgress(progress) }
assertInProgressThread()
synchronized(lock) {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionProgress(progress) }
}
lastTransitionProgress = progress
}
lastTransitionProgress = progress
}
override fun onTransitionFinishing() {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionFinishing() }
assertInProgressThread()
synchronized(lock) {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionFinishing() }
}
}
}
override fun onTransitionFinished() {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionFinished() }
assertInProgressThread()
synchronized(lock) {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionFinished() }
}
isTransitionRunning = false
lastTransitionProgress = PROGRESS_UNSET
}
isTransitionRunning = false
lastTransitionProgress = PROGRESS_UNSET
}
companion object {
private const val PROGRESS_UNSET = -1f
private fun assertInProgressThread() {
val cachedProgressHandler = progressHandler
if (cachedProgressHandler == null) {
val thisLooper = Looper.myLooper() ?: error("This thread is expected to have a looper.")
progressHandler = Handler(thisLooper)
} else {
check(cachedProgressHandler.looper.isCurrentThread) {
"""Receiving unfold transition callback from different threads.
|Current: ${Thread.currentThread()}
|expected: ${cachedProgressHandler.looper.thread}"""
.trimMargin()
}
}
}
private companion object {
const val PROGRESS_UNSET = -1f
}
}