1 package com.android.keyguard; 2 3 import android.animation.Animator; 4 import android.animation.AnimatorListenerAdapter; 5 import android.animation.AnimatorSet; 6 import android.animation.ObjectAnimator; 7 import android.content.Context; 8 import android.graphics.Rect; 9 import android.util.AttributeSet; 10 import android.view.View; 11 import android.view.ViewGroup; 12 import android.widget.FrameLayout; 13 import android.widget.RelativeLayout; 14 15 import androidx.annotation.IntDef; 16 import androidx.annotation.VisibleForTesting; 17 18 import com.android.keyguard.dagger.KeyguardStatusViewScope; 19 import com.android.systemui.R; 20 import com.android.systemui.animation.Interpolators; 21 import com.android.systemui.plugins.ClockController; 22 import com.android.systemui.plugins.log.LogBuffer; 23 import com.android.systemui.plugins.log.LogLevel; 24 25 import java.io.PrintWriter; 26 import java.lang.annotation.Retention; 27 import java.lang.annotation.RetentionPolicy; 28 29 import kotlin.Unit; 30 31 /** 32 * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. 33 */ 34 @KeyguardStatusViewScope 35 public class KeyguardClockSwitch extends RelativeLayout { 36 37 private static final String TAG = "KeyguardClockSwitch"; 38 39 private static final long CLOCK_OUT_MILLIS = 150; 40 private static final long CLOCK_IN_MILLIS = 200; 41 private static final long STATUS_AREA_MOVE_MILLIS = 350; 42 43 @IntDef({LARGE, SMALL}) 44 @Retention(RetentionPolicy.SOURCE) 45 public @interface ClockSize { } 46 47 public static final int LARGE = 0; 48 public static final int SMALL = 1; 49 50 /** Returns a region for the large clock to position itself, based on the given parent. */ getLargeClockRegion(ViewGroup parent)51 public static Rect getLargeClockRegion(ViewGroup parent) { 52 int largeClockTopMargin = parent.getResources() 53 .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin); 54 int targetHeight = parent.getResources() 55 .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2; 56 int top = parent.getHeight() / 2 - targetHeight / 2 57 + largeClockTopMargin / 2; 58 return new Rect( 59 parent.getLeft(), 60 top, 61 parent.getRight(), 62 top + targetHeight); 63 } 64 65 /** 66 * Frame for small/large clocks 67 */ 68 private FrameLayout mSmallClockFrame; 69 private FrameLayout mLargeClockFrame; 70 private ClockController mClock; 71 72 private View mStatusArea; 73 private int mSmartspaceTopOffset; 74 75 /** 76 * Maintain state so that a newly connected plugin can be initialized. 77 */ 78 private float mDarkAmount; 79 80 /** 81 * Indicates which clock is currently displayed - should be one of {@link ClockSize}. 82 * Use null to signify it is uninitialized. 83 */ 84 @ClockSize private Integer mDisplayedClockSize = null; 85 86 @VisibleForTesting AnimatorSet mClockInAnim = null; 87 @VisibleForTesting AnimatorSet mClockOutAnim = null; 88 private ObjectAnimator mStatusAreaAnim = null; 89 90 private int mClockSwitchYAmount; 91 @VisibleForTesting boolean mChildrenAreLaidOut = false; 92 @VisibleForTesting boolean mAnimateOnLayout = true; 93 private LogBuffer mLogBuffer = null; 94 KeyguardClockSwitch(Context context, AttributeSet attrs)95 public KeyguardClockSwitch(Context context, AttributeSet attrs) { 96 super(context, attrs); 97 } 98 99 /** 100 * Apply dp changes on font/scale change 101 */ onDensityOrFontScaleChanged()102 public void onDensityOrFontScaleChanged() { 103 mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( 104 R.dimen.keyguard_clock_switch_y_shift); 105 mSmartspaceTopOffset = mContext.getResources().getDimensionPixelSize( 106 R.dimen.keyguard_smartspace_top_offset); 107 } 108 109 @Override onFinishInflate()110 protected void onFinishInflate() { 111 super.onFinishInflate(); 112 113 mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); 114 mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); 115 mStatusArea = findViewById(R.id.keyguard_status_area); 116 117 onDensityOrFontScaleChanged(); 118 } 119 setLogBuffer(LogBuffer logBuffer)120 public void setLogBuffer(LogBuffer logBuffer) { 121 mLogBuffer = logBuffer; 122 } 123 getLogBuffer()124 public LogBuffer getLogBuffer() { 125 return mLogBuffer; 126 } 127 setClock(ClockController clock, int statusBarState)128 void setClock(ClockController clock, int statusBarState) { 129 mClock = clock; 130 131 // Disconnect from existing plugin. 132 mSmallClockFrame.removeAllViews(); 133 mLargeClockFrame.removeAllViews(); 134 135 if (clock == null) { 136 if (mLogBuffer != null) { 137 mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown"); 138 } 139 return; 140 } 141 142 // Attach small and big clock views to hierarchy. 143 if (mLogBuffer != null) { 144 mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch"); 145 } 146 mSmallClockFrame.addView(clock.getSmallClock().getView()); 147 mLargeClockFrame.addView(clock.getLargeClock().getView()); 148 updateClockTargetRegions(); 149 } 150 updateClockTargetRegions()151 void updateClockTargetRegions() { 152 if (mClock != null) { 153 if (mSmallClockFrame.isLaidOut()) { 154 int targetHeight = getResources() 155 .getDimensionPixelSize(R.dimen.small_clock_text_size); 156 mClock.getSmallClock().getEvents().onTargetRegionChanged(new Rect( 157 mSmallClockFrame.getLeft(), 158 mSmallClockFrame.getTop(), 159 mSmallClockFrame.getRight(), 160 mSmallClockFrame.getTop() + targetHeight)); 161 } 162 163 if (mLargeClockFrame.isLaidOut()) { 164 mClock.getLargeClock().getEvents().onTargetRegionChanged( 165 getLargeClockRegion(mLargeClockFrame)); 166 } 167 } 168 } 169 updateClockViews(boolean useLargeClock, boolean animate)170 private void updateClockViews(boolean useLargeClock, boolean animate) { 171 if (mLogBuffer != null) { 172 mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> { 173 msg.setBool1(useLargeClock); 174 msg.setBool2(animate); 175 msg.setBool3(mChildrenAreLaidOut); 176 return Unit.INSTANCE; 177 }, (msg) -> "updateClockViews" 178 + "; useLargeClock=" + msg.getBool1() 179 + "; animate=" + msg.getBool2() 180 + "; mChildrenAreLaidOut=" + msg.getBool3()); 181 } 182 183 if (mClockInAnim != null) mClockInAnim.cancel(); 184 if (mClockOutAnim != null) mClockOutAnim.cancel(); 185 if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); 186 187 mClockInAnim = null; 188 mClockOutAnim = null; 189 mStatusAreaAnim = null; 190 191 View in, out; 192 int direction = 1; 193 float statusAreaYTranslation; 194 if (useLargeClock) { 195 out = mSmallClockFrame; 196 in = mLargeClockFrame; 197 if (indexOfChild(in) == -1) addView(in, 0); 198 direction = -1; 199 statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop() 200 + mSmartspaceTopOffset; 201 } else { 202 in = mSmallClockFrame; 203 out = mLargeClockFrame; 204 statusAreaYTranslation = 0f; 205 206 // Must remove in order for notifications to appear in the proper place 207 removeView(out); 208 } 209 210 if (!animate) { 211 out.setAlpha(0f); 212 in.setAlpha(1f); 213 in.setVisibility(VISIBLE); 214 mStatusArea.setTranslationY(statusAreaYTranslation); 215 return; 216 } 217 218 mClockOutAnim = new AnimatorSet(); 219 mClockOutAnim.setDuration(CLOCK_OUT_MILLIS); 220 mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 221 mClockOutAnim.playTogether( 222 ObjectAnimator.ofFloat(out, View.ALPHA, 0f), 223 ObjectAnimator.ofFloat(out, View.TRANSLATION_Y, 0, 224 direction * -mClockSwitchYAmount)); 225 mClockOutAnim.addListener(new AnimatorListenerAdapter() { 226 public void onAnimationEnd(Animator animation) { 227 mClockOutAnim = null; 228 } 229 }); 230 231 in.setAlpha(0); 232 in.setVisibility(View.VISIBLE); 233 mClockInAnim = new AnimatorSet(); 234 mClockInAnim.setDuration(CLOCK_IN_MILLIS); 235 mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 236 mClockInAnim.playTogether(ObjectAnimator.ofFloat(in, View.ALPHA, 1f), 237 ObjectAnimator.ofFloat(in, View.TRANSLATION_Y, direction * mClockSwitchYAmount, 0)); 238 mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2); 239 mClockInAnim.addListener(new AnimatorListenerAdapter() { 240 public void onAnimationEnd(Animator animation) { 241 mClockInAnim = null; 242 } 243 }); 244 245 mClockInAnim.start(); 246 mClockOutAnim.start(); 247 248 mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y, 249 statusAreaYTranslation); 250 mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS); 251 mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 252 mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { 253 public void onAnimationEnd(Animator animation) { 254 mStatusAreaAnim = null; 255 } 256 }); 257 mStatusAreaAnim.start(); 258 } 259 260 /** 261 * Display the desired clock and hide the other one 262 * 263 * @return true if desired clock appeared and false if it was already visible 264 */ switchToClock(@lockSize int clockSize, boolean animate)265 boolean switchToClock(@ClockSize int clockSize, boolean animate) { 266 if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) { 267 return false; 268 } 269 270 // let's make sure clock is changed only after all views were laid out so we can 271 // translate them properly 272 if (mChildrenAreLaidOut) { 273 updateClockViews(clockSize == LARGE, animate); 274 } 275 276 mDisplayedClockSize = clockSize; 277 return true; 278 } 279 280 @Override onLayout(boolean changed, int l, int t, int r, int b)281 protected void onLayout(boolean changed, int l, int t, int r, int b) { 282 super.onLayout(changed, l, t, r, b); 283 284 if (changed) { 285 post(() -> updateClockTargetRegions()); 286 } 287 288 if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { 289 post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); 290 } 291 292 mChildrenAreLaidOut = true; 293 } 294 dump(PrintWriter pw, String[] args)295 public void dump(PrintWriter pw, String[] args) { 296 pw.println("KeyguardClockSwitch:"); 297 pw.println(" mSmallClockFrame: " + mSmallClockFrame); 298 pw.println(" mSmallClockFrame.alpha: " + mSmallClockFrame.getAlpha()); 299 pw.println(" mLargeClockFrame: " + mLargeClockFrame); 300 pw.println(" mLargeClockFrame.alpha: " + mLargeClockFrame.getAlpha()); 301 pw.println(" mStatusArea: " + mStatusArea); 302 pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); 303 } 304 } 305