• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.alarms.dataadapter
18 
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.content.Context
25 import android.content.Context.VIBRATOR_SERVICE
26 import android.graphics.Color
27 import android.graphics.Rect
28 import android.graphics.drawable.Drawable
29 import android.graphics.drawable.LayerDrawable
30 import android.os.Vibrator
31 import android.view.LayoutInflater
32 import android.view.View
33 import android.view.View.TRANSLATION_Y
34 import android.view.ViewGroup
35 import android.widget.CheckBox
36 import android.widget.CompoundButton
37 import android.widget.LinearLayout
38 import android.widget.TextView
39 import androidx.core.content.ContextCompat
40 import androidx.recyclerview.widget.RecyclerView.ViewHolder
41 
42 import com.android.deskclock.AnimatorUtils
43 import com.android.deskclock.ItemAdapter.ItemViewHolder
44 import com.android.deskclock.R
45 import com.android.deskclock.ThemeUtils
46 import com.android.deskclock.Utils
47 import com.android.deskclock.alarms.AlarmTimeClickHandler
48 import com.android.deskclock.data.DataModel
49 import com.android.deskclock.events.Events
50 import com.android.deskclock.provider.Alarm
51 import com.android.deskclock.uidata.UiDataModel
52 
53 /**
54  * A ViewHolder containing views for an alarm item in expanded state.
55  */
56 class ExpandedAlarmViewHolder private constructor(itemView: View, private val mHasVibrator: Boolean)
57     : AlarmItemViewHolder(itemView) {
58     val repeat: CheckBox = itemView.findViewById(R.id.repeat_onoff) as CheckBox
59     private val editLabel: TextView = itemView.findViewById(R.id.edit_label) as TextView
60     val repeatDays: LinearLayout = itemView.findViewById(R.id.repeat_days) as LinearLayout
61     private val dayButtons: Array<CompoundButton?> = arrayOfNulls<CompoundButton>(7)
62     val vibrate: CheckBox = itemView.findViewById(R.id.vibrate_onoff) as CheckBox
63     val ringtone: TextView = itemView.findViewById(R.id.choose_ringtone) as TextView
64     val delete: TextView = itemView.findViewById(R.id.delete) as TextView
65     private val hairLine: View = itemView.findViewById(R.id.hairline)
66 
67     init {
68         val context: Context = itemView.getContext()
69         itemView.setBackground(LayerDrawable(arrayOf(
70                 ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
71                 ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground)
72         )))
73 
74         // Build button for each day.
75         val inflater: LayoutInflater = LayoutInflater.from(context)
76         val weekdays = DataModel.dataModel.weekdayOrder.calendarDays
77         for (i in 0..6) {
78             val dayButtonFrame: View = inflater.inflate(R.layout.day_button, repeatDays,
79                     false /* attachToRoot */)
80             val dayButton: CompoundButton =
81                     dayButtonFrame.findViewById(R.id.day_button_box) as CompoundButton
82             val weekday = weekdays[i]
83             dayButton.text = UiDataModel.uiDataModel.getShortWeekday(weekday)
84             dayButton.setContentDescription(UiDataModel.uiDataModel.getLongWeekday(weekday))
85             repeatDays.addView(dayButtonFrame)
86             dayButtons[i] = dayButton
87         }
88 
89         // Cannot set in xml since we need compat functionality for API < 21
90         val labelIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_label)
91         editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null)
92         val deleteIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_delete_small)
93         delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null)
94 
95         // Collapse handler
96         itemView.setOnClickListener { _ ->
97             Events.sendAlarmEvent(R.string.action_collapse_implied, R.string.label_deskclock)
98             itemHolder?.collapse()
99         }
100         arrow.setOnClickListener { _ ->
101             Events.sendAlarmEvent(R.string.action_collapse, R.string.label_deskclock)
102             itemHolder?.collapse()
103         }
104         // Edit time handler
105         clock.setOnClickListener { _ ->
106             alarmTimeClickHandler.onClockClicked(itemHolder!!.item)
107         }
108         // Edit label handler
109         editLabel.setOnClickListener { _ ->
110             alarmTimeClickHandler.onEditLabelClicked(itemHolder!!.item)
111         }
112         // Vibrator checkbox handler
113         vibrate.setOnClickListener { view ->
114             alarmTimeClickHandler.setAlarmVibrationEnabled(itemHolder!!.item,
115                     (view as CheckBox).isChecked)
116         }
117         // Ringtone editor handler
118         ringtone.setOnClickListener { _ ->
119             alarmTimeClickHandler.onRingtoneClicked(context, itemHolder!!.item)
120         }
121         // Delete alarm handler
122         delete.setOnClickListener { view ->
123             alarmTimeClickHandler.onDeleteClicked(itemHolder!!)
124             view.announceForAccessibility(context.getString(R.string.alarm_deleted))
125         }
126         // Repeat checkbox handler
127         repeat.setOnClickListener { view ->
128             val checked: Boolean = (view as CheckBox).isChecked
129             alarmTimeClickHandler.setAlarmRepeatEnabled(itemHolder!!.item, checked)
130             itemHolder?.notifyItemChanged(ANIMATE_REPEAT_DAYS)
131         }
132         // Day buttons handler
133         for (i in dayButtons.indices) {
134             dayButtons[i]?.setOnClickListener { view ->
135                 val isChecked: Boolean = (view as CompoundButton).isChecked
136                 alarmTimeClickHandler.setDayOfWeekEnabled(itemHolder!!.item, isChecked, i)
137             }
138         }
139         itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO)
140     }
141 
142     override fun onBindItemView(itemHolder: AlarmItemHolder) {
143         super.onBindItemView(itemHolder)
144 
145         val alarm = itemHolder.item
146         val alarmInstance = itemHolder.alarmInstance
147         val context: Context = itemView.getContext()
148         bindEditLabel(context, alarm)
149         bindDaysOfWeekButtons(alarm, context)
150         bindVibrator(alarm)
151         bindRingtone(context, alarm)
152         bindPreemptiveDismissButton(context, alarm, alarmInstance)
153     }
154 
155     private fun bindRingtone(context: Context, alarm: Alarm) {
156         val title = DataModel.dataModel.getRingtoneTitle(alarm.alert!!)
157         ringtone.text = title
158 
159         val description: String = context.getString(R.string.ringtone_description)
160         ringtone.setContentDescription("$description $title")
161 
162         val silent: Boolean = Utils.RINGTONE_SILENT == alarm.alert
163         val icon: Drawable? = Utils.getVectorDrawable(context,
164                 if (silent) R.drawable.ic_ringtone_silent else R.drawable.ic_ringtone)
165         ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
166     }
167 
168     private fun bindDaysOfWeekButtons(alarm: Alarm, context: Context) {
169         val weekdays = DataModel.dataModel.weekdayOrder.calendarDays
170         for (i in weekdays.indices) {
171             val dayButton: CompoundButton? = dayButtons[i]
172             dayButton?.let {
173                 if (alarm.daysOfWeek.isBitOn(weekdays[i])) {
174                     dayButton.isChecked = true
175                     dayButton.setTextColor(ThemeUtils.resolveColor(context,
176                             android.R.attr.windowBackground))
177                 } else {
178                     dayButton.isChecked = false
179                     dayButton.setTextColor(Color.WHITE)
180                 }
181             }
182         }
183         if (alarm.daysOfWeek.isRepeating) {
184             repeat.isChecked = true
185             repeatDays.visibility = View.VISIBLE
186         } else {
187             repeat.isChecked = false
188             repeatDays.visibility = View.GONE
189         }
190     }
191 
192     private fun bindEditLabel(context: Context, alarm: Alarm) {
193         editLabel.text = alarm.label
194         editLabel.contentDescription = if (!alarm.label.isNullOrEmpty()) {
195             context.getString(R.string.label_description).toString() + " " + alarm.label
196         } else {
197             context.getString(R.string.no_label_specified)
198         }
199     }
200 
201     private fun bindVibrator(alarm: Alarm) {
202         if (!mHasVibrator) {
203             vibrate.visibility = View.INVISIBLE
204         } else {
205             vibrate.visibility = View.VISIBLE
206             vibrate.isChecked = alarm.vibrate
207         }
208     }
209 
210     private val alarmTimeClickHandler: AlarmTimeClickHandler
211         get() = itemHolder!!.alarmTimeClickHandler
212 
213     override fun onAnimateChange(
214         payloads: List<Any>?,
215         fromLeft: Int,
216         fromTop: Int,
217         fromRight: Int,
218         fromBottom: Int,
219         duration: Long
220     ): Animator? {
221         if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) {
222             return null
223         }
224 
225         val isExpansion = repeatDays.getVisibility() == View.VISIBLE
226         val height: Int = repeatDays.getHeight()
227         setTranslationY(if (isExpansion) {
228             -height.toFloat()
229         } else {
230             0f
231         }, if (isExpansion) {
232             -height.toFloat()
233         } else {
234             height.toFloat()
235         })
236         repeatDays.visibility = View.VISIBLE
237         repeatDays.alpha = if (isExpansion) 0f else 1f
238 
239         val animatorSet = AnimatorSet()
240         animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView,
241                 fromLeft, fromTop, fromRight, fromBottom,
242                 itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
243                 ObjectAnimator.ofFloat(repeatDays, View.ALPHA, if (isExpansion) 1f else 0f),
244                 ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, if (isExpansion) {
245                     0f
246                 } else {
247                     -height.toFloat()
248                 }),
249                 ObjectAnimator.ofFloat(ringtone, TRANSLATION_Y, 0f),
250                 ObjectAnimator.ofFloat(vibrate, TRANSLATION_Y, 0f),
251                 ObjectAnimator.ofFloat(editLabel, TRANSLATION_Y, 0f),
252                 ObjectAnimator.ofFloat(preemptiveDismissButton, TRANSLATION_Y, 0f),
253                 ObjectAnimator.ofFloat(hairLine, TRANSLATION_Y, 0f),
254                 ObjectAnimator.ofFloat(delete, TRANSLATION_Y, 0f),
255                 ObjectAnimator.ofFloat(arrow, TRANSLATION_Y, 0f))
256         animatorSet.addListener(object : AnimatorListenerAdapter() {
257             override fun onAnimationEnd(animator: Animator?) {
258                 setTranslationY(0f, 0f)
259                 repeatDays.alpha = 1f
260                 repeatDays.visibility = if (isExpansion) View.VISIBLE else View.GONE
261                 itemView.requestLayout()
262             }
263         })
264         animatorSet.duration = duration
265         animatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
266 
267         return animatorSet
268     }
269 
270     private fun setTranslationY(repeatDaysTranslationY: Float, translationY: Float) {
271         repeatDays.setTranslationY(repeatDaysTranslationY)
272         ringtone.setTranslationY(translationY)
273         vibrate.setTranslationY(translationY)
274         editLabel.setTranslationY(translationY)
275         preemptiveDismissButton.setTranslationY(translationY)
276         hairLine.setTranslationY(translationY)
277         delete.setTranslationY(translationY)
278         arrow.setTranslationY(translationY)
279     }
280 
281     override fun onAnimateChange(
282         oldHolder: ViewHolder,
283         newHolder: ViewHolder,
284         duration: Long
285     ): Animator? {
286         if (oldHolder !is AlarmItemViewHolder ||
287                 newHolder !is AlarmItemViewHolder) {
288             return null
289         }
290 
291         val isExpanding = this == newHolder
292         AnimatorUtils.setBackgroundAlpha(itemView, if (isExpanding) 0 else 255)
293         setChangingViewsAlpha(if (isExpanding) 0f else 1f)
294 
295         val changeAnimatorSet: Animator = if (isExpanding) {
296             createExpandingAnimator(oldHolder, duration)
297         } else {
298             createCollapsingAnimator(newHolder, duration)
299         }
300         changeAnimatorSet.addListener(object : AnimatorListenerAdapter() {
301             override fun onAnimationEnd(animator: Animator?) {
302                 AnimatorUtils.setBackgroundAlpha(itemView, 255)
303                 clock.visibility = View.VISIBLE
304                 onOff.visibility = View.VISIBLE
305                 arrow.visibility = View.VISIBLE
306                 arrow.setTranslationY(0f)
307                 setChangingViewsAlpha(1f)
308                 arrow.jumpDrawablesToCurrentState()
309             }
310         })
311         return changeAnimatorSet
312     }
313 
314     private fun createCollapsingAnimator(newHolder: AlarmItemViewHolder, duration: Long): Animator {
315         arrow.visibility = View.INVISIBLE
316         clock.visibility = View.INVISIBLE
317         onOff.visibility = View.INVISIBLE
318 
319         val daysVisible = repeatDays.getVisibility() == View.VISIBLE
320         val numberOfItems = countNumberOfItems()
321 
322         val oldView: View = itemView
323         val newView: View = newHolder.itemView
324 
325         val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(oldView,
326                 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 255, 0))
327         backgroundAnimator.duration = duration
328 
329         val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
330         boundsAnimator.duration = duration
331         boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
332 
333         val shortDuration = (duration * ANIM_SHORT_DURATION_MULTIPLIER).toLong()
334         val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 0f)
335                 .setDuration(shortDuration)
336         val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 0f)
337                 .setDuration(shortDuration)
338         val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 0f)
339                 .setDuration(shortDuration)
340         val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f)
341                 .setDuration(shortDuration)
342         val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 0f)
343                 .setDuration(shortDuration)
344         val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton,
345                 View.ALPHA, 0f).setDuration(shortDuration)
346         val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 0f)
347                 .setDuration(shortDuration)
348         val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f)
349                 .setDuration(shortDuration)
350 
351         // Set the staggered delays; use the first portion (duration * (1 - 1/4 - 1/6)) of the time,
352         // so that the final animation, with a duration of 1/4 the total duration, finishes exactly
353         // before the collapsed holder begins expanding.
354         var startDelay = 0L
355         val delayIncrement = (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER).toLong() /
356                 (numberOfItems - 1)
357         deleteAnimation.setStartDelay(startDelay)
358         if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
359             startDelay += delayIncrement
360             dismissAnimation.setStartDelay(startDelay)
361         }
362         hairLineAnimation.setStartDelay(startDelay)
363         startDelay += delayIncrement
364         editLabelAnimation.setStartDelay(startDelay)
365         startDelay += delayIncrement
366         vibrateAnimation.setStartDelay(startDelay)
367         ringtoneAnimation.setStartDelay(startDelay)
368         startDelay += delayIncrement
369         if (daysVisible) {
370             repeatDaysAnimation.setStartDelay(startDelay)
371             startDelay += delayIncrement
372         }
373         repeatAnimation.setStartDelay(startDelay)
374 
375         val animatorSet = AnimatorSet()
376         animatorSet.playTogether(backgroundAnimator, boundsAnimator, repeatAnimation,
377                 repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
378                 deleteAnimation, hairLineAnimation, dismissAnimation)
379         return animatorSet
380     }
381 
382     private fun createExpandingAnimator(oldHolder: AlarmItemViewHolder, duration: Long): Animator {
383         val oldView: View = oldHolder.itemView
384         val newView: View = itemView
385         val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
386         boundsAnimator.duration = duration
387         boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
388 
389         val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(newView,
390                 PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255))
391         backgroundAnimator.duration = duration
392 
393         val oldArrow: View = oldHolder.arrow
394         val oldArrowRect = Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight())
395         val newArrowRect = Rect(0, 0, arrow.getWidth(), arrow.getHeight())
396         (newView as ViewGroup).offsetDescendantRectToMyCoords(arrow, newArrowRect)
397         (oldView as ViewGroup).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect)
398         val arrowTranslationY: Float = (oldArrowRect.bottom - newArrowRect.bottom).toFloat()
399 
400         arrow.setTranslationY(arrowTranslationY)
401         arrow.visibility = View.VISIBLE
402         clock.visibility = View.VISIBLE
403         onOff.visibility = View.VISIBLE
404 
405         val longDuration = (duration * ANIM_LONG_DURATION_MULTIPLIER).toLong()
406         val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 1f)
407                 .setDuration(longDuration)
408         val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 1f)
409                 .setDuration(longDuration)
410         val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 1f)
411                 .setDuration(longDuration)
412         val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton,
413                 View.ALPHA, 1f).setDuration(longDuration)
414         val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f)
415                 .setDuration(longDuration)
416         val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 1f)
417                 .setDuration(longDuration)
418         val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f)
419                 .setDuration(longDuration)
420         val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 1f)
421                 .setDuration(longDuration)
422         val arrowAnimation: Animator = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
423                 .setDuration(duration)
424         arrowAnimation.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
425 
426         // Set the stagger delays; delay the first by the amount of time it takes for the collapse
427         // to complete, then stagger the expansion with the remaining time.
428         var startDelay = (duration * ANIM_STANDARD_DELAY_MULTIPLIER).toLong()
429         val numberOfItems = countNumberOfItems()
430         val delayIncrement = (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER).toLong() /
431                 (numberOfItems - 1)
432         repeatAnimation.setStartDelay(startDelay)
433         startDelay += delayIncrement
434         val daysVisible = repeatDays.getVisibility() == View.VISIBLE
435         if (daysVisible) {
436             repeatDaysAnimation.setStartDelay(startDelay)
437             startDelay += delayIncrement
438         }
439         ringtoneAnimation.setStartDelay(startDelay)
440         vibrateAnimation.setStartDelay(startDelay)
441         startDelay += delayIncrement
442         editLabelAnimation.setStartDelay(startDelay)
443         startDelay += delayIncrement
444         hairLineAnimation.setStartDelay(startDelay)
445         if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
446             dismissAnimation.setStartDelay(startDelay)
447             startDelay += delayIncrement
448         }
449         deleteAnimation.setStartDelay(startDelay)
450 
451         val animatorSet = AnimatorSet()
452         animatorSet.playTogether(backgroundAnimator, repeatAnimation, boundsAnimator,
453                 repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
454                 deleteAnimation, hairLineAnimation, dismissAnimation, arrowAnimation)
455         animatorSet.addListener(object : AnimatorListenerAdapter() {
456             override fun onAnimationStart(animator: Animator?) {
457                 AnimatorUtils.startDrawableAnimation(arrow)
458             }
459         })
460         return animatorSet
461     }
462 
463     private fun countNumberOfItems(): Int {
464         // Always between 4 and 6 items.
465         var numberOfItems = 4
466         if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
467             numberOfItems++
468         }
469         if (repeatDays.getVisibility() == View.VISIBLE) {
470             numberOfItems++
471         }
472         return numberOfItems
473     }
474 
475     private fun setChangingViewsAlpha(alpha: Float) {
476         repeat.alpha = alpha
477         editLabel.alpha = alpha
478         repeatDays.alpha = alpha
479         vibrate.alpha = alpha
480         ringtone.alpha = alpha
481         hairLine.alpha = alpha
482         delete.alpha = alpha
483         preemptiveDismissButton.alpha = alpha
484     }
485 
486     class Factory(context: Context) : ItemViewHolder.Factory {
487         private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context)
488         private val mHasVibrator: Boolean =
489                 (context.getSystemService(VIBRATOR_SERVICE) as Vibrator).hasVibrator()
490 
491         override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
492             val itemView: View = mLayoutInflater.inflate(viewType, parent, false)
493             return ExpandedAlarmViewHolder(itemView, mHasVibrator)
494         }
495     }
496 
497     companion object {
498         @JvmField
499         val VIEW_TYPE: Int = R.layout.alarm_time_expanded
500     }
501 }