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.accessibilityservice.AccessibilityServiceInfo; 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.PropertyValuesHolder; 24 import android.animation.TimeInterpolator; 25 import android.animation.ValueAnimator; 26 import android.content.BroadcastReceiver; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.ServiceConnection; 32 import android.content.pm.ActivityInfo; 33 import android.graphics.Color; 34 import android.graphics.Rect; 35 import android.graphics.drawable.ColorDrawable; 36 import android.media.AudioManager; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.IBinder; 40 import androidx.annotation.NonNull; 41 import androidx.core.graphics.ColorUtils; 42 import androidx.core.view.animation.PathInterpolatorCompat; 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.BaseActivity; 55 import com.android.deskclock.LogUtils; 56 import com.android.deskclock.R; 57 import com.android.deskclock.ThemeUtils; 58 import com.android.deskclock.Utils; 59 import com.android.deskclock.data.DataModel; 60 import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior; 61 import com.android.deskclock.events.Events; 62 import com.android.deskclock.provider.AlarmInstance; 63 import com.android.deskclock.widget.CircleView; 64 65 import java.util.List; 66 67 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC; 68 69 public 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 onCreate(Bundle savedInstanceState)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 onResume()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 onPause()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 dispatchKeyEvent(@onNull KeyEvent keyEvent)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 onBackPressed()317 public void onBackPressed() { 318 // Don't allow back to dismiss. 319 } 320 321 @Override onClick(View view)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 onTouch(View view, MotionEvent event)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 hideNavigationBar()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 */ isAccessibilityEnabled()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 hintSnooze()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 hintDismiss()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 */ resetAnimations()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 */ snooze()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 */ dismiss()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 */ bindAlarmService()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 */ unbindAlarmService()544 private void unbindAlarmService() { 545 if (mServiceBound) { 546 unbindService(mConnection); 547 mServiceBound = false; 548 } 549 } 550 setAnimatedFractions(float snoozeFraction, float dismissFraction)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 getFraction(float x0, float x1, float x)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 getButtonAnimator(ImageView button, int tintColor)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 getAlarmBounceAnimator(float translationX, final int hintResId)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 getAlertAnimator(final View source, final int titleResId, final String infoText, final String accessibilityText, final int revealColor, final int backgroundColor)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