1 /* 2 * Copyright (C) 2020 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 17 package com.android.deskclock.timer 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.AnimatorSet 22 import android.animation.ObjectAnimator 23 import android.content.Context 24 import android.content.Intent 25 import android.os.Bundle 26 import android.os.SystemClock 27 import android.view.KeyEvent 28 import android.view.LayoutInflater 29 import android.view.View 30 import android.view.ViewGroup 31 import android.view.ViewTreeObserver.OnPreDrawListener 32 import android.view.animation.AccelerateInterpolator 33 import android.view.animation.DecelerateInterpolator 34 import android.widget.Button 35 import android.widget.ImageView 36 import androidx.annotation.VisibleForTesting 37 import androidx.viewpager.widget.ViewPager 38 39 import com.android.deskclock.data.DataModel 40 import com.android.deskclock.data.Timer 41 import com.android.deskclock.data.TimerListener 42 import com.android.deskclock.data.TimerStringFormatter 43 import com.android.deskclock.events.Events 44 import com.android.deskclock.uidata.UiDataModel 45 import com.android.deskclock.AnimatorUtils 46 import com.android.deskclock.DeskClock 47 import com.android.deskclock.DeskClockFragment 48 import com.android.deskclock.FabContainer 49 import com.android.deskclock.R 50 import com.android.deskclock.Utils 51 52 import java.io.Serializable 53 import kotlin.math.max 54 import kotlin.math.min 55 56 /** 57 * Displays a vertical list of timers in all states. 58 */ 59 class TimerFragment : DeskClockFragment(UiDataModel.Tab.TIMERS) { 60 /** Notified when the user swipes vertically to change the visible timer. */ 61 private val mTimerPageChangeListener = TimerPageChangeListener() 62 63 /** Scheduled to update the timers while at least one is running. */ 64 private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable() 65 66 /** Updates the [.mPageIndicators] in response to timers being added or removed. */ 67 private val mTimerWatcher: TimerListener = TimerWatcher() 68 69 private lateinit var mCreateTimerView: TimerSetupView 70 private lateinit var mViewPager: ViewPager 71 private lateinit var mAdapter: TimerPagerAdapter 72 private var mTimersView: View? = null 73 private var mCurrentView: View? = null 74 private lateinit var mPageIndicators: Array<ImageView> 75 76 private var mTimerSetupState: Serializable? = null 77 78 /** `true` while this fragment is creating a new timer; `false` otherwise. */ 79 private var mCreatingTimer = false 80 onCreateViewnull81 override fun onCreateView( 82 inflater: LayoutInflater, 83 container: ViewGroup?, 84 savedInstanceState: Bundle? 85 ): View? { 86 val view = inflater.inflate(R.layout.timer_fragment, container, false) 87 88 mAdapter = TimerPagerAdapter(parentFragmentManager) 89 mViewPager = view.findViewById<View>(R.id.vertical_view_pager) as ViewPager 90 mViewPager.setAdapter(mAdapter) 91 mViewPager.addOnPageChangeListener(mTimerPageChangeListener) 92 93 mTimersView = view.findViewById(R.id.timer_view) 94 mCreateTimerView = view.findViewById<View>(R.id.timer_setup) as TimerSetupView 95 mCreateTimerView.setFabContainer(this) 96 mPageIndicators = arrayOf( 97 view.findViewById<View>(R.id.page_indicator0) as ImageView, 98 view.findViewById<View>(R.id.page_indicator1) as ImageView, 99 view.findViewById<View>(R.id.page_indicator2) as ImageView, 100 view.findViewById<View>(R.id.page_indicator3) as ImageView 101 ) 102 103 DataModel.dataModel.addTimerListener(mAdapter) 104 DataModel.dataModel.addTimerListener(mTimerWatcher) 105 106 // If timer setup state is present, retrieve it to be later honored. 107 savedInstanceState?.let { 108 mTimerSetupState = it.getSerializable(KEY_TIMER_SETUP_STATE) 109 } 110 111 return view 112 } 113 onStartnull114 override fun onStart() { 115 super.onStart() 116 117 // Initialize the page indicators. 118 updatePageIndicators() 119 var createTimer = false 120 var showTimerId = -1 121 122 // Examine the intent of the parent activity to determine which view to display. 123 val intent = requireActivity().intent 124 intent?.let { 125 // These extras are single-use; remove them after honoring them. 126 createTimer = it.getBooleanExtra(EXTRA_TIMER_SETUP, false) 127 it.removeExtra(EXTRA_TIMER_SETUP) 128 129 showTimerId = it.getIntExtra(TimerService.EXTRA_TIMER_ID, -1) 130 it.removeExtra(TimerService.EXTRA_TIMER_ID) 131 } 132 133 // Choose the view to display in this fragment. 134 if (showTimerId != -1) { 135 // A specific timer must be shown; show the list of timers. 136 showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 137 } else if (!hasTimers() || createTimer || mTimerSetupState != null) { 138 // No timers exist, a timer is being created, or the last view was timer setup; 139 // show the timer setup view. 140 showCreateTimerView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 141 142 if (mTimerSetupState != null) { 143 mCreateTimerView.state = mTimerSetupState 144 mTimerSetupState = null 145 } 146 } else { 147 // Otherwise, default to showing the list of timers. 148 showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 149 } 150 151 // If the intent did not specify a timer to show, show the last timer that expired. 152 if (showTimerId == -1) { 153 val timer: Timer? = DataModel.dataModel.mostRecentExpiredTimer 154 showTimerId = timer?.id ?: -1 155 } 156 157 // If a specific timer should be displayed, display the corresponding timer tab. 158 if (showTimerId != -1) { 159 val timer: Timer? = DataModel.dataModel.getTimer(showTimerId) 160 timer?.let { 161 val index: Int = DataModel.dataModel.timers.indexOf(it) 162 mViewPager.setCurrentItem(index) 163 } 164 } 165 } 166 onResumenull167 override fun onResume() { 168 super.onResume() 169 170 // We may have received a new intent while paused. 171 val intent = requireActivity().intent 172 if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) { 173 // This extra is single-use; remove after honoring it. 174 val showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1) 175 intent.removeExtra(TimerService.EXTRA_TIMER_ID) 176 177 val timer: Timer? = DataModel.dataModel.getTimer(showTimerId) 178 timer?.let { 179 // A specific timer must be shown; show the list of timers. 180 val index: Int = DataModel.dataModel.timers.indexOf(it) 181 mViewPager.setCurrentItem(index) 182 183 animateToView(mTimersView, null, false) 184 } 185 } 186 } 187 onStopnull188 override fun onStop() { 189 super.onStop() 190 191 // Stop updating the timers when this fragment is no longer visible. 192 stopUpdatingTime() 193 } 194 onDestroyViewnull195 override fun onDestroyView() { 196 super.onDestroyView() 197 198 DataModel.dataModel.removeTimerListener(mAdapter) 199 DataModel.dataModel.removeTimerListener(mTimerWatcher) 200 } 201 onSaveInstanceStatenull202 override fun onSaveInstanceState(outState: Bundle) { 203 super.onSaveInstanceState(outState) 204 205 // If the timer creation view is visible, store the input for later restoration. 206 if (mCurrentView === mCreateTimerView) { 207 mTimerSetupState = mCreateTimerView.state 208 outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState) 209 } 210 } 211 updateFabnull212 private fun updateFab(fab: ImageView, animate: Boolean) { 213 if (mCurrentView === mTimersView) { 214 val timer = timer 215 if (timer == null) { 216 fab.visibility = View.INVISIBLE 217 return 218 } 219 220 fab.visibility = View.VISIBLE 221 when (timer.state) { 222 Timer.State.RUNNING -> { 223 if (animate) { 224 fab.setImageResource(R.drawable.ic_play_pause_animation) 225 } else { 226 fab.setImageResource(R.drawable.ic_play_pause) 227 } 228 fab.contentDescription = fab.resources.getString(R.string.timer_stop) 229 } 230 Timer.State.RESET -> { 231 if (animate) { 232 fab.setImageResource(R.drawable.ic_stop_play_animation) 233 } else { 234 fab.setImageResource(R.drawable.ic_pause_play) 235 } 236 fab.contentDescription = fab.resources.getString(R.string.timer_start) 237 } 238 Timer.State.PAUSED -> { 239 if (animate) { 240 fab.setImageResource(R.drawable.ic_pause_play_animation) 241 } else { 242 fab.setImageResource(R.drawable.ic_pause_play) 243 } 244 fab.contentDescription = fab.resources.getString(R.string.timer_start) 245 } 246 Timer.State.MISSED, Timer.State.EXPIRED -> { 247 fab.setImageResource(R.drawable.ic_stop_white_24dp) 248 fab.contentDescription = fab.resources.getString(R.string.timer_stop) 249 } 250 } 251 } else if (mCurrentView === mCreateTimerView) { 252 if (mCreateTimerView.hasValidInput()) { 253 fab.setImageResource(R.drawable.ic_start_white_24dp) 254 fab.contentDescription = fab.resources.getString(R.string.timer_start) 255 fab.visibility = View.VISIBLE 256 } else { 257 fab.contentDescription = null 258 fab.visibility = View.INVISIBLE 259 } 260 } 261 } 262 onUpdateFabnull263 override fun onUpdateFab(fab: ImageView) { 264 updateFab(fab, false) 265 } 266 onMorphFabnull267 override fun onMorphFab(fab: ImageView) { 268 // Update the fab's drawable to match the current timer state. 269 updateFab(fab, Utils.isNOrLater) 270 // Animate the drawable. 271 AnimatorUtils.startDrawableAnimation(fab) 272 } 273 onUpdateFabButtonsnull274 override fun onUpdateFabButtons(left: Button, right: Button) { 275 if (mCurrentView === mTimersView) { 276 left.isClickable = true 277 left.setText(R.string.timer_delete) 278 left.contentDescription = left.resources.getString(R.string.timer_delete) 279 left.visibility = View.VISIBLE 280 281 right.isClickable = true 282 right.setText(R.string.timer_add_timer) 283 right.contentDescription = right.resources.getString(R.string.timer_add_timer) 284 right.visibility = View.VISIBLE 285 } else if (mCurrentView === mCreateTimerView) { 286 left.isClickable = true 287 left.setText(R.string.timer_cancel) 288 left.contentDescription = left.resources.getString(R.string.timer_cancel) 289 // If no timers yet exist, the user is forced to create the first one. 290 left.visibility = if (hasTimers()) View.VISIBLE else View.INVISIBLE 291 292 right.visibility = View.INVISIBLE 293 } 294 } 295 onFabClicknull296 override fun onFabClick(fab: ImageView) { 297 if (mCurrentView === mTimersView) { 298 // If no timer is currently showing a fab action is meaningless. 299 val timer = timer ?: return 300 301 val context = fab.context 302 val currentTime: Long = timer.remainingTime 303 304 when (timer.state) { 305 Timer.State.RUNNING -> { 306 DataModel.dataModel.pauseTimer(timer) 307 Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock) 308 if (currentTime > 0) { 309 mTimersView?.announceForAccessibility(TimerStringFormatter.formatString( 310 context, R.string.timer_accessibility_stopped, currentTime, true)) 311 } 312 } 313 Timer.State.PAUSED, Timer.State.RESET -> { 314 DataModel.dataModel.startTimer(timer) 315 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock) 316 if (currentTime > 0) { 317 mTimersView?.announceForAccessibility(TimerStringFormatter.formatString( 318 context, R.string.timer_accessibility_started, currentTime, true)) 319 } 320 } 321 Timer.State.MISSED, Timer.State.EXPIRED -> { 322 DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock) 323 } 324 } 325 } else if (mCurrentView === mCreateTimerView) { 326 mCreatingTimer = true 327 try { 328 // Create the new timer. 329 val timerLength: Long = mCreateTimerView.timeInMillis 330 val timer: Timer = DataModel.dataModel.addTimer(timerLength, "", false) 331 Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock) 332 333 // Start the new timer. 334 DataModel.dataModel.startTimer(timer) 335 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock) 336 337 // Display the freshly created timer view. 338 mViewPager.setCurrentItem(0) 339 } finally { 340 mCreatingTimer = false 341 } 342 343 // Return to the list of timers. 344 animateToView(mTimersView, null, true) 345 } 346 } 347 onLeftButtonClicknull348 override fun onLeftButtonClick(left: Button) { 349 if (mCurrentView === mTimersView) { 350 // Clicking the "delete" button. 351 val timer = timer ?: return 352 353 if (mAdapter.getCount() > 1) { 354 animateTimerRemove(timer) 355 } else { 356 animateToView(mCreateTimerView, timer, false) 357 } 358 359 left.announceForAccessibility(requireActivity().getString(R.string.timer_deleted)) 360 } else if (mCurrentView === mCreateTimerView) { 361 // Clicking the "cancel" button on the timer creation page returns to the timers list. 362 mCreateTimerView.reset() 363 364 animateToView(mTimersView, null, false) 365 366 left.announceForAccessibility(requireActivity().getString(R.string.timer_canceled)) 367 } 368 } 369 onRightButtonClicknull370 override fun onRightButtonClick(right: Button) { 371 if (mCurrentView !== mCreateTimerView) { 372 animateToView(mCreateTimerView, null, true) 373 } 374 } 375 onKeyDownnull376 override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { 377 return if (mCurrentView === mCreateTimerView) { 378 mCreateTimerView.onKeyDown(keyCode, event) 379 } else super.onKeyDown(keyCode, event) 380 } 381 382 /** 383 * Updates the state of the page indicators so they reflect the selected page in the context of 384 * all pages. 385 */ updatePageIndicatorsnull386 private fun updatePageIndicators() { 387 val page: Int = mViewPager.getCurrentItem() 388 val pageIndicatorCount = mPageIndicators.size 389 val pageCount = mAdapter.getCount() 390 391 val states = computePageIndicatorStates(page, pageIndicatorCount, pageCount) 392 for (i in states.indices) { 393 val state = states[i] 394 val pageIndicator = mPageIndicators[i] 395 if (state == 0) { 396 pageIndicator.visibility = View.GONE 397 } else { 398 pageIndicator.visibility = View.VISIBLE 399 pageIndicator.setImageResource(state) 400 } 401 } 402 } 403 404 /** 405 * Display the view that creates a new timer. 406 */ showCreateTimerViewnull407 private fun showCreateTimerView(updateTypes: Int) { 408 // Stop animating the timers. 409 stopUpdatingTime() 410 411 // Show the creation view; hide the timer view. 412 mTimersView?.visibility = View.GONE 413 mCreateTimerView.visibility = View.VISIBLE 414 415 // Record the fact that the create view is visible. 416 mCurrentView = mCreateTimerView 417 418 // Update the fab and buttons. 419 updateFab(updateTypes) 420 } 421 422 /** 423 * Display the view that lists all existing timers. 424 */ showTimersViewnull425 private fun showTimersView(updateTypes: Int) { 426 // Clear any defunct timer creation state; the next timer creation starts fresh. 427 mTimerSetupState = null 428 429 // Show the timer view; hide the creation view. 430 mTimersView?.visibility = View.VISIBLE 431 mCreateTimerView.visibility = View.GONE 432 433 // Record the fact that the create view is visible. 434 mCurrentView = mTimersView 435 436 // Update the fab and buttons. 437 updateFab(updateTypes) 438 439 // Start animating the timers. 440 startUpdatingTime() 441 } 442 443 /** 444 * @param timerToRemove the timer to be removed during the animation 445 */ animateTimerRemovenull446 private fun animateTimerRemove(timerToRemove: Timer) { 447 val duration = UiDataModel.uiDataModel.shortAnimationDuration 448 449 val fadeOut: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 1f, 0f) 450 fadeOut.duration = duration 451 fadeOut.interpolator = DecelerateInterpolator() 452 fadeOut.addListener(object : AnimatorListenerAdapter() { 453 override fun onAnimationEnd(animation: Animator) { 454 DataModel.dataModel.removeTimer(timerToRemove) 455 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock) 456 } 457 }) 458 459 val fadeIn: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 0f, 1f) 460 fadeIn.duration = duration 461 fadeIn.interpolator = AccelerateInterpolator() 462 463 val animatorSet = AnimatorSet() 464 animatorSet.play(fadeOut).before(fadeIn) 465 animatorSet.start() 466 } 467 468 /** 469 * @param toView one of [.mTimersView] or [.mCreateTimerView] 470 * @param timerToRemove the timer to be removed during the animation; `null` if no timer 471 * should be removed 472 * @param animateDown `true` if the views should animate upwards, otherwise downwards 473 */ animateToViewnull474 private fun animateToView( 475 toView: View?, 476 timerToRemove: Timer?, 477 animateDown: Boolean 478 ) { 479 if (mCurrentView === toView) { 480 return 481 } 482 483 val toTimers = toView === mTimersView 484 if (toTimers) { 485 mTimersView?.visibility = View.VISIBLE 486 } else { 487 mCreateTimerView.visibility = View.VISIBLE 488 } 489 // Avoid double-taps by enabling/disabling the set of buttons active on the new view. 490 updateFab(FabContainer.BUTTONS_DISABLE) 491 492 val animationDuration = UiDataModel.uiDataModel.longAnimationDuration 493 494 val viewTreeObserver = toView!!.viewTreeObserver 495 viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { 496 override fun onPreDraw(): Boolean { 497 if (viewTreeObserver.isAlive) { 498 viewTreeObserver.removeOnPreDrawListener(this) 499 } 500 501 val view = mTimersView?.findViewById<View>(R.id.timer_time) 502 val distanceY: Float = if (view != null) view.height + view.y else 0f 503 val translationDistance = if (animateDown) distanceY else -distanceY 504 505 toView.translationY = -translationDistance 506 mCurrentView?.translationY = 0f 507 toView.alpha = 0f 508 mCurrentView?.alpha = 1f 509 510 val translateCurrent: Animator = ObjectAnimator.ofFloat(mCurrentView, 511 View.TRANSLATION_Y, translationDistance) 512 val translateNew: Animator = ObjectAnimator.ofFloat(toView, View.TRANSLATION_Y, 0f) 513 val translationAnimatorSet = AnimatorSet() 514 translationAnimatorSet.playTogether(translateCurrent, translateNew) 515 translationAnimatorSet.duration = animationDuration 516 translationAnimatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN 517 518 val fadeOutAnimator: Animator = ObjectAnimator.ofFloat(mCurrentView, View.ALPHA, 0f) 519 fadeOutAnimator.duration = animationDuration / 2 520 fadeOutAnimator.addListener(object : AnimatorListenerAdapter() { 521 override fun onAnimationStart(animation: Animator) { 522 super.onAnimationStart(animation) 523 524 // The fade-out animation and fab-shrinking animation should run together. 525 updateFab(FabContainer.FAB_AND_BUTTONS_SHRINK) 526 } 527 528 override fun onAnimationEnd(animation: Animator) { 529 super.onAnimationEnd(animation) 530 if (toTimers) { 531 showTimersView(FabContainer.FAB_AND_BUTTONS_EXPAND) 532 533 // Reset the state of the create view. 534 mCreateTimerView.reset() 535 } else { 536 showCreateTimerView(FabContainer.FAB_AND_BUTTONS_EXPAND) 537 } 538 if (timerToRemove != null) { 539 DataModel.dataModel.removeTimer(timerToRemove) 540 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock) 541 } 542 543 // Update the fab and button states now that the correct view is visible and 544 // before the animation to expand the fab and buttons starts. 545 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 546 } 547 }) 548 549 val fadeInAnimator: Animator = ObjectAnimator.ofFloat(toView, View.ALPHA, 1f) 550 fadeInAnimator.duration = animationDuration / 2 551 fadeInAnimator.startDelay = animationDuration / 2 552 553 val animatorSet = AnimatorSet() 554 animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet) 555 animatorSet.addListener(object : AnimatorListenerAdapter() { 556 override fun onAnimationEnd(animation: Animator) { 557 super.onAnimationEnd(animation) 558 mTimersView?.translationY = 0f 559 mCreateTimerView.translationY = 0f 560 mTimersView?.alpha = 1f 561 mCreateTimerView.alpha = 1f 562 } 563 }) 564 animatorSet.start() 565 566 return true 567 } 568 }) 569 } 570 hasTimersnull571 private fun hasTimers(): Boolean { 572 return mAdapter.getCount() > 0 573 } 574 575 private val timer: Timer? 576 get() { 577 if (!::mViewPager.isInitialized) { 578 return null 579 } 580 581 return if (mAdapter.getCount() == 0) { 582 null 583 } else { 584 mAdapter.getTimer(mViewPager.getCurrentItem()) 585 } 586 } 587 startUpdatingTimenull588 private fun startUpdatingTime() { 589 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 590 stopUpdatingTime() 591 mViewPager.post(mTimeUpdateRunnable) 592 } 593 stopUpdatingTimenull594 private fun stopUpdatingTime() { 595 mViewPager.removeCallbacks(mTimeUpdateRunnable) 596 } 597 598 /** 599 * Periodically refreshes the state of each timer. 600 */ 601 private inner class TimeUpdateRunnable : Runnable { runnull602 override fun run() { 603 val startTime = SystemClock.elapsedRealtime() 604 // If no timers require continuous updates, avoid scheduling the next update. 605 if (!mAdapter.updateTime()) { 606 return 607 } 608 val endTime = SystemClock.elapsedRealtime() 609 610 // Try to maintain a consistent period of time between redraws. 611 val delay = max(0, startTime + 20 - endTime) 612 mTimersView?.postDelayed(this, delay) 613 } 614 } 615 616 /** 617 * Update the page indicators and fab in response to a new timer becoming visible. 618 */ 619 private inner class TimerPageChangeListener : ViewPager.SimpleOnPageChangeListener() { onPageSelectednull620 override fun onPageSelected(position: Int) { 621 updatePageIndicators() 622 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 623 624 // Showing a new timer page may introduce a timer requiring continuous updates. 625 startUpdatingTime() 626 } 627 onPageScrollStateChangednull628 override fun onPageScrollStateChanged(state: Int) { 629 // Teasing a neighboring timer may introduce a timer requiring continuous updates. 630 if (state == ViewPager.SCROLL_STATE_DRAGGING) { 631 startUpdatingTime() 632 } 633 } 634 } 635 636 /** 637 * Update the page indicators in response to timers being added or removed. 638 * Update the fab in response to the visible timer changing. 639 */ 640 private inner class TimerWatcher : TimerListener { timerAddednull641 override fun timerAdded(timer: Timer) { 642 updatePageIndicators() 643 // If the timer is being created via this fragment avoid adjusting the fab. 644 // Timer setup view is about to be animated away in response to this timer creation. 645 // Changes to the fab immediately preceding that animation are jarring. 646 if (!mCreatingTimer) { 647 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 648 } 649 } 650 timerUpdatednull651 override fun timerUpdated(before: Timer, after: Timer) { 652 // If the timer started, animate the timers. 653 if (before.isReset && !after.isReset) { 654 startUpdatingTime() 655 } 656 657 // Fetch the index of the change. 658 val index: Int = DataModel.dataModel.timers.indexOf(after) 659 660 // If the timer just expired but is not displayed, display it now. 661 if (!before.isExpired && after.isExpired && index != mViewPager.getCurrentItem()) { 662 mViewPager.setCurrentItem(index, true) 663 } else if (mCurrentView === mTimersView && index == mViewPager.getCurrentItem()) { 664 // Morph the fab from its old state to new state if necessary. 665 if (before.state != after.state && 666 !(before.isPaused && after.isReset)) { 667 updateFab(FabContainer.FAB_MORPH) 668 } 669 } 670 } 671 timerRemovednull672 override fun timerRemoved(timer: Timer) { 673 updatePageIndicators() 674 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 675 676 if (mCurrentView === mTimersView && mAdapter.getCount() == 0) { 677 animateToView(mCreateTimerView, null, false) 678 } 679 } 680 } 681 682 companion object { 683 private const val EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP" 684 685 private const val KEY_TIMER_SETUP_STATE = "timer_setup_input" 686 687 /** 688 * @return an Intent that selects the timers tab with the 689 * setup screen for a new timer in place. 690 */ 691 @VisibleForTesting 692 @JvmStatic createTimerSetupIntentnull693 fun createTimerSetupIntent(context: Context): Intent { 694 return Intent(context, DeskClock::class.java).putExtra(EXTRA_TIMER_SETUP, true) 695 } 696 697 /** 698 * @param page the selected page; value between 0 and `pageCount` 699 * @param pageIndicatorCount the number of indicators displaying the `page` location 700 * @param pageCount the number of pages that exist 701 * @return an array of length `pageIndicatorCount` specifying which image to display for 702 * each page indicator or 0 if the page indicator should be hidden 703 */ 704 @VisibleForTesting 705 @JvmStatic computePageIndicatorStatesnull706 fun computePageIndicatorStates( 707 page: Int, 708 pageIndicatorCount: Int, 709 pageCount: Int 710 ): IntArray { 711 // Compute the number of page indicators that will be visible. 712 val rangeSize = min(pageIndicatorCount, pageCount) 713 714 // Compute the inclusive range of pages to indicate centered around the selected page. 715 var rangeStart = page - rangeSize / 2 716 var rangeEnd = rangeStart + rangeSize - 1 717 718 // Clamp the range of pages if they extend beyond the last page. 719 if (rangeEnd >= pageCount) { 720 rangeEnd = pageCount - 1 721 rangeStart = rangeEnd - rangeSize + 1 722 } 723 724 // Clamp the range of pages if they extend beyond the first page. 725 if (rangeStart < 0) { 726 rangeStart = 0 727 rangeEnd = rangeSize - 1 728 } 729 730 // Build the result with all page indicators initially hidden. 731 val states = IntArray(pageIndicatorCount) 732 states.fill(0) 733 734 // If 0 or 1 total pages exist, all page indicators must remain hidden. 735 if (rangeSize < 2) { 736 return states 737 } 738 739 // Initialize the visible page indicators to be dark. 740 states.fill(R.drawable.ic_swipe_circle_dark, 0, rangeSize) 741 742 // If more pages exist before the first page indicator, make it a fade-in gradient. 743 if (rangeStart > 0) { 744 states[0] = R.drawable.ic_swipe_circle_top 745 } 746 747 // If more pages exist after the last page indicator, make it a fade-out gradient. 748 if (rangeEnd < pageCount - 1) { 749 states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom 750 } 751 752 // Set the indicator of the selected page to be light. 753 states[page - rangeStart] = R.drawable.ic_swipe_circle_light 754 return states 755 } 756 } 757 }