update uiunfold
This commit is contained in:
1 parent
7571d54a1a
commit
f751457b95
41 files changed
+4460
-1919
No files matched your search
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
+89
@@ -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
|
||||
}
|
||||
}
|
||||
+57
-12
@@ -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
|
||||
}
|
||||
}
|
||||
+17
-5
@@ -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
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -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"
|
||||
|
||||
+78
-18
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user