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
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.view.View
23 import android.view.animation.AccelerateInterpolator
24 import android.view.animation.DecelerateInterpolator
25 import android.view.animation.Interpolator
26 
27 import com.android.deskclock.uidata.UiDataModel
28 
29 import kotlin.math.min
30 
31 /**
32  * This runnable chooses a random initial position for [.mSaverView] within
33  * [.mContentView] if [.mSaverView] is transparent. It also schedules itself to run
34  * each minute, at which time [.mSaverView] is faded out, set to a new random location, and
35  * faded in.
36  */
37 class MoveScreensaverRunnable(
38     /** The container that houses [.mSaverView].  */
39     private val mContentView: View,
40     /** The display within the [.mContentView] that is randomly positioned.  */
41     private val mSaverView: View
42 ) : Runnable {
43     /** Accelerate the hide animation.  */
44     private val mAcceleration: Interpolator = AccelerateInterpolator()
45 
46     /** Decelerate the show animation.  */
47     private val mDeceleration: Interpolator = DecelerateInterpolator()
48 
49     /** Tracks the currently executing animation if any; used to gracefully stop the animation.  */
50     private var mActiveAnimator: Animator? = null
51 
52     /** Start or restart the random movement of the saver view within the content view. */
startnull53     fun start() {
54         // Stop any existing animations or callbacks.
55         stop()
56 
57         // Reset the alpha to 0 so saver view will be randomly positioned within the new bounds.
58         mSaverView.alpha = 0f
59 
60         // Execute the position updater runnable to choose the first random position of saver view.
61         run()
62 
63         // Schedule callbacks every minute to adjust the position of mSaverView.
64         UiDataModel.uiDataModel.addMinuteCallback(this, -FADE_TIME)
65     }
66 
67     /** Stop the random movement of the saver view within the content view. */
stopnull68     fun stop() {
69         UiDataModel.uiDataModel.removePeriodicCallback(this)
70 
71         // End any animation currently running.
72         if (mActiveAnimator != null) {
73             mActiveAnimator?.end()
74             mActiveAnimator = null
75         }
76     }
77 
runnull78     override fun run() {
79         Utils.enforceMainLooper()
80 
81         val selectInitialPosition = mSaverView.alpha == 0f
82         if (selectInitialPosition) {
83             // When selecting an initial position for the saver view the width and height of
84             // mContentView are untrustworthy if this was caused by a configuration change. To
85             // combat this, we position the mSaverView randomly within the smallest box that is
86             // guaranteed to work.
87             val smallestDim = min(mContentView.width, mContentView.height)
88             val newX = getRandomPoint(smallestDim - mSaverView.width.toFloat())
89             val newY = getRandomPoint(smallestDim - mSaverView.height.toFloat())
90 
91             mSaverView.x = newX
92             mSaverView.y = newY
93             mActiveAnimator = AnimatorUtils.getAlphaAnimator(mSaverView, 0f, 1f)
94             mActiveAnimator?.duration = FADE_TIME
95             mActiveAnimator?.interpolator = mDeceleration
96             mActiveAnimator?.start()
97         } else {
98             // Select a new random position anywhere in mContentView that will fit mSaverView.
99             val newX = getRandomPoint(mContentView.width - mSaverView.width.toFloat())
100             val newY = getRandomPoint(mContentView.height - mSaverView.height.toFloat())
101 
102             // Fade out and shrink the saver view.
103             val hide = AnimatorSet()
104             hide.duration = FADE_TIME
105             hide.interpolator = mAcceleration
106             hide.play(AnimatorUtils.getAlphaAnimator(mSaverView, 1f, 0f))
107                     .with(AnimatorUtils.getScaleAnimator(mSaverView, 1f, 0.85f))
108 
109             // Fade in and grow the saver view after altering its position.
110             val show = AnimatorSet()
111             show.duration = FADE_TIME
112             show.interpolator = mDeceleration
113             show.play(AnimatorUtils.getAlphaAnimator(mSaverView, 0f, 1f))
114                     .with(AnimatorUtils.getScaleAnimator(mSaverView, 0.85f, 1f))
115             show.addListener(object : AnimatorListenerAdapter() {
116                 override fun onAnimationStart(animation: Animator) {
117                     mSaverView.x = newX
118                     mSaverView.y = newY
119                 }
120             })
121 
122             // Execute hide followed by show.
123             val all = AnimatorSet()
124             all.play(show).after(hide)
125             mActiveAnimator = all
126             mActiveAnimator?.start()
127         }
128     }
129 
130     companion object {
131         /** The duration over which the fade in/out animations occur.  */
132         private const val FADE_TIME = 3000L
133 
134         /**
135          * @return a random integer between 0 and the `maximum` exclusive.
136          */
getRandomPointnull137         private fun getRandomPoint(maximum: Float): Float {
138             return (Math.random() * maximum).toFloat()
139         }
140     }
141 }