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