blob: 47c8c0ad8a4e7760b5b27b26bbb0ce8379d7eae3 [file] [log] [blame]
Evan Roskyc48c9392020-05-19 11:54:21 -07001/*
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
17package com.android.systemui.stackdivider;
18
19import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
20import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
21
22import android.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.ValueAnimator;
25import android.graphics.Rect;
26import android.os.Handler;
27import android.util.Slog;
28import android.view.SurfaceControl;
29import android.window.TaskOrganizer;
30import android.window.WindowContainerToken;
31import android.window.WindowContainerTransaction;
Evan Roskye1a31c92020-06-18 16:46:47 -070032import android.window.WindowOrganizer;
Evan Roskyc48c9392020-05-19 11:54:21 -070033
34import androidx.annotation.Nullable;
35
36import com.android.systemui.TransactionPool;
37import com.android.systemui.wm.DisplayImeController;
38
39class 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 Rosky4dae54d2020-06-18 18:36:59 -0700130 mHiddenTop = hiddenTop;
131 mShownTop = shownTop;
132 mTargetShown = imeShouldShow;
Evan Roskyc48c9392020-05-19 11:54:21 -0700133 if (!isDividerVisible()) {
134 return;
135 }
136 final boolean splitIsVisible = !getView().isHidden();
137 mSecondaryHasFocus = getSecondaryHasFocus(displayId);
138 final boolean targetAdjusted = splitIsVisible && imeShouldShow && mSecondaryHasFocus
Evan Rosky4dae54d2020-06-18 18:36:59 -0700139 && !getLayout().mDisplayLayout.isLandscape() && !mSplits.mDivider.isMinimized();
Evan Roskyc48c9392020-05-19 11:54:21 -0700140 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 Roskye1a31c92020-06-18 16:46:47 -0700177 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 Roskyc48c9392020-05-19 11:54:21 -0700196
Evan Roskye1a31c92020-06-18 16:46:47 -0700197 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 Roskyc48c9392020-05-19 11:54:21 -0700220 }
221
Evan Roskyc48c9392020-05-19 11:54:21 -0700222 // 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 Roskyc3a50902020-05-19 13:36:28 -0700273 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 Roskyc48c9392020-05-19 11:54:21 -0700285 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}