Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2020 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.systemui.stackdivider; |
| 18 | |
| 19 | import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; |
| 20 | import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; |
| 21 | |
| 22 | import android.animation.Animator; |
| 23 | import android.animation.AnimatorListenerAdapter; |
| 24 | import android.animation.ValueAnimator; |
| 25 | import android.graphics.Rect; |
| 26 | import android.os.Handler; |
| 27 | import android.util.Slog; |
| 28 | import android.view.SurfaceControl; |
| 29 | import android.window.TaskOrganizer; |
| 30 | import android.window.WindowContainerToken; |
| 31 | import android.window.WindowContainerTransaction; |
Evan Rosky | e1a31c9 | 2020-06-18 16:46:47 -0700 | [diff] [blame] | 32 | import android.window.WindowOrganizer; |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 33 | |
| 34 | import androidx.annotation.Nullable; |
| 35 | |
| 36 | import com.android.systemui.TransactionPool; |
| 37 | import com.android.systemui.wm.DisplayImeController; |
| 38 | |
| 39 | class DividerImeController implements DisplayImeController.ImePositionProcessor { |
| 40 | private static final String TAG = "DividerImeController"; |
| 41 | private static final boolean DEBUG = Divider.DEBUG; |
| 42 | |
| 43 | private static final float ADJUSTED_NONFOCUS_DIM = 0.3f; |
| 44 | |
| 45 | private final SplitScreenTaskOrganizer mSplits; |
| 46 | private final TransactionPool mTransactionPool; |
| 47 | private final Handler mHandler; |
| 48 | |
| 49 | /** |
| 50 | * These are the y positions of the top of the IME surface when it is hidden and when it is |
| 51 | * shown respectively. These are NOT necessarily the top of the visible IME itself. |
| 52 | */ |
| 53 | private int mHiddenTop = 0; |
| 54 | private int mShownTop = 0; |
| 55 | |
| 56 | // The following are target states (what we are curretly animating towards). |
| 57 | /** |
| 58 | * {@code true} if, at the end of the animation, the split task positions should be |
| 59 | * adjusted by height of the IME. This happens when the secondary split is the IME target. |
| 60 | */ |
| 61 | private boolean mTargetAdjusted = false; |
| 62 | /** |
| 63 | * {@code true} if, at the end of the animation, the IME should be shown/visible |
| 64 | * regardless of what has focus. |
| 65 | */ |
| 66 | private boolean mTargetShown = false; |
| 67 | private float mTargetPrimaryDim = 0.f; |
| 68 | private float mTargetSecondaryDim = 0.f; |
| 69 | |
| 70 | // The following are the current (most recent) states set during animation |
| 71 | /** {@code true} if the secondary split has IME focus. */ |
| 72 | private boolean mSecondaryHasFocus = false; |
| 73 | /** The dimming currently applied to the primary/secondary splits. */ |
| 74 | private float mLastPrimaryDim = 0.f; |
| 75 | private float mLastSecondaryDim = 0.f; |
| 76 | /** The most recent y position of the top of the IME surface */ |
| 77 | private int mLastAdjustTop = -1; |
| 78 | |
| 79 | // The following are states reached last time an animation fully completed. |
| 80 | /** {@code true} if the IME was shown/visible by the last-completed animation. */ |
| 81 | private boolean mImeWasShown = false; |
| 82 | /** {@code true} if the split positions were adjusted by the last-completed animation. */ |
| 83 | private boolean mAdjusted = false; |
| 84 | |
| 85 | /** |
| 86 | * When some aspect of split-screen needs to animate independent from the IME, |
| 87 | * this will be non-null and control split animation. |
| 88 | */ |
| 89 | @Nullable |
| 90 | private ValueAnimator mAnimation = null; |
| 91 | |
| 92 | private boolean mPaused = true; |
| 93 | private boolean mPausedTargetAdjusted = false; |
| 94 | |
| 95 | DividerImeController(SplitScreenTaskOrganizer splits, TransactionPool pool, Handler handler) { |
| 96 | mSplits = splits; |
| 97 | mTransactionPool = pool; |
| 98 | mHandler = handler; |
| 99 | } |
| 100 | |
| 101 | private DividerView getView() { |
| 102 | return mSplits.mDivider.getView(); |
| 103 | } |
| 104 | |
| 105 | private SplitDisplayLayout getLayout() { |
| 106 | return mSplits.mDivider.getSplitLayout(); |
| 107 | } |
| 108 | |
| 109 | private boolean isDividerVisible() { |
| 110 | return mSplits.mDivider.isDividerVisible(); |
| 111 | } |
| 112 | |
| 113 | private boolean getSecondaryHasFocus(int displayId) { |
| 114 | WindowContainerToken imeSplit = TaskOrganizer.getImeTarget(displayId); |
| 115 | return imeSplit != null |
| 116 | && (imeSplit.asBinder() == mSplits.mSecondary.token.asBinder()); |
| 117 | } |
| 118 | |
| 119 | private void updateDimTargets() { |
| 120 | final boolean splitIsVisible = !getView().isHidden(); |
| 121 | mTargetPrimaryDim = (mSecondaryHasFocus && mTargetShown && splitIsVisible) |
| 122 | ? ADJUSTED_NONFOCUS_DIM : 0.f; |
| 123 | mTargetSecondaryDim = (!mSecondaryHasFocus && mTargetShown && splitIsVisible) |
| 124 | ? ADJUSTED_NONFOCUS_DIM : 0.f; |
| 125 | } |
| 126 | |
| 127 | @Override |
| 128 | public void onImeStartPositioning(int displayId, int hiddenTop, int shownTop, |
| 129 | boolean imeShouldShow, SurfaceControl.Transaction t) { |
Evan Rosky | 4dae54d | 2020-06-18 18:36:59 -0700 | [diff] [blame] | 130 | mHiddenTop = hiddenTop; |
| 131 | mShownTop = shownTop; |
| 132 | mTargetShown = imeShouldShow; |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 133 | if (!isDividerVisible()) { |
| 134 | return; |
| 135 | } |
| 136 | final boolean splitIsVisible = !getView().isHidden(); |
| 137 | mSecondaryHasFocus = getSecondaryHasFocus(displayId); |
| 138 | final boolean targetAdjusted = splitIsVisible && imeShouldShow && mSecondaryHasFocus |
Evan Rosky | 4dae54d | 2020-06-18 18:36:59 -0700 | [diff] [blame] | 139 | && !getLayout().mDisplayLayout.isLandscape() && !mSplits.mDivider.isMinimized(); |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 140 | if (mLastAdjustTop < 0) { |
| 141 | mLastAdjustTop = imeShouldShow ? hiddenTop : shownTop; |
| 142 | } else if (mLastAdjustTop != (imeShouldShow ? mShownTop : mHiddenTop)) { |
| 143 | if (mTargetAdjusted != targetAdjusted && targetAdjusted == mAdjusted) { |
| 144 | // Check for an "interruption" of an existing animation. In this case, we |
| 145 | // need to fake-flip the last-known state direction so that the animation |
| 146 | // completes in the other direction. |
| 147 | mAdjusted = mTargetAdjusted; |
| 148 | } else if (targetAdjusted && mTargetAdjusted && mAdjusted) { |
| 149 | // Already fully adjusted for IME, but IME height has changed; so, force-start |
| 150 | // an async animation to the new IME height. |
| 151 | mAdjusted = false; |
| 152 | } |
| 153 | } |
| 154 | if (mPaused) { |
| 155 | mPausedTargetAdjusted = targetAdjusted; |
| 156 | if (DEBUG) Slog.d(TAG, " ime starting but paused " + dumpState()); |
| 157 | return; |
| 158 | } |
| 159 | mTargetAdjusted = targetAdjusted; |
| 160 | updateDimTargets(); |
| 161 | if (DEBUG) Slog.d(TAG, " ime starting. vis:" + splitIsVisible + " " + dumpState()); |
| 162 | if (mAnimation != null || (mImeWasShown && imeShouldShow |
| 163 | && mTargetAdjusted != mAdjusted)) { |
| 164 | // We need to animate adjustment independently of the IME position, so |
| 165 | // start our own animation to drive adjustment. This happens when a |
| 166 | // different split's editor has gained focus while the IME is still visible. |
| 167 | startAsyncAnimation(); |
| 168 | } |
| 169 | if (splitIsVisible) { |
| 170 | // If split is hidden, we don't want to trigger any relayouts that would cause the |
| 171 | // divider to show again. |
| 172 | updateImeAdjustState(); |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | private void updateImeAdjustState() { |
Evan Rosky | e1a31c9 | 2020-06-18 16:46:47 -0700 | [diff] [blame] | 177 | if (mAdjusted != mTargetAdjusted) { |
| 178 | // Reposition the server's secondary split position so that it evaluates |
| 179 | // insets properly. |
| 180 | WindowContainerTransaction wct = new WindowContainerTransaction(); |
| 181 | final SplitDisplayLayout splitLayout = getLayout(); |
| 182 | if (mTargetAdjusted) { |
| 183 | splitLayout.updateAdjustedBounds(mShownTop, mHiddenTop, mShownTop); |
| 184 | wct.setBounds(mSplits.mSecondary.token, splitLayout.mAdjustedSecondary); |
| 185 | // "Freeze" the configuration size so that the app doesn't get a config |
| 186 | // or relaunch. This is required because normally nav-bar contributes |
| 187 | // to configuration bounds (via nondecorframe). |
| 188 | Rect adjustAppBounds = new Rect(mSplits.mSecondary.configuration |
| 189 | .windowConfiguration.getAppBounds()); |
| 190 | adjustAppBounds.offset(0, splitLayout.mAdjustedSecondary.top |
| 191 | - splitLayout.mSecondary.top); |
| 192 | wct.setAppBounds(mSplits.mSecondary.token, adjustAppBounds); |
| 193 | wct.setScreenSizeDp(mSplits.mSecondary.token, |
| 194 | mSplits.mSecondary.configuration.screenWidthDp, |
| 195 | mSplits.mSecondary.configuration.screenHeightDp); |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 196 | |
Evan Rosky | e1a31c9 | 2020-06-18 16:46:47 -0700 | [diff] [blame] | 197 | wct.setBounds(mSplits.mPrimary.token, splitLayout.mAdjustedPrimary); |
| 198 | adjustAppBounds = new Rect(mSplits.mPrimary.configuration |
| 199 | .windowConfiguration.getAppBounds()); |
| 200 | adjustAppBounds.offset(0, splitLayout.mAdjustedPrimary.top |
| 201 | - splitLayout.mPrimary.top); |
| 202 | wct.setAppBounds(mSplits.mPrimary.token, adjustAppBounds); |
| 203 | wct.setScreenSizeDp(mSplits.mPrimary.token, |
| 204 | mSplits.mPrimary.configuration.screenWidthDp, |
| 205 | mSplits.mPrimary.configuration.screenHeightDp); |
| 206 | } else { |
| 207 | wct.setBounds(mSplits.mSecondary.token, splitLayout.mSecondary); |
| 208 | wct.setAppBounds(mSplits.mSecondary.token, null); |
| 209 | wct.setScreenSizeDp(mSplits.mSecondary.token, |
| 210 | SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); |
| 211 | wct.setBounds(mSplits.mPrimary.token, splitLayout.mPrimary); |
| 212 | wct.setAppBounds(mSplits.mPrimary.token, null); |
| 213 | wct.setScreenSizeDp(mSplits.mPrimary.token, |
| 214 | SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); |
| 215 | } |
| 216 | |
| 217 | if (!mSplits.mDivider.getWmProxy().queueSyncTransactionIfWaiting(wct)) { |
| 218 | WindowOrganizer.applyTransaction(wct); |
| 219 | } |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 220 | } |
| 221 | |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 222 | // Update all the adjusted-for-ime states |
| 223 | if (!mPaused) { |
| 224 | final DividerView view = getView(); |
| 225 | if (view != null) { |
| 226 | view.setAdjustedForIme(mTargetShown, mTargetShown |
| 227 | ? DisplayImeController.ANIMATION_DURATION_SHOW_MS |
| 228 | : DisplayImeController.ANIMATION_DURATION_HIDE_MS); |
| 229 | } |
| 230 | } |
| 231 | mSplits.mDivider.setAdjustedForIme(mTargetShown && !mPaused); |
| 232 | } |
| 233 | |
| 234 | @Override |
| 235 | public void onImePositionChanged(int displayId, int imeTop, |
| 236 | SurfaceControl.Transaction t) { |
| 237 | if (mAnimation != null || !isDividerVisible() || mPaused) { |
| 238 | // Not synchronized with IME anymore, so return. |
| 239 | return; |
| 240 | } |
| 241 | final float fraction = ((float) imeTop - mHiddenTop) / (mShownTop - mHiddenTop); |
| 242 | final float progress = mTargetShown ? fraction : 1.f - fraction; |
| 243 | onProgress(progress, t); |
| 244 | } |
| 245 | |
| 246 | @Override |
| 247 | public void onImeEndPositioning(int displayId, boolean cancelled, |
| 248 | SurfaceControl.Transaction t) { |
| 249 | if (mAnimation != null || !isDividerVisible() || mPaused) { |
| 250 | // Not synchronized with IME anymore, so return. |
| 251 | return; |
| 252 | } |
| 253 | onEnd(cancelled, t); |
| 254 | } |
| 255 | |
| 256 | private void onProgress(float progress, SurfaceControl.Transaction t) { |
| 257 | final DividerView view = getView(); |
| 258 | if (mTargetAdjusted != mAdjusted && !mPaused) { |
| 259 | final SplitDisplayLayout splitLayout = getLayout(); |
| 260 | final float fraction = mTargetAdjusted ? progress : 1.f - progress; |
| 261 | mLastAdjustTop = (int) (fraction * mShownTop + (1.f - fraction) * mHiddenTop); |
| 262 | splitLayout.updateAdjustedBounds(mLastAdjustTop, mHiddenTop, mShownTop); |
| 263 | view.resizeSplitSurfaces(t, splitLayout.mAdjustedPrimary, |
| 264 | splitLayout.mAdjustedSecondary); |
| 265 | } |
| 266 | final float invProg = 1.f - progress; |
| 267 | view.setResizeDimLayer(t, true /* primary */, |
| 268 | mLastPrimaryDim * invProg + progress * mTargetPrimaryDim); |
| 269 | view.setResizeDimLayer(t, false /* primary */, |
| 270 | mLastSecondaryDim * invProg + progress * mTargetSecondaryDim); |
| 271 | } |
| 272 | |
Evan Rosky | c3a5090 | 2020-05-19 13:36:28 -0700 | [diff] [blame] | 273 | void setDimsHidden(SurfaceControl.Transaction t, boolean hidden) { |
| 274 | final DividerView view = getView(); |
| 275 | if (hidden) { |
| 276 | view.setResizeDimLayer(t, true /* primary */, 0.f /* alpha */); |
| 277 | view.setResizeDimLayer(t, false /* primary */, 0.f /* alpha */); |
| 278 | } else { |
| 279 | updateDimTargets(); |
| 280 | view.setResizeDimLayer(t, true /* primary */, mTargetPrimaryDim); |
| 281 | view.setResizeDimLayer(t, false /* primary */, mTargetSecondaryDim); |
| 282 | } |
| 283 | } |
| 284 | |
Evan Rosky | c48c939 | 2020-05-19 11:54:21 -0700 | [diff] [blame] | 285 | private void onEnd(boolean cancelled, SurfaceControl.Transaction t) { |
| 286 | if (!cancelled) { |
| 287 | onProgress(1.f, t); |
| 288 | mAdjusted = mTargetAdjusted; |
| 289 | mImeWasShown = mTargetShown; |
| 290 | mLastAdjustTop = mAdjusted ? mShownTop : mHiddenTop; |
| 291 | mLastPrimaryDim = mTargetPrimaryDim; |
| 292 | mLastSecondaryDim = mTargetSecondaryDim; |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | private void startAsyncAnimation() { |
| 297 | if (mAnimation != null) { |
| 298 | mAnimation.cancel(); |
| 299 | } |
| 300 | mAnimation = ValueAnimator.ofFloat(0.f, 1.f); |
| 301 | mAnimation.setDuration(DisplayImeController.ANIMATION_DURATION_SHOW_MS); |
| 302 | if (mTargetAdjusted != mAdjusted) { |
| 303 | final float fraction = |
| 304 | ((float) mLastAdjustTop - mHiddenTop) / (mShownTop - mHiddenTop); |
| 305 | final float progress = mTargetAdjusted ? fraction : 1.f - fraction; |
| 306 | mAnimation.setCurrentFraction(progress); |
| 307 | } |
| 308 | |
| 309 | mAnimation.addUpdateListener(animation -> { |
| 310 | SurfaceControl.Transaction t = mTransactionPool.acquire(); |
| 311 | float value = (float) animation.getAnimatedValue(); |
| 312 | onProgress(value, t); |
| 313 | t.apply(); |
| 314 | mTransactionPool.release(t); |
| 315 | }); |
| 316 | mAnimation.setInterpolator(DisplayImeController.INTERPOLATOR); |
| 317 | mAnimation.addListener(new AnimatorListenerAdapter() { |
| 318 | private boolean mCancel = false; |
| 319 | @Override |
| 320 | public void onAnimationCancel(Animator animation) { |
| 321 | mCancel = true; |
| 322 | } |
| 323 | @Override |
| 324 | public void onAnimationEnd(Animator animation) { |
| 325 | SurfaceControl.Transaction t = mTransactionPool.acquire(); |
| 326 | onEnd(mCancel, t); |
| 327 | t.apply(); |
| 328 | mTransactionPool.release(t); |
| 329 | mAnimation = null; |
| 330 | } |
| 331 | }); |
| 332 | mAnimation.start(); |
| 333 | } |
| 334 | |
| 335 | private String dumpState() { |
| 336 | return "top:" + mHiddenTop + "->" + mShownTop |
| 337 | + " adj:" + mAdjusted + "->" + mTargetAdjusted + "(" + mLastAdjustTop + ")" |
| 338 | + " shw:" + mImeWasShown + "->" + mTargetShown |
| 339 | + " dims:" + mLastPrimaryDim + "," + mLastSecondaryDim |
| 340 | + "->" + mTargetPrimaryDim + "," + mTargetSecondaryDim |
| 341 | + " shf:" + mSecondaryHasFocus + " desync:" + (mAnimation != null) |
| 342 | + " paus:" + mPaused + "[" + mPausedTargetAdjusted + "]"; |
| 343 | } |
| 344 | |
| 345 | /** Completely aborts/resets adjustment state */ |
| 346 | public void pause(int displayId) { |
| 347 | if (DEBUG) Slog.d(TAG, "ime pause posting " + dumpState()); |
| 348 | mHandler.post(() -> { |
| 349 | if (DEBUG) Slog.d(TAG, "ime pause run posted " + dumpState()); |
| 350 | if (mPaused) { |
| 351 | return; |
| 352 | } |
| 353 | mPaused = true; |
| 354 | mPausedTargetAdjusted = mTargetAdjusted; |
| 355 | mTargetAdjusted = false; |
| 356 | mTargetPrimaryDim = mTargetSecondaryDim = 0.f; |
| 357 | updateImeAdjustState(); |
| 358 | startAsyncAnimation(); |
| 359 | if (mAnimation != null) { |
| 360 | mAnimation.end(); |
| 361 | } |
| 362 | }); |
| 363 | } |
| 364 | |
| 365 | public void resume(int displayId) { |
| 366 | if (DEBUG) Slog.d(TAG, "ime resume posting " + dumpState()); |
| 367 | mHandler.post(() -> { |
| 368 | if (DEBUG) Slog.d(TAG, "ime resume run posted " + dumpState()); |
| 369 | if (!mPaused) { |
| 370 | return; |
| 371 | } |
| 372 | mPaused = false; |
| 373 | mTargetAdjusted = mPausedTargetAdjusted; |
| 374 | updateDimTargets(); |
| 375 | final DividerView view = getView(); |
| 376 | if ((mTargetAdjusted != mAdjusted) && !mSplits.mDivider.isMinimized() && view != null) { |
| 377 | // End unminimize animations since they conflict with adjustment animations. |
| 378 | view.finishAnimations(); |
| 379 | } |
| 380 | updateImeAdjustState(); |
| 381 | startAsyncAnimation(); |
| 382 | }); |
| 383 | } |
| 384 | } |