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 // TODO(b/157255731) Replace the deprecated Fragment related calls 60 class TimerFragment : DeskClockFragment(UiDataModel.Tab.TIMERS) { 61 /** Notified when the user swipes vertically to change the visible timer. */ 62 private val mTimerPageChangeListener = TimerPageChangeListener() 63 64 /** Scheduled to update the timers while at least one is running. */ 65 private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable() 66 67 /** Updates the [.mPageIndicators] in response to timers being added or removed. */ 68 private val mTimerWatcher: TimerListener = TimerWatcher() 69 70 private lateinit var mCreateTimerView: TimerSetupView 71 private lateinit var mViewPager: ViewPager 72 private lateinit var mAdapter: TimerPagerAdapter 73 private var mTimersView: View? = null 74 private var mCurrentView: View? = null 75 private lateinit var mPageIndicators: Array<ImageView> 76 77 private var mTimerSetupState: Serializable? = null 78 79 /** `true` while this fragment is creating a new timer; `false` otherwise. */ 80 private var mCreatingTimer = false 81 onCreateViewnull82 override fun onCreateView( 83 inflater: LayoutInflater, 84 container: ViewGroup?, 85 savedInstanceState: Bundle? 86 ): View? { 87 val view = inflater.inflate(R.layout.timer_fragment, container, false) 88 89 mAdapter = TimerPagerAdapter(fragmentManager) 90 mViewPager = view.findViewById<View>(R.id.vertical_view_pager) as ViewPager 91 mViewPager.setAdapter(mAdapter) 92 mViewPager.addOnPageChangeListener(mTimerPageChangeListener) 93 94 mTimersView = view.findViewById(R.id.timer_view) 95 mCreateTimerView = view.findViewById<View>(R.id.timer_setup) as TimerSetupView 96 mCreateTimerView.setFabContainer(this) 97 mPageIndicators = arrayOf( 98 view.findViewById<View>(R.id.page_indicator0) as ImageView, 99 view.findViewById<View>(R.id.page_indicator1) as ImageView, 100 view.findViewById<View>(R.id.page_indicator2) as ImageView, 101 view.findViewById<View>(R.id.page_indicator3) as ImageView 102 ) 103 104 DataModel.dataModel.addTimerListener(mAdapter) 105 DataModel.dataModel.addTimerListener(mTimerWatcher) 106 107 // If timer setup state is present, retrieve it to be later honored. 108 savedInstanceState?.let { 109 mTimerSetupState = it.getSerializable(KEY_TIMER_SETUP_STATE) 110 } 111 112 return view 113 } 114 onStartnull115 override fun onStart() { 116 super.onStart() 117 118 // Initialize the page indicators. 119 updatePageIndicators() 120 var createTimer = false 121 var showTimerId = -1 122 123 // Examine the intent of the parent activity to determine which view to display. 124 val intent = activity.intent 125 intent?.let { 126 // These extras are single-use; remove them after honoring them. 127 createTimer = it.getBooleanExtra(EXTRA_TIMER_SETUP, false) 128 it.removeExtra(EXTRA_TIMER_SETUP) 129 130 showTimerId = it.getIntExtra(TimerService.EXTRA_TIMER_ID, -1) 131 it.removeExtra(TimerService.EXTRA_TIMER_ID) 132 } 133 134 // Choose the view to display in this fragment. 135 if (showTimerId != -1) { 136 // A specific timer must be shown; show the list of timers. 137 showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 138 } else if (!hasTimers() || createTimer || mTimerSetupState != null) { 139 // No timers exist, a timer is being created, or the last view was timer setup; 140 // show the timer setup view. 141 showCreateTimerView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 142 143 if (mTimerSetupState != null) { 144 mCreateTimerView.state = mTimerSetupState 145 mTimerSetupState = null 146 } 147 } else { 148 // Otherwise, default to showing the list of timers. 149 showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 150 } 151 152 // If the intent did not specify a timer to show, show the last timer that expired. 153 if (showTimerId == -1) { 154 val timer: Timer? = DataModel.dataModel.mostRecentExpiredTimer 155 showTimerId = timer?.id ?: -1 156 } 157 158 // If a specific timer should be displayed, display the corresponding timer tab. 159 if (showTimerId != -1) { 160 val timer: Timer? = DataModel.dataModel.getTimer(showTimerId) 161 timer?.let { 162 val index: Int = DataModel.dataModel.timers.indexOf(it) 163 mViewPager.setCurrentItem(index) 164 } 165 } 166 } 167 onResumenull168 override fun onResume() { 169 super.onResume() 170 171 // We may have received a new intent while paused. 172 val intent = activity.intent 173 if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) { 174 // This extra is single-use; remove after honoring it. 175 val showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1) 176 intent.removeExtra(TimerService.EXTRA_TIMER_ID) 177 178 val timer: Timer? = DataModel.dataModel.getTimer(showTimerId) 179 timer?.let { 180 // A specific timer must be shown; show the list of timers. 181 val index: Int = DataModel.dataModel.timers.indexOf(it) 182 mViewPager.setCurrentItem(index) 183 184 animateToView(mTimersView, null, false) 185 } 186 } 187 } 188 onStopnull189 override fun onStop() { 190 super.onStop() 191 192 // Stop updating the timers when this fragment is no longer visible. 193 stopUpdatingTime() 194 } 195 onDestroyViewnull196 override fun onDestroyView() { 197 super.onDestroyView() 198 199 DataModel.dataModel.removeTimerListener(mAdapter) 200 DataModel.dataModel.removeTimerListener(mTimerWatcher) 201 } 202 onSaveInstanceStatenull203 override fun onSaveInstanceState(outState: Bundle) { 204 super.onSaveInstanceState(outState) 205 206 // If the timer creation view is visible, store the input for later restoration. 207 if (mCurrentView === mCreateTimerView) { 208 mTimerSetupState = mCreateTimerView.state 209 outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState) 210 } 211 } 212 updateFabnull213 private fun updateFab(fab: ImageView, animate: Boolean) { 214 if (mCurrentView === mTimersView) { 215 val timer = timer 216 if (timer == null) { 217 fab.visibility = View.INVISIBLE 218 return 219 } 220 221 fab.visibility = View.VISIBLE 222 when (timer.state) { 223 Timer.State.RUNNING -> { 224 if (animate) { 225 fab.setImageResource(R.drawable.ic_play_pause_animation) 226 } else { 227 fab.setImageResource(R.drawable.ic_play_pause) 228 } 229 fab.contentDescription = fab.resources.getString(R.string.timer_stop) 230 } 231 Timer.State.RESET -> { 232 if (animate) { 233 fab.setImageResource(R.drawable.ic_stop_play_animation) 234 } else { 235 fab.setImageResource(R.drawable.ic_pause_play) 236 } 237 fab.contentDescription = fab.resources.getString(R.string.timer_start) 238 } 239 Timer.State.PAUSED -> { 240 if (animate) { 241 fab.setImageResource(R.drawable.ic_pause_play_animation) 242 } else { 243 fab.setImageResource(R.drawable.ic_pause_play) 244 } 245 fab.contentDescription = fab.resources.getString(R.string.timer_start) 246 } 247 Timer.State.MISSED, Timer.State.EXPIRED -> { 248 fab.setImageResource(R.drawable.ic_stop_white_24dp) 249 fab.contentDescription = fab.resources.getString(R.string.timer_stop) 250 } 251 } 252 } else if (mCurrentView === mCreateTimerView) { 253 if (mCreateTimerView.hasValidInput()) { 254 fab.setImageResource(R.drawable.ic_start_white_24dp) 255 fab.contentDescription = fab.resources.getString(R.string.timer_start) 256 fab.visibility = View.VISIBLE 257 } else { 258 fab.contentDescription = null 259 fab.visibility = View.INVISIBLE 260 } 261 } 262 } 263 onUpdateFabnull264 override fun onUpdateFab(fab: ImageView) { 265 updateFab(fab, false) 266 } 267 onMorphFabnull268 override fun onMorphFab(fab: ImageView) { 269 // Update the fab's drawable to match the current timer state. 270 updateFab(fab, Utils.isNOrLater()) 271 // Animate the drawable. 272 AnimatorUtils.startDrawableAnimation(fab) 273 } 274 onUpdateFabButtonsnull275 override fun onUpdateFabButtons(left: Button, right: Button) { 276 if (mCurrentView === mTimersView) { 277 left.isClickable = true 278 left.setText(R.string.timer_delete) 279 left.contentDescription = left.resources.getString(R.string.timer_delete) 280 left.visibility = View.VISIBLE 281 282 right.isClickable = true 283 right.setText(R.string.timer_add_timer) 284 right.contentDescription = right.resources.getString(R.string.timer_add_timer) 285 right.visibility = View.VISIBLE 286 } else if (mCurrentView === mCreateTimerView) { 287 left.isClickable = true 288 left.setText(R.string.timer_cancel) 289 left.contentDescription = left.resources.getString(R.string.timer_cancel) 290 // If no timers yet exist, the user is forced to create the first one. 291 left.visibility = if (hasTimers()) View.VISIBLE else View.INVISIBLE 292 293 right.visibility = View.INVISIBLE 294 } 295 } 296 onFabClicknull297 override fun onFabClick(fab: ImageView) { 298 if (mCurrentView === mTimersView) { 299 // If no timer is currently showing a fab action is meaningless. 300 val timer = timer ?: return 301 302 val context = fab.context 303 val currentTime: Long = timer.remainingTime 304 305 when (timer.state) { 306 Timer.State.RUNNING -> { 307 DataModel.dataModel.pauseTimer(timer) 308 Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock) 309 if (currentTime > 0) { 310 mTimersView?.announceForAccessibility(TimerStringFormatter.formatString( 311 context, R.string.timer_accessibility_stopped, currentTime, true)) 312 } 313 } 314 Timer.State.PAUSED, Timer.State.RESET -> { 315 DataModel.dataModel.startTimer(timer) 316 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock) 317 if (currentTime > 0) { 318 mTimersView?.announceForAccessibility(TimerStringFormatter.formatString( 319 context, R.string.timer_accessibility_started, currentTime, true)) 320 } 321 } 322 Timer.State.MISSED, Timer.State.EXPIRED -> { 323 DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock) 324 } 325 } 326 } else if (mCurrentView === mCreateTimerView) { 327 mCreatingTimer = true 328 try { 329 // Create the new timer. 330 val timerLength: Long = mCreateTimerView.timeInMillis 331 val timer: Timer = DataModel.dataModel.addTimer(timerLength, "", false) 332 Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock) 333 334 // Start the new timer. 335 DataModel.dataModel.startTimer(timer) 336 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock) 337 338 // Display the freshly created timer view. 339 mViewPager.setCurrentItem(0) 340 } finally { 341 mCreatingTimer = false 342 } 343 344 // Return to the list of timers. 345 animateToView(mTimersView, null, true) 346 } 347 } 348 onLeftButtonClicknull349 override fun onLeftButtonClick(left: Button) { 350 if (mCurrentView === mTimersView) { 351 // Clicking the "delete" button. 352 val timer = timer ?: return 353 354 if (mAdapter.getCount() > 1) { 355 animateTimerRemove(timer) 356 } else { 357 animateToView(mCreateTimerView, timer, false) 358 } 359 360 left.announceForAccessibility(activity.getString(R.string.timer_deleted)) 361 } else if (mCurrentView === mCreateTimerView) { 362 // Clicking the "cancel" button on the timer creation page returns to the timers list. 363 mCreateTimerView.reset() 364 365 animateToView(mTimersView, null, false) 366 367 left.announceForAccessibility(activity.getString(R.string.timer_canceled)) 368 } 369 } 370 onRightButtonClicknull371 override fun onRightButtonClick(right: Button) { 372 if (mCurrentView !== mCreateTimerView) { 373 animateToView(mCreateTimerView, null, true) 374 } 375 } 376 onKeyDownnull377 override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { 378 return if (mCurrentView === mCreateTimerView) { 379 mCreateTimerView.onKeyDown(keyCode, event) 380 } else super.onKeyDown(keyCode, event) 381 } 382 383 /** 384 * Updates the state of the page indicators so they reflect the selected page in the context of 385 * all pages. 386 */ updatePageIndicatorsnull387 private fun updatePageIndicators() { 388 val page: Int = mViewPager.getCurrentItem() 389 val pageIndicatorCount = mPageIndicators.size 390 val pageCount = mAdapter.getCount() 391 392 val states = computePageIndicatorStates(page, pageIndicatorCount, pageCount) 393 for (i in states.indices) { 394 val state = states[i] 395 val pageIndicator = mPageIndicators[i] 396 if (state == 0) { 397 pageIndicator.visibility = View.GONE 398 } else { 399 pageIndicator.visibility = View.VISIBLE 400 pageIndicator.setImageResource(state) 401 } 402 } 403 } 404 405 /** 406 * Display the view that creates a new timer. 407 */ showCreateTimerViewnull408 private fun showCreateTimerView(updateTypes: Int) { 409 // Stop animating the timers. 410 stopUpdatingTime() 411 412 // Show the creation view; hide the timer view. 413 mTimersView?.visibility = View.GONE 414 mCreateTimerView.visibility = View.VISIBLE 415 416 // Record the fact that the create view is visible. 417 mCurrentView = mCreateTimerView 418 419 // Update the fab and buttons. 420 updateFab(updateTypes) 421 } 422 423 /** 424 * Display the view that lists all existing timers. 425 */ showTimersViewnull426 private fun showTimersView(updateTypes: Int) { 427 // Clear any defunct timer creation state; the next timer creation starts fresh. 428 mTimerSetupState = null 429 430 // Show the timer view; hide the creation view. 431 mTimersView?.visibility = View.VISIBLE 432 mCreateTimerView.visibility = View.GONE 433 434 // Record the fact that the create view is visible. 435 mCurrentView = mTimersView 436 437 // Update the fab and buttons. 438 updateFab(updateTypes) 439 440 // Start animating the timers. 441 startUpdatingTime() 442 } 443 444 /** 445 * @param timerToRemove the timer to be removed during the animation 446 */ animateTimerRemovenull447 private fun animateTimerRemove(timerToRemove: Timer) { 448 val duration = UiDataModel.uiDataModel.shortAnimationDuration 449 450 val fadeOut: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 1f, 0f) 451 fadeOut.duration = duration 452 fadeOut.interpolator = DecelerateInterpolator() 453 fadeOut.addListener(object : AnimatorListenerAdapter() { 454 override fun onAnimationEnd(animation: Animator) { 455 DataModel.dataModel.removeTimer(timerToRemove) 456 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock) 457 } 458 }) 459 460 val fadeIn: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 0f, 1f) 461 fadeIn.duration = duration 462 fadeIn.interpolator = AccelerateInterpolator() 463 464 val animatorSet = AnimatorSet() 465 animatorSet.play(fadeOut).before(fadeIn) 466 animatorSet.start() 467 } 468 469 /** 470 * @param toView one of [.mTimersView] or [.mCreateTimerView] 471 * @param timerToRemove the timer to be removed during the animation; `null` if no timer 472 * should be removed 473 * @param animateDown `true` if the views should animate upwards, otherwise downwards 474 */ animateToViewnull475 private fun animateToView( 476 toView: View?, 477 timerToRemove: Timer?, 478 animateDown: Boolean 479 ) { 480 if (mCurrentView === toView) { 481 return 482 } 483 484 val toTimers = toView === mTimersView 485 if (toTimers) { 486 mTimersView?.visibility = View.VISIBLE 487 } else { 488 mCreateTimerView.visibility = View.VISIBLE 489 } 490 // Avoid double-taps by enabling/disabling the set of buttons active on the new view. 491 updateFab(FabContainer.BUTTONS_DISABLE) 492 493 val animationDuration = UiDataModel.uiDataModel.longAnimationDuration 494 495 val viewTreeObserver = toView!!.viewTreeObserver 496 viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { 497 override fun onPreDraw(): Boolean { 498 if (viewTreeObserver.isAlive) { 499 viewTreeObserver.removeOnPreDrawListener(this) 500 } 501 502 val view = mTimersView?.findViewById<View>(R.id.timer_time) 503 val distanceY: Float = if (view != null) view.height + view.y else 0f 504 val translationDistance = if (animateDown) distanceY else -distanceY 505 506 toView.translationY = -translationDistance 507 mCurrentView?.translationY = 0f 508 toView.alpha = 0f 509 mCurrentView?.alpha = 1f 510 511 val translateCurrent: Animator = ObjectAnimator.ofFloat(mCurrentView, 512 View.TRANSLATION_Y, translationDistance) 513 val translateNew: Animator = ObjectAnimator.ofFloat(toView, View.TRANSLATION_Y, 0f) 514 val translationAnimatorSet = AnimatorSet() 515 translationAnimatorSet.playTogether(translateCurrent, translateNew) 516 translationAnimatorSet.duration = animationDuration 517 translationAnimatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN 518 519 val fadeOutAnimator: Animator = ObjectAnimator.ofFloat(mCurrentView, View.ALPHA, 0f) 520 fadeOutAnimator.duration = animationDuration / 2 521 fadeOutAnimator.addListener(object : AnimatorListenerAdapter() { 522 override fun onAnimationStart(animation: Animator) { 523 super.onAnimationStart(animation) 524 525 // The fade-out animation and fab-shrinking animation should run together. 526 updateFab(FabContainer.FAB_AND_BUTTONS_SHRINK) 527 } 528 529 override fun onAnimationEnd(animation: Animator) { 530 super.onAnimationEnd(animation) 531 if (toTimers) { 532 showTimersView(FabContainer.FAB_AND_BUTTONS_EXPAND) 533 534 // Reset the state of the create view. 535 mCreateTimerView.reset() 536 } else { 537 showCreateTimerView(FabContainer.FAB_AND_BUTTONS_EXPAND) 538 } 539 if (timerToRemove != null) { 540 DataModel.dataModel.removeTimer(timerToRemove) 541 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock) 542 } 543 544 // Update the fab and button states now that the correct view is visible and 545 // before the animation to expand the fab and buttons starts. 546 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 547 } 548 }) 549 550 val fadeInAnimator: Animator = ObjectAnimator.ofFloat(toView, View.ALPHA, 1f) 551 fadeInAnimator.duration = animationDuration / 2 552 fadeInAnimator.startDelay = animationDuration / 2 553 554 val animatorSet = AnimatorSet() 555 animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet) 556 animatorSet.addListener(object : AnimatorListenerAdapter() { 557 override fun onAnimationEnd(animation: Animator) { 558 super.onAnimationEnd(animation) 559 mTimersView?.translationY = 0f 560 mCreateTimerView.translationY = 0f 561 mTimersView?.alpha = 1f 562 mCreateTimerView.alpha = 1f 563 } 564 }) 565 animatorSet.start() 566 567 return true 568 } 569 }) 570 } 571 hasTimersnull572 private fun hasTimers(): Boolean { 573 return mAdapter.getCount() > 0 574 } 575 576 private val timer: Timer? 577 get() { 578 if (!::mViewPager.isInitialized) { 579 return null 580 } 581 582 return if (mAdapter.getCount() == 0) { 583 null 584 } else { 585 mAdapter.getTimer(mViewPager.getCurrentItem()) 586 } 587 } 588 startUpdatingTimenull589 private fun startUpdatingTime() { 590 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 591 stopUpdatingTime() 592 mViewPager.post(mTimeUpdateRunnable) 593 } 594 stopUpdatingTimenull595 private fun stopUpdatingTime() { 596 mViewPager.removeCallbacks(mTimeUpdateRunnable) 597 } 598 599 /** 600 * Periodically refreshes the state of each timer. 601 */ 602 private inner class TimeUpdateRunnable : Runnable { runnull603 override fun run() { 604 val startTime = SystemClock.elapsedRealtime() 605 // If no timers require continuous updates, avoid scheduling the next update. 606 if (!mAdapter.updateTime()) { 607 return 608 } 609 val endTime = SystemClock.elapsedRealtime() 610 611 // Try to maintain a consistent period of time between redraws. 612 val delay = max(0, startTime + 20 - endTime) 613 mTimersView?.postDelayed(this, delay) 614 } 615 } 616 617 /** 618 * Update the page indicators and fab in response to a new timer becoming visible. 619 */ 620 private inner class TimerPageChangeListener : ViewPager.SimpleOnPageChangeListener() { onPageSelectednull621 override fun onPageSelected(position: Int) { 622 updatePageIndicators() 623 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 624 625 // Showing a new timer page may introduce a timer requiring continuous updates. 626 startUpdatingTime() 627 } 628 onPageScrollStateChangednull629 override fun onPageScrollStateChanged(state: Int) { 630 // Teasing a neighboring timer may introduce a timer requiring continuous updates. 631 if (state == ViewPager.SCROLL_STATE_DRAGGING) { 632 startUpdatingTime() 633 } 634 } 635 } 636 637 /** 638 * Update the page indicators in response to timers being added or removed. 639 * Update the fab in response to the visible timer changing. 640 */ 641 private inner class TimerWatcher : TimerListener { timerAddednull642 override fun timerAdded(timer: Timer) { 643 updatePageIndicators() 644 // If the timer is being created via this fragment avoid adjusting the fab. 645 // Timer setup view is about to be animated away in response to this timer creation. 646 // Changes to the fab immediately preceding that animation are jarring. 647 if (!mCreatingTimer) { 648 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 649 } 650 } 651 timerUpdatednull652 override fun timerUpdated(before: Timer, after: Timer) { 653 // If the timer started, animate the timers. 654 if (before.isReset && !after.isReset) { 655 startUpdatingTime() 656 } 657 658 // Fetch the index of the change. 659 val index: Int = DataModel.dataModel.timers.indexOf(after) 660 661 // If the timer just expired but is not displayed, display it now. 662 if (!before.isExpired && after.isExpired && index != mViewPager.getCurrentItem()) { 663 mViewPager.setCurrentItem(index, true) 664 } else if (mCurrentView === mTimersView && index == mViewPager.getCurrentItem()) { 665 // Morph the fab from its old state to new state if necessary. 666 if (before.state != after.state && 667 !(before.isPaused && after.isReset)) { 668 updateFab(FabContainer.FAB_MORPH) 669 } 670 } 671 } 672 timerRemovednull673 override fun timerRemoved(timer: Timer) { 674 updatePageIndicators() 675 updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) 676 677 if (mCurrentView === mTimersView && mAdapter.getCount() == 0) { 678 animateToView(mCreateTimerView, null, false) 679 } 680 } 681 } 682 683 companion object { 684 private const val EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP" 685 686 private const val KEY_TIMER_SETUP_STATE = "timer_setup_input" 687 688 /** 689 * @return an Intent that selects the timers tab with the 690 * setup screen for a new timer in place. 691 */ 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 }