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 onCreate(Bundle savedInstanceState)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 onResume()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 onPause()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 dispatchKeyEvent(@onNull KeyEvent keyEvent)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 onBackPressed()313 public void onBackPressed() { 314 // Don't allow back to dismiss. 315 } 316 317 @Override onClick(View view)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 onTouch(View view, MotionEvent motionEvent)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 hideNavigationBar()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 hintSnooze()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 hintDismiss()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 */ resetAnimations()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 */ snooze()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 */ dismiss()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 */ bindAlarmService()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 */ unbindAlarmService()507 private void unbindAlarmService() { 508 if (mServiceBound) { 509 unbindService(mConnection); 510 mServiceBound = false; 511 } 512 } 513 setAnimatedFractions(float snoozeFraction, float dismissFraction)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 getFraction(float x0, float x1, float x)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 getButtonAnimator(ImageView button, int tintColor)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 getAlarmBounceAnimator(float translationX, final int hintResId)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 getAlertAnimator(final View source, final int titleResId, final String infoText, final String accessibilityText, final int revealColor, final int backgroundColor)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