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 }