1 /* 2 * Copyright (C) 2014 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 package com.android.deskclock.alarms; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.AnimatorSet; 21 import android.animation.ObjectAnimator; 22 import android.animation.PropertyValuesHolder; 23 import android.animation.TimeInterpolator; 24 import android.animation.ValueAnimator; 25 import android.content.BroadcastReceiver; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.ServiceConnection; 31 import android.content.pm.ActivityInfo; 32 import android.graphics.Color; 33 import android.graphics.Rect; 34 import android.graphics.drawable.ColorDrawable; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.IBinder; 38 import android.preference.PreferenceManager; 39 import android.support.annotation.NonNull; 40 import android.support.v4.graphics.ColorUtils; 41 import android.support.v4.view.animation.PathInterpolatorCompat; 42 import android.support.v7.app.AppCompatActivity; 43 import android.view.KeyEvent; 44 import android.view.MotionEvent; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.view.WindowManager; 48 import android.view.accessibility.AccessibilityManager; 49 import android.widget.ImageView; 50 import android.widget.TextClock; 51 import android.widget.TextView; 52 53 import com.android.deskclock.AnimatorUtils; 54 import com.android.deskclock.LogUtils; 55 import com.android.deskclock.R; 56 import com.android.deskclock.Utils; 57 import com.android.deskclock.events.Events; 58 import com.android.deskclock.provider.AlarmInstance; 59 import com.android.deskclock.settings.SettingsActivity; 60 import com.android.deskclock.widget.CircleView; 61 62 public class AlarmActivity extends AppCompatActivity 63 implements View.OnClickListener, View.OnTouchListener { 64 65 private static final String LOGTAG = AlarmActivity.class.getSimpleName(); 66 67 private static final TimeInterpolator PULSE_INTERPOLATOR = 68 PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f); 69 private static final TimeInterpolator REVEAL_INTERPOLATOR = 70 PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f); 71 72 private static final int PULSE_DURATION_MILLIS = 1000; 73 private static final int ALARM_BOUNCE_DURATION_MILLIS = 500; 74 private static final int ALERT_REVEAL_DURATION_MILLIS = 500; 75 private static final int ALERT_FADE_DURATION_MILLIS = 500; 76 private static final int ALERT_DISMISS_DELAY_MILLIS = 2000; 77 78 private static final float BUTTON_SCALE_DEFAULT = 0.7f; 79 private static final int BUTTON_DRAWABLE_ALPHA_DEFAULT = 165; 80 81 private final Handler mHandler = new Handler(); 82 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 83 @Override 84 public void onReceive(Context context, Intent intent) { 85 final String action = intent.getAction(); 86 LogUtils.v(LOGTAG, "Received broadcast: %s", action); 87 88 if (!mAlarmHandled) { 89 switch (action) { 90 case AlarmService.ALARM_SNOOZE_ACTION: 91 snooze(); 92 break; 93 case AlarmService.ALARM_DISMISS_ACTION: 94 dismiss(); 95 break; 96 case AlarmService.ALARM_DONE_ACTION: 97 finish(); 98 break; 99 default: 100 LogUtils.i(LOGTAG, "Unknown broadcast: %s", action); 101 break; 102 } 103 } else { 104 LogUtils.v(LOGTAG, "Ignored broadcast: %s", action); 105 } 106 } 107 }; 108 109 private final ServiceConnection mConnection = new ServiceConnection() { 110 @Override 111 public void onServiceConnected(ComponentName name, IBinder service) { 112 LogUtils.i("Finished binding to AlarmService"); 113 } 114 115 @Override 116 public void onServiceDisconnected(ComponentName name) { 117 LogUtils.i("Disconnected from AlarmService"); 118 } 119 }; 120 121 private AlarmInstance mAlarmInstance; 122 private boolean mAlarmHandled; 123 private String mVolumeBehavior; 124 private int mCurrentHourColor; 125 private boolean mReceiverRegistered; 126 /** Whether the AlarmService is currently bound */ 127 private boolean mServiceBound; 128 129 private AccessibilityManager mAccessibilityManager; 130 131 private ViewGroup mAlertView; 132 private TextView mAlertTitleView; 133 private TextView mAlertInfoView; 134 135 private ViewGroup mContentView; 136 private ImageView mAlarmButton; 137 private ImageView mSnoozeButton; 138 private ImageView mDismissButton; 139 private TextView mHintView; 140 141 private ValueAnimator mAlarmAnimator; 142 private ValueAnimator mSnoozeAnimator; 143 private ValueAnimator mDismissAnimator; 144 private ValueAnimator mPulseAnimator; 145 146 @Override 147 protected void onCreate(Bundle savedInstanceState) { 148 super.onCreate(savedInstanceState); 149 150 final long instanceId = AlarmInstance.getId(getIntent().getData()); 151 mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId); 152 if (mAlarmInstance == null) { 153 // The alarm was deleted before the activity got created, so just finish() 154 LogUtils.e(LOGTAG, "Error displaying alarm for intent: %s", getIntent()); 155 finish(); 156 return; 157 } else if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) { 158 LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance); 159 finish(); 160 return; 161 } 162 163 LogUtils.i(LOGTAG, "Displaying alarm for instance: %s", mAlarmInstance); 164 165 // Get the volume/camera button behavior setting 166 mVolumeBehavior = Utils.getDefaultSharedPreferences(this) 167 .getString(SettingsActivity.KEY_VOLUME_BUTTONS, 168 SettingsActivity.DEFAULT_VOLUME_BEHAVIOR); 169 170 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 171 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD 172 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 173 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 174 | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); 175 176 // Hide navigation bar to minimize accidental tap on Home key 177 hideNavigationBar(); 178 179 // Close dialogs and window shade, so this is fully visible 180 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 181 182 // In order to allow tablets to freely rotate and phones to stick 183 // with "nosensor" (use default device orientation) we have to have 184 // the manifest start with an orientation of unspecified" and only limit 185 // to "nosensor" for phones. Otherwise we get behavior like in b/8728671 186 // where tablets start off in their default orientation and then are 187 // able to freely rotate. 188 if (!getResources().getBoolean(R.bool.config_rotateAlarmAlert)) { 189 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); 190 } 191 192 mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE); 193 194 setContentView(R.layout.alarm_activity); 195 196 mAlertView = (ViewGroup) findViewById(R.id.alert); 197 mAlertTitleView = (TextView) mAlertView.findViewById(R.id.alert_title); 198 mAlertInfoView = (TextView) mAlertView.findViewById(R.id.alert_info); 199 200 mContentView = (ViewGroup) findViewById(R.id.content); 201 mAlarmButton = (ImageView) mContentView.findViewById(R.id.alarm); 202 mSnoozeButton = (ImageView) mContentView.findViewById(R.id.snooze); 203 mDismissButton = (ImageView) mContentView.findViewById(R.id.dismiss); 204 mHintView = (TextView) mContentView.findViewById(R.id.hint); 205 206 final TextView titleView = (TextView) mContentView.findViewById(R.id.title); 207 final TextClock digitalClock = (TextClock) mContentView.findViewById(R.id.digital_clock); 208 final CircleView pulseView = (CircleView) mContentView.findViewById(R.id.pulse); 209 210 titleView.setText(mAlarmInstance.getLabelOrDefault(this)); 211 Utils.setTimeFormat(this, digitalClock); 212 213 mCurrentHourColor = Utils.getCurrentHourColor(); 214 getWindow().setBackgroundDrawable(new ColorDrawable(mCurrentHourColor)); 215 216 mAlarmButton.setOnTouchListener(this); 217 mSnoozeButton.setOnClickListener(this); 218 mDismissButton.setOnClickListener(this); 219 220 mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f); 221 mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE); 222 mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor); 223 mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView, 224 PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.getRadius()), 225 PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR, 226 ColorUtils.setAlphaComponent(pulseView.getFillColor(), 0))); 227 mPulseAnimator.setDuration(PULSE_DURATION_MILLIS); 228 mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR); 229 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE); 230 mPulseAnimator.start(); 231 } 232 233 @Override 234 protected void onResume() { 235 super.onResume(); 236 237 // Re-query for AlarmInstance in case the state has changed externally 238 final long instanceId = AlarmInstance.getId(getIntent().getData()); 239 mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId); 240 241 if (mAlarmInstance == null) { 242 LogUtils.i(LOGTAG, "No alarm instance for instanceId: %d", instanceId); 243 finish(); 244 return; 245 } 246 247 // Verify that the alarm is still firing before showing the activity 248 if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) { 249 LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance); 250 finish(); 251 return; 252 } 253 254 if (!mReceiverRegistered) { 255 // Register to get the alarm done/snooze/dismiss intent. 256 final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION); 257 filter.addAction(AlarmService.ALARM_SNOOZE_ACTION); 258 filter.addAction(AlarmService.ALARM_DISMISS_ACTION); 259 registerReceiver(mReceiver, filter); 260 mReceiverRegistered = true; 261 } 262 263 bindAlarmService(); 264 265 resetAnimations(); 266 } 267 268 @Override 269 protected void onPause() { 270 super.onPause(); 271 272 unbindAlarmService(); 273 274 // Skip if register didn't happen to avoid IllegalArgumentException 275 if (mReceiverRegistered) { 276 unregisterReceiver(mReceiver); 277 mReceiverRegistered = false; 278 } 279 } 280 281 @Override 282 public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) { 283 // Do this in dispatch to intercept a few of the system keys. 284 LogUtils.v(LOGTAG, "dispatchKeyEvent: %s", keyEvent); 285 286 switch (keyEvent.getKeyCode()) { 287 // Volume keys and camera keys dismiss the alarm. 288 case KeyEvent.KEYCODE_POWER: 289 case KeyEvent.KEYCODE_VOLUME_UP: 290 case KeyEvent.KEYCODE_VOLUME_DOWN: 291 case KeyEvent.KEYCODE_VOLUME_MUTE: 292 case KeyEvent.KEYCODE_CAMERA: 293 case KeyEvent.KEYCODE_FOCUS: 294 if (!mAlarmHandled && keyEvent.getAction() == KeyEvent.ACTION_UP) { 295 switch (mVolumeBehavior) { 296 case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE: 297 snooze(); 298 break; 299 case SettingsActivity.VOLUME_BEHAVIOR_DISMISS: 300 dismiss(); 301 break; 302 default: 303 break; 304 } 305 } 306 return true; 307 default: 308 return super.dispatchKeyEvent(keyEvent); 309 } 310 } 311 312 @Override 313 public void onBackPressed() { 314 // Don't allow back to dismiss. 315 } 316 317 @Override 318 public void onClick(View view) { 319 if (mAlarmHandled) { 320 LogUtils.v(LOGTAG, "onClick ignored: %s", view); 321 return; 322 } 323 LogUtils.v(LOGTAG, "onClick: %s", view); 324 325 // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons. 326 if (mAccessibilityManager != null && mAccessibilityManager.isTouchExplorationEnabled()) { 327 if (view == mSnoozeButton) { 328 snooze(); 329 } else if (view == mDismissButton) { 330 dismiss(); 331 } 332 return; 333 } 334 335 if (view == mSnoozeButton) { 336 hintSnooze(); 337 } else if (view == mDismissButton) { 338 hintDismiss(); 339 } 340 } 341 342 @Override 343 public boolean onTouch(View view, MotionEvent motionEvent) { 344 if (mAlarmHandled) { 345 LogUtils.v(LOGTAG, "onTouch ignored: %s", motionEvent); 346 return false; 347 } 348 349 final int[] contentLocation = {0, 0}; 350 mContentView.getLocationOnScreen(contentLocation); 351 352 final float x = motionEvent.getRawX() - contentLocation[0]; 353 final float y = motionEvent.getRawY() - contentLocation[1]; 354 355 final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft(); 356 final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight(); 357 358 final float snoozeFraction, dismissFraction; 359 if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 360 snoozeFraction = getFraction(alarmRight, mSnoozeButton.getLeft(), x); 361 dismissFraction = getFraction(alarmLeft, mDismissButton.getRight(), x); 362 } else { 363 snoozeFraction = getFraction(alarmLeft, mSnoozeButton.getRight(), x); 364 dismissFraction = getFraction(alarmRight, mDismissButton.getLeft(), x); 365 } 366 setAnimatedFractions(snoozeFraction, dismissFraction); 367 368 switch (motionEvent.getActionMasked()) { 369 case MotionEvent.ACTION_DOWN: 370 LogUtils.v(LOGTAG, "onTouch started: %s", motionEvent); 371 372 // Stop the pulse, allowing the last pulse to finish. 373 mPulseAnimator.setRepeatCount(0); 374 break; 375 case MotionEvent.ACTION_UP: 376 LogUtils.v(LOGTAG, "onTouch ended: %s", motionEvent); 377 378 if (snoozeFraction == 1.0f) { 379 snooze(); 380 } else if (dismissFraction == 1.0f) { 381 dismiss(); 382 } else { 383 if (snoozeFraction > 0.0f || dismissFraction > 0.0f) { 384 // Animate back to the initial state. 385 AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator); 386 } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) { 387 // User touched the alarm button, hint the dismiss action 388 hintDismiss(); 389 } 390 391 // Restart the pulse. 392 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE); 393 if (!mPulseAnimator.isStarted()) { 394 mPulseAnimator.start(); 395 } 396 } 397 break; 398 case MotionEvent.ACTION_CANCEL: 399 resetAnimations(); 400 break; 401 default: 402 break; 403 } 404 405 return true; 406 } 407 408 private void hideNavigationBar() { 409 getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 410 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 411 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 412 } 413 414 private void hintSnooze() { 415 final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft(); 416 final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight(); 417 final float translationX = Math.max(mSnoozeButton.getLeft() - alarmRight, 0) 418 + Math.min(mSnoozeButton.getRight() - alarmLeft, 0); 419 getAlarmBounceAnimator(translationX, translationX < 0.0f ? 420 R.string.description_direction_left : R.string.description_direction_right).start(); 421 } 422 423 private void hintDismiss() { 424 final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft(); 425 final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight(); 426 final float translationX = Math.max(mDismissButton.getLeft() - alarmRight, 0) 427 + Math.min(mDismissButton.getRight() - alarmLeft, 0); 428 getAlarmBounceAnimator(translationX, translationX < 0.0f ? 429 R.string.description_direction_left : R.string.description_direction_right).start(); 430 } 431 432 /** 433 * Set animators to initial values and restart pulse on alarm button. 434 */ 435 private void resetAnimations() { 436 // Set the animators to their initial values. 437 setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */); 438 // Restart the pulse. 439 mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE); 440 if (!mPulseAnimator.isStarted()) { 441 mPulseAnimator.start(); 442 } 443 } 444 445 /** 446 * Perform snooze animation and send snooze intent. 447 */ 448 private void snooze() { 449 mAlarmHandled = true; 450 LogUtils.v(LOGTAG, "Snoozed: %s", mAlarmInstance); 451 452 final int accentColor = Utils.obtainStyledColor(this, R.attr.colorAccent, Color.RED); 453 setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */); 454 455 final int snoozeMinutes = AlarmStateManager.getSnoozedMinutes(this); 456 final String infoText = getResources().getQuantityString( 457 R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes); 458 final String accessibilityText = getResources().getQuantityString( 459 R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes); 460 461 getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText, 462 accessibilityText, accentColor, accentColor).start(); 463 464 AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */); 465 466 Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock); 467 468 // Unbind here, otherwise alarm will keep ringing until activity finishes. 469 unbindAlarmService(); 470 } 471 472 /** 473 * Perform dismiss animation and send dismiss intent. 474 */ 475 private void dismiss() { 476 mAlarmHandled = true; 477 LogUtils.v(LOGTAG, "Dismissed: %s", mAlarmInstance); 478 479 setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */); 480 481 getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */, 482 getString(R.string.alarm_alert_off_text) /* accessibilityText */, 483 Color.WHITE, mCurrentHourColor).start(); 484 485 AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance); 486 487 Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock); 488 489 // Unbind here, otherwise alarm will keep ringing until activity finishes. 490 unbindAlarmService(); 491 } 492 493 /** 494 * Bind AlarmService if not yet bound. 495 */ 496 private void bindAlarmService() { 497 if (!mServiceBound) { 498 final Intent intent = new Intent(this, AlarmService.class); 499 bindService(intent, mConnection, Context.BIND_AUTO_CREATE); 500 mServiceBound = true; 501 } 502 } 503 504 /** 505 * Unbind AlarmService if bound. 506 */ 507 private void unbindAlarmService() { 508 if (mServiceBound) { 509 unbindService(mConnection); 510 mServiceBound = false; 511 } 512 } 513 514 private void setAnimatedFractions(float snoozeFraction, float dismissFraction) { 515 final float alarmFraction = Math.max(snoozeFraction, dismissFraction); 516 AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction); 517 AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction); 518 AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction); 519 } 520 521 private float getFraction(float x0, float x1, float x) { 522 return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f); 523 } 524 525 private ValueAnimator getButtonAnimator(ImageView button, int tintColor) { 526 return ObjectAnimator.ofPropertyValuesHolder(button, 527 PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f), 528 PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f), 529 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255), 530 PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA, 531 BUTTON_DRAWABLE_ALPHA_DEFAULT, 255), 532 PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT, 533 AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor)); 534 } 535 536 private ValueAnimator getAlarmBounceAnimator(float translationX, final int hintResId) { 537 final ValueAnimator bounceAnimator = ObjectAnimator.ofFloat(mAlarmButton, 538 View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f); 539 bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR); 540 bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS); 541 bounceAnimator.addListener(new AnimatorListenerAdapter() { 542 @Override 543 public void onAnimationStart(Animator animator) { 544 mHintView.setText(hintResId); 545 if (mHintView.getVisibility() != View.VISIBLE) { 546 mHintView.setVisibility(View.VISIBLE); 547 ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start(); 548 } 549 } 550 }); 551 return bounceAnimator; 552 } 553 554 private Animator getAlertAnimator(final View source, final int titleResId, 555 final String infoText, final String accessibilityText, final int revealColor, 556 final int backgroundColor) { 557 final ViewGroup containerView = (ViewGroup) findViewById(android.R.id.content); 558 559 final Rect sourceBounds = new Rect(0, 0, source.getHeight(), source.getWidth()); 560 containerView.offsetDescendantRectToMyCoords(source, sourceBounds); 561 562 final int centerX = sourceBounds.centerX(); 563 final int centerY = sourceBounds.centerY(); 564 565 final int xMax = Math.max(centerX, containerView.getWidth() - centerX); 566 final int yMax = Math.max(centerY, containerView.getHeight() - centerY); 567 568 final float startRadius = Math.max(sourceBounds.width(), sourceBounds.height()) / 2.0f; 569 final float endRadius = (float) Math.sqrt(xMax * xMax + yMax * yMax); 570 571 final CircleView revealView = new CircleView(this) 572 .setCenterX(centerX) 573 .setCenterY(centerY) 574 .setFillColor(revealColor); 575 containerView.addView(revealView); 576 577 // TODO: Fade out source icon over the reveal (like LOLLIPOP version). 578 579 final Animator revealAnimator = ObjectAnimator.ofFloat( 580 revealView, CircleView.RADIUS, startRadius, endRadius); 581 revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS); 582 revealAnimator.setInterpolator(REVEAL_INTERPOLATOR); 583 revealAnimator.addListener(new AnimatorListenerAdapter() { 584 @Override 585 public void onAnimationEnd(Animator animator) { 586 mAlertView.setVisibility(View.VISIBLE); 587 mAlertTitleView.setText(titleResId); 588 589 if (infoText != null) { 590 mAlertInfoView.setText(infoText); 591 mAlertInfoView.setVisibility(View.VISIBLE); 592 } 593 mContentView.setVisibility(View.GONE); 594 595 getWindow().setBackgroundDrawable(new ColorDrawable(backgroundColor)); 596 } 597 }); 598 599 final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f); 600 fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS); 601 fadeAnimator.addListener(new AnimatorListenerAdapter() { 602 @Override 603 public void onAnimationEnd(Animator animation) { 604 containerView.removeView(revealView); 605 } 606 }); 607 608 final AnimatorSet alertAnimator = new AnimatorSet(); 609 alertAnimator.play(revealAnimator).before(fadeAnimator); 610 alertAnimator.addListener(new AnimatorListenerAdapter() { 611 @Override 612 public void onAnimationEnd(Animator animator) { 613 mAlertView.announceForAccessibility(accessibilityText); 614 mHandler.postDelayed(new Runnable() { 615 @Override 616 public void run() { 617 finish(); 618 } 619 }, ALERT_DISMISS_DELAY_MILLIS); 620 } 621 }); 622 623 return alertAnimator; 624 } 625 } 626