| /* |
| * Copyright (C) 2021 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.window.extensions.embedding; |
| |
| import static android.view.RemoteAnimationTarget.MODE_CLOSING; |
| |
| import android.app.ActivityThread; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.graphics.Rect; |
| import android.os.Handler; |
| import android.provider.Settings; |
| import android.view.RemoteAnimationTarget; |
| import android.view.WindowManager; |
| import android.view.animation.AlphaAnimation; |
| import android.view.animation.Animation; |
| import android.view.animation.AnimationSet; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.ClipRectAnimation; |
| import android.view.animation.Interpolator; |
| import android.view.animation.LinearInterpolator; |
| import android.view.animation.ScaleAnimation; |
| import android.view.animation.TranslateAnimation; |
| |
| import androidx.annotation.NonNull; |
| |
| import com.android.internal.R; |
| import com.android.internal.policy.AttributeCache; |
| import com.android.internal.policy.TransitionAnimation; |
| |
| /** Animation spec for TaskFragment transition. */ |
| // TODO(b/206557124): provide an easier way to customize animation |
| class TaskFragmentAnimationSpec { |
| |
| private static final String TAG = "TaskFragAnimationSpec"; |
| private static final int CHANGE_ANIMATION_DURATION = 517; |
| private static final int CHANGE_ANIMATION_FADE_DURATION = 80; |
| private static final int CHANGE_ANIMATION_FADE_OFFSET = 30; |
| |
| private final Context mContext; |
| private final TransitionAnimation mTransitionAnimation; |
| private final Interpolator mFastOutExtraSlowInInterpolator; |
| private final LinearInterpolator mLinearInterpolator; |
| private float mTransitionAnimationScaleSetting; |
| |
| TaskFragmentAnimationSpec(@NonNull Handler handler) { |
| mContext = ActivityThread.currentActivityThread().getApplication(); |
| mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG); |
| // Initialize the AttributeCache for the TransitionAnimation. |
| AttributeCache.init(mContext); |
| mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( |
| mContext, android.R.interpolator.fast_out_extra_slow_in); |
| mLinearInterpolator = new LinearInterpolator(); |
| |
| // The transition animation should be adjusted based on the developer option. |
| final ContentResolver resolver = mContext.getContentResolver(); |
| mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); |
| resolver.registerContentObserver( |
| Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false, |
| new SettingsObserver(handler)); |
| } |
| |
| /** For target that doesn't need to be animated. */ |
| @NonNull |
| static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) { |
| // Noop but just keep the target showing/hiding. |
| final float alpha = target.mode == MODE_CLOSING ? 0f : 1f; |
| return new AlphaAnimation(alpha, alpha); |
| } |
| |
| /** Animation for target that is opening in a change transition. */ |
| @NonNull |
| Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) { |
| final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); |
| final Rect bounds = target.screenSpaceBounds; |
| final int startLeft; |
| final int startTop; |
| if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { |
| // The window will be animated in from left or right depending on its position. |
| startTop = 0; |
| startLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); |
| } else { |
| // The window will be animated in from top or bottom depending on its position. |
| startTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); |
| startLeft = 0; |
| } |
| |
| // The position should be 0-based as we will post translate in |
| // TaskFragmentAnimationAdapter#onAnimationUpdate |
| final Animation animation = new TranslateAnimation(startLeft, 0, startTop, 0); |
| animation.setInterpolator(mFastOutExtraSlowInInterpolator); |
| animation.setDuration(CHANGE_ANIMATION_DURATION); |
| animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); |
| animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); |
| return animation; |
| } |
| |
| /** Animation for target that is closing in a change transition. */ |
| @NonNull |
| Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) { |
| final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); |
| // Use startBounds if the window is closing in case it may also resize. |
| final Rect bounds = target.startBounds; |
| final int endTop; |
| final int endLeft; |
| if (parentBounds.top == bounds.top && parentBounds.bottom == bounds.bottom) { |
| // The window will be animated out to left or right depending on its position. |
| endTop = 0; |
| endLeft = parentBounds.left == bounds.left ? -bounds.width() : bounds.width(); |
| } else { |
| // The window will be animated out to top or bottom depending on its position. |
| endTop = parentBounds.top == bounds.top ? -bounds.height() : bounds.height(); |
| endLeft = 0; |
| } |
| |
| // The position should be 0-based as we will post translate in |
| // TaskFragmentAnimationAdapter#onAnimationUpdate |
| final Animation animation = new TranslateAnimation(0, endLeft, 0, endTop); |
| animation.setInterpolator(mFastOutExtraSlowInInterpolator); |
| animation.setDuration(CHANGE_ANIMATION_DURATION); |
| animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); |
| animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); |
| return animation; |
| } |
| |
| /** |
| * Animation for target that is changing (bounds change) in a change transition. |
| * @return the return array always has two elements. The first one is for the start leash, and |
| * the second one is for the end leash. |
| */ |
| @NonNull |
| Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) { |
| // Both start bounds and end bounds are in screen coordinates. We will post translate |
| // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate |
| final Rect startBounds = target.startBounds; |
| final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); |
| final Rect endBounds = target.screenSpaceBounds; |
| float scaleX = ((float) startBounds.width()) / endBounds.width(); |
| float scaleY = ((float) startBounds.height()) / endBounds.height(); |
| // Start leash is a child of the end leash. Reverse the scale so that the start leash won't |
| // be scaled up with its parent. |
| float startScaleX = 1.f / scaleX; |
| float startScaleY = 1.f / scaleY; |
| |
| // The start leash will be fade out. |
| final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */); |
| final Animation startAlpha = new AlphaAnimation(1f, 0f); |
| startAlpha.setInterpolator(mLinearInterpolator); |
| startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION); |
| startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET); |
| startSet.addAnimation(startAlpha); |
| final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY, |
| startScaleY); |
| startScale.setInterpolator(mFastOutExtraSlowInInterpolator); |
| startScale.setDuration(CHANGE_ANIMATION_DURATION); |
| startSet.addAnimation(startScale); |
| startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(), |
| endBounds.height()); |
| startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); |
| |
| // The end leash will be moved into the end position while scaling. |
| final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */); |
| endSet.setInterpolator(mFastOutExtraSlowInInterpolator); |
| final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1); |
| endScale.setDuration(CHANGE_ANIMATION_DURATION); |
| endSet.addAnimation(endScale); |
| // The position should be 0-based as we will post translate in |
| // TaskFragmentAnimationAdapter#onAnimationUpdate |
| final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, |
| startBounds.top - endBounds.top, 0); |
| endTranslate.setDuration(CHANGE_ANIMATION_DURATION); |
| endSet.addAnimation(endTranslate); |
| // The end leash is resizing, we should update the window crop based on the clip rect. |
| final Rect startClip = new Rect(startBounds); |
| final Rect endClip = new Rect(endBounds); |
| startClip.offsetTo(0, 0); |
| endClip.offsetTo(0, 0); |
| final Animation clipAnim = new ClipRectAnimation(startClip, endClip); |
| clipAnim.setDuration(CHANGE_ANIMATION_DURATION); |
| endSet.addAnimation(clipAnim); |
| endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(), |
| parentBounds.height()); |
| endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); |
| |
| return new Animation[]{startSet, endSet}; |
| } |
| |
| @NonNull |
| Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target, |
| @NonNull Rect wholeAnimationBounds) { |
| final boolean isEnter = target.mode != MODE_CLOSING; |
| final Animation animation; |
| // Background color on TaskDisplayArea has already been set earlier in |
| // WindowContainer#getAnimationAdapter. |
| if (target.showBackdrop) { |
| animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter |
| ? com.android.internal.R.anim.task_fragment_clear_top_open_enter |
| : com.android.internal.R.anim.task_fragment_clear_top_open_exit); |
| } else { |
| animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter |
| ? com.android.internal.R.anim.task_fragment_open_enter |
| : com.android.internal.R.anim.task_fragment_open_exit); |
| } |
| // Use the whole animation bounds instead of the change bounds, so that when multiple change |
| // targets are opening at the same time, the animation applied to each will be the same. |
| // Otherwise, we may see gap between the activities that are launching together. |
| animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), |
| wholeAnimationBounds.width(), wholeAnimationBounds.height()); |
| animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); |
| return animation; |
| } |
| |
| @NonNull |
| Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target, |
| @NonNull Rect wholeAnimationBounds) { |
| final boolean isEnter = target.mode != MODE_CLOSING; |
| final Animation animation; |
| if (target.showBackdrop) { |
| animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter |
| ? com.android.internal.R.anim.task_fragment_clear_top_close_enter |
| : com.android.internal.R.anim.task_fragment_clear_top_close_exit); |
| } else { |
| animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter |
| ? com.android.internal.R.anim.task_fragment_close_enter |
| : com.android.internal.R.anim.task_fragment_close_exit); |
| } |
| // Use the whole animation bounds instead of the change bounds, so that when multiple change |
| // targets are closing at the same time, the animation applied to each will be the same. |
| // Otherwise, we may see gap between the activities that are finishing together. |
| animation.initialize(wholeAnimationBounds.width(), wholeAnimationBounds.height(), |
| wholeAnimationBounds.width(), wholeAnimationBounds.height()); |
| animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); |
| return animation; |
| } |
| |
| private float getTransitionAnimationScaleSetting() { |
| return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(), |
| Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( |
| R.dimen.config_appTransitionAnimationDurationScaleDefault))); |
| } |
| |
| private class SettingsObserver extends ContentObserver { |
| SettingsObserver(@NonNull Handler handler) { |
| super(handler); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); |
| } |
| } |
| } |