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.keyguard; 18 19 import static androidx.constraintlayout.widget.ConstraintSet.END; 20 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; 21 22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; 23 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; 24 25 import android.animation.Animator; 26 import android.animation.ValueAnimator; 27 import android.annotation.Nullable; 28 import android.content.res.Configuration; 29 import android.graphics.Rect; 30 import android.transition.ChangeBounds; 31 import android.transition.Transition; 32 import android.transition.TransitionListenerAdapter; 33 import android.transition.TransitionManager; 34 import android.transition.TransitionSet; 35 import android.transition.TransitionValues; 36 import android.util.Slog; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.widget.FrameLayout; 40 41 import androidx.annotation.NonNull; 42 import androidx.annotation.VisibleForTesting; 43 import androidx.constraintlayout.widget.ConstraintLayout; 44 import androidx.constraintlayout.widget.ConstraintSet; 45 import androidx.viewpager.widget.ViewPager; 46 47 import com.android.app.animation.Interpolators; 48 import com.android.internal.jank.InteractionJankMonitor; 49 import com.android.keyguard.KeyguardClockSwitch.ClockSize; 50 import com.android.keyguard.logging.KeyguardLogger; 51 import com.android.systemui.Dumpable; 52 import com.android.systemui.animation.ViewHierarchyAnimator; 53 import com.android.systemui.dump.DumpManager; 54 import com.android.systemui.keyguard.MigrateClocksToBlueprint; 55 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; 56 import com.android.systemui.plugins.clocks.ClockController; 57 import com.android.systemui.power.domain.interactor.PowerInteractor; 58 import com.android.systemui.power.shared.model.ScreenPowerState; 59 import com.android.systemui.res.R; 60 import com.android.systemui.statusbar.notification.AnimatableProperty; 61 import com.android.systemui.statusbar.notification.PropertyAnimator; 62 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 63 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 64 import com.android.systemui.statusbar.phone.DozeParameters; 65 import com.android.systemui.statusbar.phone.ScreenOffAnimationController; 66 import com.android.systemui.statusbar.policy.ConfigurationController; 67 import com.android.systemui.statusbar.policy.KeyguardStateController; 68 import com.android.systemui.util.ViewController; 69 70 import kotlin.coroutines.CoroutineContext; 71 import kotlin.coroutines.EmptyCoroutineContext; 72 73 import java.io.PrintWriter; 74 75 import javax.inject.Inject; 76 77 /** 78 * Injectable controller for {@link KeyguardStatusView}. 79 */ 80 public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> implements 81 Dumpable { 82 private static final boolean DEBUG = KeyguardConstants.DEBUG; 83 @VisibleForTesting static final String TAG = "KeyguardStatusViewController"; 84 private static final long STATUS_AREA_HEIGHT_ANIMATION_MILLIS = 133; 85 86 /** 87 * Duration to use for the animator when the keyguard status view alignment changes, and a 88 * custom clock animation is in use. 89 */ 90 private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000; 91 92 public static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = 93 new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 94 95 private final KeyguardSliceViewController mKeyguardSliceViewController; 96 private final KeyguardClockSwitchController mKeyguardClockSwitchController; 97 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 98 private final ConfigurationController mConfigurationController; 99 private final KeyguardVisibilityHelper mKeyguardVisibilityHelper; 100 private final InteractionJankMonitor mInteractionJankMonitor; 101 private final Rect mClipBounds = new Rect(); 102 private final KeyguardInteractor mKeyguardInteractor; 103 private final PowerInteractor mPowerInteractor; 104 private final DozeParameters mDozeParameters; 105 106 private View mStatusArea = null; 107 private ValueAnimator mStatusAreaHeightAnimator = null; 108 109 private Boolean mSplitShadeEnabled = false; 110 private Boolean mStatusViewCentered = true; 111 private DumpManager mDumpManager; 112 113 private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener = 114 new TransitionListenerAdapter() { 115 @Override 116 public void onTransitionCancel(Transition transition) { 117 mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); 118 } 119 120 @Override 121 public void onTransitionEnd(Transition transition) { 122 mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); 123 } 124 }; 125 126 private final View.OnLayoutChangeListener mStatusAreaLayoutChangeListener = 127 new View.OnLayoutChangeListener() { 128 @Override 129 public void onLayoutChange(View v, 130 int left, int top, int right, int bottom, 131 int oldLeft, int oldTop, int oldRight, int oldBottom 132 ) { 133 if (!mDozeParameters.getAlwaysOn()) { 134 return; 135 } 136 137 int oldHeight = oldBottom - oldTop; 138 int diff = v.getHeight() - oldHeight; 139 if (diff == 0) { 140 return; 141 } 142 143 int startValue = -1 * diff; 144 long duration = STATUS_AREA_HEIGHT_ANIMATION_MILLIS; 145 if (mStatusAreaHeightAnimator != null 146 && mStatusAreaHeightAnimator.isRunning()) { 147 duration += mStatusAreaHeightAnimator.getDuration() 148 - mStatusAreaHeightAnimator.getCurrentPlayTime(); 149 startValue += (int) mStatusAreaHeightAnimator.getAnimatedValue(); 150 mStatusAreaHeightAnimator.cancel(); 151 mStatusAreaHeightAnimator = null; 152 } 153 154 mStatusAreaHeightAnimator = ValueAnimator.ofInt(startValue, 0); 155 mStatusAreaHeightAnimator.setDuration(duration); 156 final View nic = mKeyguardClockSwitchController.getAodNotifIconContainer(); 157 if (nic != null) { 158 mStatusAreaHeightAnimator.addUpdateListener(anim -> { 159 nic.setTranslationY((int) anim.getAnimatedValue()); 160 }); 161 } 162 mStatusAreaHeightAnimator.start(); 163 } 164 }; 165 166 @Inject KeyguardStatusViewController( KeyguardStatusView keyguardStatusView, KeyguardSliceViewController keyguardSliceViewController, KeyguardClockSwitchController keyguardClockSwitchController, KeyguardStateController keyguardStateController, KeyguardUpdateMonitor keyguardUpdateMonitor, ConfigurationController configurationController, DozeParameters dozeParameters, ScreenOffAnimationController screenOffAnimationController, KeyguardLogger logger, InteractionJankMonitor interactionJankMonitor, KeyguardInteractor keyguardInteractor, DumpManager dumpManager, PowerInteractor powerInteractor)167 public KeyguardStatusViewController( 168 KeyguardStatusView keyguardStatusView, 169 KeyguardSliceViewController keyguardSliceViewController, 170 KeyguardClockSwitchController keyguardClockSwitchController, 171 KeyguardStateController keyguardStateController, 172 KeyguardUpdateMonitor keyguardUpdateMonitor, 173 ConfigurationController configurationController, 174 DozeParameters dozeParameters, 175 ScreenOffAnimationController screenOffAnimationController, 176 KeyguardLogger logger, 177 InteractionJankMonitor interactionJankMonitor, 178 KeyguardInteractor keyguardInteractor, 179 DumpManager dumpManager, 180 PowerInteractor powerInteractor) { 181 super(keyguardStatusView); 182 mKeyguardSliceViewController = keyguardSliceViewController; 183 mKeyguardClockSwitchController = keyguardClockSwitchController; 184 mKeyguardUpdateMonitor = keyguardUpdateMonitor; 185 mConfigurationController = configurationController; 186 mDozeParameters = dozeParameters; 187 mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController, 188 dozeParameters, screenOffAnimationController, /* animateYPos= */ true, 189 logger.getBuffer()); 190 mInteractionJankMonitor = interactionJankMonitor; 191 mDumpManager = dumpManager; 192 mKeyguardInteractor = keyguardInteractor; 193 mPowerInteractor = powerInteractor; 194 } 195 196 @Override onInit()197 public void onInit() { 198 mKeyguardClockSwitchController.init(); 199 final View mediaHostContainer = mView.findViewById(R.id.status_view_media_container); 200 if (mediaHostContainer != null) { 201 mKeyguardClockSwitchController.getView().addOnLayoutChangeListener( 202 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 203 if (!mSplitShadeEnabled 204 || mKeyguardClockSwitchController.getView().getSplitShadeCentered() 205 // Note: isKeyguardVisible() returns false after Launcher -> AOD. 206 || !mKeyguardUpdateMonitor.isKeyguardVisible()) { 207 return; 208 } 209 210 int oldHeight = oldBottom - oldTop; 211 if (v.getHeight() == oldHeight) return; 212 213 if (mediaHostContainer.getVisibility() != View.VISIBLE 214 // If the media is appearing, also don't do the transition. 215 || mediaHostContainer.getHeight() == 0) { 216 return; 217 } 218 219 ViewHierarchyAnimator.Companion.animateNextUpdate(mediaHostContainer, 220 Interpolators.STANDARD, /* duration= */ 500L, 221 /* animateChildren= */ false); 222 }); 223 } 224 225 mDumpManager.registerDumpable(getInstanceName(), this); 226 if (MigrateClocksToBlueprint.isEnabled()) { 227 startCoroutines(EmptyCoroutineContext.INSTANCE); 228 mView.setVisibility(View.GONE); 229 } 230 } 231 startCoroutines(CoroutineContext context)232 void startCoroutines(CoroutineContext context) { 233 collectFlow(mView, mKeyguardInteractor.getDozeTimeTick(), 234 (Long millis) -> { 235 dozeTimeTick(); 236 }, context); 237 238 collectFlow(mView, mPowerInteractor.getScreenPowerState(), 239 (ScreenPowerState powerState) -> { 240 if (powerState == ScreenPowerState.SCREEN_TURNING_ON) { 241 dozeTimeTick(); 242 } 243 }, context); 244 } 245 getView()246 public KeyguardStatusView getView() { 247 return mView; 248 } 249 250 @Override onViewAttached()251 protected void onViewAttached() { 252 mStatusArea = mView.findViewById(R.id.keyguard_status_area); 253 if (MigrateClocksToBlueprint.isEnabled()) { 254 return; 255 } 256 257 mStatusArea.addOnLayoutChangeListener(mStatusAreaLayoutChangeListener); 258 mKeyguardUpdateMonitor.registerCallback(mInfoCallback); 259 mConfigurationController.addCallback(mConfigurationListener); 260 } 261 262 @Override onViewDetached()263 protected void onViewDetached() { 264 if (MigrateClocksToBlueprint.isEnabled()) { 265 return; 266 } 267 268 mStatusArea.removeOnLayoutChangeListener(mStatusAreaLayoutChangeListener); 269 mKeyguardUpdateMonitor.removeCallback(mInfoCallback); 270 mConfigurationController.removeCallback(mConfigurationListener); 271 } 272 273 /** Sets the StatusView as shown on an external display. */ setDisplayedOnSecondaryDisplay()274 public void setDisplayedOnSecondaryDisplay() { 275 mKeyguardClockSwitchController.setShownOnSecondaryDisplay(true); 276 } 277 278 /** 279 * Called in notificationPanelViewController to avoid leak 280 */ onDestroy()281 public void onDestroy() { 282 mDumpManager.unregisterDumpable(getInstanceName()); 283 } 284 285 /** 286 * Updates views on doze time tick. 287 */ dozeTimeTick()288 public void dozeTimeTick() { 289 refreshTime(); 290 mKeyguardSliceViewController.refresh(); 291 } 292 293 /** 294 * Set which clock should be displayed on the keyguard. The other one will be automatically 295 * hidden. 296 */ displayClock(@lockSize int clockSize, boolean animate)297 public void displayClock(@ClockSize int clockSize, boolean animate) { 298 mKeyguardClockSwitchController.displayClock(clockSize, animate); 299 } 300 301 /** 302 * Performs fold to aod animation of the clocks (changes font weight from bold to thin). 303 * This animation is played when AOD is enabled and foldable device is fully folded, it is 304 * displayed on the outer screen 305 * @param foldFraction current fraction of fold animation complete 306 */ animateFoldToAod(float foldFraction)307 public void animateFoldToAod(float foldFraction) { 308 mKeyguardClockSwitchController.animateFoldToAod(foldFraction); 309 } 310 311 /** 312 * Sets a translationY on the views on the keyguard, except on the media view. 313 */ setTranslationY(float translationY, boolean excludeMedia)314 public void setTranslationY(float translationY, boolean excludeMedia) { 315 mView.setChildrenTranslationY(translationY, excludeMedia); 316 } 317 318 /** 319 * Set keyguard status view alpha. 320 */ setAlpha(float alpha)321 public void setAlpha(float alpha) { 322 if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) { 323 mView.setAlpha(alpha); 324 } 325 } 326 327 /** 328 * Update the pivot position based on the parent view 329 */ updatePivot(float parentWidth, float parentHeight)330 public void updatePivot(float parentWidth, float parentHeight) { 331 mView.setPivotX(parentWidth / 2f); 332 mView.setPivotY(mKeyguardClockSwitchController.getClockHeight() / 2f); 333 } 334 335 /** 336 * Get the height of the keyguard status view without the notification icon area, as that's 337 * only visible on AOD. 338 * 339 * We internally animate height changes to the status area to prevent discontinuities in the 340 * doze animation introduced by the height suddenly changing due to smartpace. 341 */ getLockscreenHeight()342 public int getLockscreenHeight() { 343 int heightAnimValue = mStatusAreaHeightAnimator == null ? 0 : 344 (int) mStatusAreaHeightAnimator.getAnimatedValue(); 345 return mView.getHeight() + heightAnimValue 346 - mKeyguardClockSwitchController.getNotificationIconAreaHeight(); 347 } 348 349 /** 350 * Get y-bottom position of the currently visible clock. 351 */ getClockBottom(int statusBarHeaderHeight)352 public int getClockBottom(int statusBarHeaderHeight) { 353 return mKeyguardClockSwitchController.getClockBottom(statusBarHeaderHeight); 354 } 355 356 /** 357 * @return true if the currently displayed clock is top aligned (as opposed to center aligned) 358 */ isClockTopAligned()359 public boolean isClockTopAligned() { 360 return mKeyguardClockSwitchController.isClockTopAligned(); 361 } 362 363 /** 364 * Pass top margin from ClockPositionAlgorithm in NotificationPanelViewController 365 * Use for clock view in LS to compensate for top margin to align to the screen 366 * Regardless of translation from AOD and unlock gestures 367 */ setLockscreenClockY(int clockY)368 public void setLockscreenClockY(int clockY) { 369 mKeyguardClockSwitchController.setLockscreenClockY(clockY); 370 } 371 372 /** 373 * Set whether the view accessibility importance mode. 374 */ setStatusAccessibilityImportance(int mode)375 public void setStatusAccessibilityImportance(int mode) { 376 mView.setImportantForAccessibility(mode); 377 } 378 379 @VisibleForTesting setProperty(AnimatableProperty property, float value, boolean animate)380 void setProperty(AnimatableProperty property, float value, boolean animate) { 381 PropertyAnimator.setProperty(mView, property, value, CLOCK_ANIMATION_PROPERTIES, animate); 382 } 383 384 /** 385 * Update position of the view with an optional animation 386 */ updatePosition(int x, int y, float scale, boolean animate)387 public void updatePosition(int x, int y, float scale, boolean animate) { 388 setProperty(AnimatableProperty.Y, y, animate); 389 390 ClockController clock = mKeyguardClockSwitchController.getClock(); 391 if (clock != null && clock.getConfig().getUseAlternateSmartspaceAODTransition()) { 392 // If requested, scale the entire view instead of just the clock view 393 mKeyguardClockSwitchController.updatePosition(x, 1f /* scale */, 394 CLOCK_ANIMATION_PROPERTIES, animate); 395 setProperty(AnimatableProperty.SCALE_X, scale, animate); 396 setProperty(AnimatableProperty.SCALE_Y, scale, animate); 397 } else { 398 mKeyguardClockSwitchController.updatePosition(x, scale, 399 CLOCK_ANIMATION_PROPERTIES, animate); 400 setProperty(AnimatableProperty.SCALE_X, 1f, animate); 401 setProperty(AnimatableProperty.SCALE_Y, 1f, animate); 402 } 403 } 404 405 /** 406 * Set the visibility of the keyguard status view based on some new state. 407 */ setKeyguardStatusViewVisibility( int statusBarState, boolean keyguardFadingAway, boolean goingToFullShade, int oldStatusBarState)408 public void setKeyguardStatusViewVisibility( 409 int statusBarState, 410 boolean keyguardFadingAway, 411 boolean goingToFullShade, 412 int oldStatusBarState) { 413 mKeyguardVisibilityHelper.setViewVisibility( 414 statusBarState, keyguardFadingAway, goingToFullShade, oldStatusBarState); 415 } 416 refreshTime()417 private void refreshTime() { 418 mKeyguardClockSwitchController.refresh(); 419 } 420 421 private final ConfigurationController.ConfigurationListener mConfigurationListener = 422 new ConfigurationController.ConfigurationListener() { 423 @Override 424 public void onLocaleListChanged() { 425 refreshTime(); 426 mKeyguardClockSwitchController.onLocaleListChanged(); 427 } 428 429 @Override 430 public void onConfigChanged(Configuration newConfig) { 431 mKeyguardClockSwitchController.onConfigChanged(); 432 } 433 }; 434 435 private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { 436 @Override 437 public void onTimeChanged() { 438 Slog.v(TAG, "onTimeChanged"); 439 refreshTime(); 440 } 441 442 @Override 443 public void onKeyguardVisibilityChanged(boolean visible) { 444 if (visible) { 445 if (DEBUG) Slog.v(TAG, "refresh statusview visible:true"); 446 refreshTime(); 447 } 448 } 449 }; 450 451 /** 452 * Rect that specifies how KSV should be clipped, on its parent's coordinates. 453 */ setClipBounds(Rect clipBounds)454 public void setClipBounds(Rect clipBounds) { 455 if (clipBounds != null) { 456 mClipBounds.set(clipBounds.left, (int) (clipBounds.top - mView.getY()), 457 clipBounds.right, (int) (clipBounds.bottom - mView.getY())); 458 mView.setClipBounds(mClipBounds); 459 } else { 460 mView.setClipBounds(null); 461 } 462 } 463 464 /** 465 * Returns true if the large clock will block the notification shelf in AOD 466 */ isLargeClockBlockingNotificationShelf()467 public boolean isLargeClockBlockingNotificationShelf() { 468 ClockController clock = mKeyguardClockSwitchController.getClock(); 469 return clock != null && clock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay(); 470 } 471 472 /** 473 * Set if the split shade is enabled 474 */ setSplitShadeEnabled(boolean enabled)475 public void setSplitShadeEnabled(boolean enabled) { 476 mKeyguardClockSwitchController.setSplitShadeEnabled(enabled); 477 mSplitShadeEnabled = enabled; 478 } 479 480 /** 481 * Updates the alignment of the KeyguardStatusView and animates the transition if requested. 482 */ updateAlignment( ConstraintLayout layout, boolean splitShadeEnabled, boolean shouldBeCentered, boolean animate)483 public void updateAlignment( 484 ConstraintLayout layout, 485 boolean splitShadeEnabled, 486 boolean shouldBeCentered, 487 boolean animate) { 488 if (MigrateClocksToBlueprint.isEnabled()) { 489 mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); 490 } else { 491 mKeyguardClockSwitchController.setSplitShadeCentered( 492 splitShadeEnabled && shouldBeCentered); 493 } 494 if (mStatusViewCentered == shouldBeCentered) { 495 return; 496 } 497 498 mStatusViewCentered = shouldBeCentered; 499 if (layout == null) { 500 return; 501 } 502 503 ConstraintSet constraintSet = new ConstraintSet(); 504 constraintSet.clone(layout); 505 int guideline; 506 if (MigrateClocksToBlueprint.isEnabled()) { 507 guideline = R.id.split_shade_guideline; 508 } else { 509 guideline = R.id.qs_edge_guideline; 510 } 511 512 int statusConstraint = shouldBeCentered ? PARENT_ID : guideline; 513 constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); 514 if (!animate) { 515 constraintSet.applyTo(layout); 516 return; 517 } 518 519 mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION); 520 /* This transition blocks any layout changes while running. For that reason 521 * special logic with setting visibility was added to {@link BcSmartspaceView#setDozing} 522 * for split shade to avoid jump of the media object. */ 523 ChangeBounds transition = new ChangeBounds(); 524 if (splitShadeEnabled) { 525 // Excluding media from the transition on split-shade, as it doesn't transition 526 // horizontally properly. 527 transition.excludeTarget(R.id.status_view_media_container, true); 528 529 // Exclude smartspace viewpager and its children from the transition. 530 // - Each step of the transition causes the ViewPager to invoke resize, 531 // which invokes scrolling to the recalculated position. The scrolling 532 // actions are congested, resulting in kinky translation, and 533 // delay in settling to the final position. (http://b/281620564#comment1) 534 // - Also, the scrolling is unnecessary in the transition. We just want 535 // the viewpager to stay on the same page. 536 // - Exclude by Class type instead of resource id, since the resource id 537 // isn't available for all devices, and probably better to exclude all 538 // ViewPagers any way. 539 transition.excludeTarget(ViewPager.class, true); 540 transition.excludeChildren(ViewPager.class, true); 541 } 542 543 transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 544 transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 545 546 ClockController clock = mKeyguardClockSwitchController.getClock(); 547 boolean customClockAnimation = clock != null 548 && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation(); 549 // When migrateClocksToBlueprint is on, customized clock animation is conducted in 550 // KeyguardClockViewBinder 551 if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) { 552 // Find the clock, so we can exclude it from this transition. 553 FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large); 554 555 // The clock container can sometimes be null. If it is, just fall back to the 556 // old animation rather than setting up the custom animations. 557 if (clockContainerView == null || clockContainerView.getChildCount() == 0) { 558 transition.addListener(mKeyguardStatusAlignmentTransitionListener); 559 TransitionManager.beginDelayedTransition(layout, transition); 560 } else { 561 View clockView = clockContainerView.getChildAt(0); 562 563 TransitionSet set = new TransitionSet(); 564 set.addTransition(transition); 565 566 SplitShadeTransitionAdapter adapter = 567 new SplitShadeTransitionAdapter(mKeyguardClockSwitchController); 568 569 // Use linear here, so the actual clock can pick its own interpolator. 570 adapter.setInterpolator(Interpolators.LINEAR); 571 adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION); 572 adapter.addTarget(clockView); 573 set.addTransition(adapter); 574 575 if (splitShadeEnabled) { 576 // Exclude smartspace viewpager and its children from the transition set. 577 // - This is necessary in addition to excluding them from the 578 // ChangeBounds child transition. 579 // - Without this, the viewpager is scrolled to the new position 580 // (corresponding to its end size) before the size change is realized. 581 // Note that the size change is realized at the end of the ChangeBounds 582 // transition. With the "prescrolling", the viewpager ends up in a weird 583 // position, then recovers smoothly during the transition, and ends at 584 // the position for the current page. 585 // - Exclude by Class type instead of resource id, since the resource id 586 // isn't available for all devices, and probably better to exclude all 587 // ViewPagers any way. 588 set.excludeTarget(ViewPager.class, true); 589 set.excludeChildren(ViewPager.class, true); 590 } 591 592 set.addListener(mKeyguardStatusAlignmentTransitionListener); 593 TransitionManager.beginDelayedTransition(layout, set); 594 } 595 } else { 596 transition.addListener(mKeyguardStatusAlignmentTransitionListener); 597 TransitionManager.beginDelayedTransition(layout, transition); 598 } 599 600 constraintSet.applyTo(layout); 601 } 602 getClockController()603 public ClockController getClockController() { 604 return mKeyguardClockSwitchController.getClock(); 605 } 606 607 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)608 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 609 mView.dump(pw, args); 610 } 611 getInstanceName()612 String getInstanceName() { 613 return TAG + "#" + hashCode(); 614 } 615 616 @VisibleForTesting 617 static class SplitShadeTransitionAdapter extends Transition { 618 private static final String PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft"; 619 private static final String PROP_BOUNDS_RIGHT = "splitShadeTransitionAdapter:boundsRight"; 620 private static final String PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow"; 621 private static final String[] TRANSITION_PROPERTIES = { 622 PROP_BOUNDS_LEFT, PROP_BOUNDS_RIGHT, PROP_X_IN_WINDOW}; 623 624 private final KeyguardClockSwitchController mController; 625 626 @VisibleForTesting SplitShadeTransitionAdapter(KeyguardClockSwitchController controller)627 SplitShadeTransitionAdapter(KeyguardClockSwitchController controller) { 628 mController = controller; 629 } 630 captureValues(TransitionValues transitionValues)631 private void captureValues(TransitionValues transitionValues) { 632 transitionValues.values.put(PROP_BOUNDS_LEFT, transitionValues.view.getLeft()); 633 transitionValues.values.put(PROP_BOUNDS_RIGHT, transitionValues.view.getRight()); 634 int[] locationInWindowTmp = new int[2]; 635 transitionValues.view.getLocationInWindow(locationInWindowTmp); 636 transitionValues.values.put(PROP_X_IN_WINDOW, locationInWindowTmp[0]); 637 } 638 639 @Override captureEndValues(TransitionValues transitionValues)640 public void captureEndValues(TransitionValues transitionValues) { 641 captureValues(transitionValues); 642 } 643 644 @Override captureStartValues(TransitionValues transitionValues)645 public void captureStartValues(TransitionValues transitionValues) { 646 captureValues(transitionValues); 647 } 648 649 @Nullable 650 @Override createAnimator(@onNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)651 public Animator createAnimator(@NonNull ViewGroup sceneRoot, 652 @Nullable TransitionValues startValues, 653 @Nullable TransitionValues endValues) { 654 if (startValues == null || endValues == null) { 655 return null; 656 } 657 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 658 659 int fromLeft = (int) startValues.values.get(PROP_BOUNDS_LEFT); 660 int fromWindowX = (int) startValues.values.get(PROP_X_IN_WINDOW); 661 int toWindowX = (int) endValues.values.get(PROP_X_IN_WINDOW); 662 // Using windowX, to determine direction, instead of left, as in RTL the difference of 663 // toLeft - fromLeft is always positive, even when moving left. 664 int direction = toWindowX - fromWindowX > 0 ? 1 : -1; 665 666 anim.addUpdateListener(animation -> { 667 ClockController clock = mController.getClock(); 668 if (clock == null) { 669 return; 670 } 671 672 clock.getLargeClock().getAnimations() 673 .onPositionUpdated(fromLeft, direction, animation.getAnimatedFraction()); 674 }); 675 676 return anim; 677 } 678 679 @Override getTransitionProperties()680 public String[] getTransitionProperties() { 681 return TRANSITION_PROPERTIES; 682 } 683 } 684 } 685