/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deskclock.alarms.dataadapter import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.PropertyValuesHolder import android.content.Context import android.content.Context.VIBRATOR_SERVICE import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.os.Vibrator import android.view.LayoutInflater import android.view.View import android.view.View.TRANSLATION_Y import android.view.ViewGroup import android.widget.CheckBox import android.widget.CompoundButton import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.android.deskclock.AnimatorUtils import com.android.deskclock.ItemAdapter.ItemViewHolder import com.android.deskclock.R import com.android.deskclock.ThemeUtils import com.android.deskclock.Utils import com.android.deskclock.alarms.AlarmTimeClickHandler import com.android.deskclock.data.DataModel import com.android.deskclock.events.Events import com.android.deskclock.provider.Alarm import com.android.deskclock.uidata.UiDataModel /** * A ViewHolder containing views for an alarm item in expanded state. */ class ExpandedAlarmViewHolder private constructor(itemView: View, private val mHasVibrator: Boolean) : AlarmItemViewHolder(itemView) { val repeat: CheckBox = itemView.findViewById(R.id.repeat_onoff) as CheckBox private val editLabel: TextView = itemView.findViewById(R.id.edit_label) as TextView val repeatDays: LinearLayout = itemView.findViewById(R.id.repeat_days) as LinearLayout private val dayButtons: Array = arrayOfNulls(7) val vibrate: CheckBox = itemView.findViewById(R.id.vibrate_onoff) as CheckBox val ringtone: TextView = itemView.findViewById(R.id.choose_ringtone) as TextView val delete: TextView = itemView.findViewById(R.id.delete) as TextView private val hairLine: View = itemView.findViewById(R.id.hairline) init { val context: Context = itemView.getContext() itemView.setBackground(LayerDrawable(arrayOf( ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded), ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground) ))) // Build button for each day. val inflater: LayoutInflater = LayoutInflater.from(context) val weekdays = DataModel.dataModel.weekdayOrder.calendarDays for (i in 0..6) { val dayButtonFrame: View = inflater.inflate(R.layout.day_button, repeatDays, false /* attachToRoot */) val dayButton: CompoundButton = dayButtonFrame.findViewById(R.id.day_button_box) as CompoundButton val weekday = weekdays[i] dayButton.text = UiDataModel.uiDataModel.getShortWeekday(weekday) dayButton.setContentDescription(UiDataModel.uiDataModel.getLongWeekday(weekday)) repeatDays.addView(dayButtonFrame) dayButtons[i] = dayButton } // Cannot set in xml since we need compat functionality for API < 21 val labelIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_label) editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null) val deleteIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_delete_small) delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null) // Collapse handler itemView.setOnClickListener { _ -> Events.sendAlarmEvent(R.string.action_collapse_implied, R.string.label_deskclock) itemHolder?.collapse() } arrow.setOnClickListener { _ -> Events.sendAlarmEvent(R.string.action_collapse, R.string.label_deskclock) itemHolder?.collapse() } // Edit time handler clock.setOnClickListener { _ -> alarmTimeClickHandler.onClockClicked(itemHolder!!.item) } // Edit label handler editLabel.setOnClickListener { _ -> alarmTimeClickHandler.onEditLabelClicked(itemHolder!!.item) } // Vibrator checkbox handler vibrate.setOnClickListener { view -> alarmTimeClickHandler.setAlarmVibrationEnabled(itemHolder!!.item, (view as CheckBox).isChecked) } // Ringtone editor handler ringtone.setOnClickListener { _ -> alarmTimeClickHandler.onRingtoneClicked(context, itemHolder!!.item) } // Delete alarm handler delete.setOnClickListener { view -> alarmTimeClickHandler.onDeleteClicked(itemHolder!!) view.announceForAccessibility(context.getString(R.string.alarm_deleted)) } // Repeat checkbox handler repeat.setOnClickListener { view -> val checked: Boolean = (view as CheckBox).isChecked alarmTimeClickHandler.setAlarmRepeatEnabled(itemHolder!!.item, checked) itemHolder?.notifyItemChanged(ANIMATE_REPEAT_DAYS) } // Day buttons handler for (i in dayButtons.indices) { dayButtons[i]?.setOnClickListener { view -> val isChecked: Boolean = (view as CompoundButton).isChecked alarmTimeClickHandler.setDayOfWeekEnabled(itemHolder!!.item, isChecked, i) } } itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO) } override fun onBindItemView(itemHolder: AlarmItemHolder) { super.onBindItemView(itemHolder) val alarm = itemHolder.item val alarmInstance = itemHolder.alarmInstance val context: Context = itemView.getContext() bindEditLabel(context, alarm) bindDaysOfWeekButtons(alarm, context) bindVibrator(alarm) bindRingtone(context, alarm) bindPreemptiveDismissButton(context, alarm, alarmInstance) } private fun bindRingtone(context: Context, alarm: Alarm) { val title = DataModel.dataModel.getRingtoneTitle(alarm.alert!!) ringtone.text = title val description: String = context.getString(R.string.ringtone_description) ringtone.setContentDescription("$description $title") val silent: Boolean = Utils.RINGTONE_SILENT == alarm.alert val icon: Drawable? = Utils.getVectorDrawable(context, if (silent) R.drawable.ic_ringtone_silent else R.drawable.ic_ringtone) ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) } private fun bindDaysOfWeekButtons(alarm: Alarm, context: Context) { val weekdays = DataModel.dataModel.weekdayOrder.calendarDays for (i in weekdays.indices) { val dayButton: CompoundButton? = dayButtons[i] dayButton?.let { if (alarm.daysOfWeek.isBitOn(weekdays[i])) { dayButton.isChecked = true dayButton.setTextColor(ThemeUtils.resolveColor(context, android.R.attr.windowBackground)) } else { dayButton.isChecked = false dayButton.setTextColor(Color.WHITE) } } } if (alarm.daysOfWeek.isRepeating) { repeat.isChecked = true repeatDays.visibility = View.VISIBLE } else { repeat.isChecked = false repeatDays.visibility = View.GONE } } private fun bindEditLabel(context: Context, alarm: Alarm) { editLabel.text = alarm.label editLabel.contentDescription = if (!alarm.label.isNullOrEmpty()) { context.getString(R.string.label_description).toString() + " " + alarm.label } else { context.getString(R.string.no_label_specified) } } private fun bindVibrator(alarm: Alarm) { if (!mHasVibrator) { vibrate.visibility = View.INVISIBLE } else { vibrate.visibility = View.VISIBLE vibrate.isChecked = alarm.vibrate } } private val alarmTimeClickHandler: AlarmTimeClickHandler get() = itemHolder!!.alarmTimeClickHandler override fun onAnimateChange( payloads: List?, fromLeft: Int, fromTop: Int, fromRight: Int, fromBottom: Int, duration: Long ): Animator? { if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) { return null } val isExpansion = repeatDays.getVisibility() == View.VISIBLE val height: Int = repeatDays.getHeight() setTranslationY(if (isExpansion) { -height.toFloat() } else { 0f }, if (isExpansion) { -height.toFloat() } else { height.toFloat() }) repeatDays.visibility = View.VISIBLE repeatDays.alpha = if (isExpansion) 0f else 1f val animatorSet = AnimatorSet() animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView, fromLeft, fromTop, fromRight, fromBottom, itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()), ObjectAnimator.ofFloat(repeatDays, View.ALPHA, if (isExpansion) 1f else 0f), ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, if (isExpansion) { 0f } else { -height.toFloat() }), ObjectAnimator.ofFloat(ringtone, TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(vibrate, TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(editLabel, TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(preemptiveDismissButton, TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(hairLine, TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(delete, TRANSLATION_Y, 0f), ObjectAnimator.ofFloat(arrow, TRANSLATION_Y, 0f)) animatorSet.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animator: Animator?) { setTranslationY(0f, 0f) repeatDays.alpha = 1f repeatDays.visibility = if (isExpansion) View.VISIBLE else View.GONE itemView.requestLayout() } }) animatorSet.duration = duration animatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN return animatorSet } private fun setTranslationY(repeatDaysTranslationY: Float, translationY: Float) { repeatDays.setTranslationY(repeatDaysTranslationY) ringtone.setTranslationY(translationY) vibrate.setTranslationY(translationY) editLabel.setTranslationY(translationY) preemptiveDismissButton.setTranslationY(translationY) hairLine.setTranslationY(translationY) delete.setTranslationY(translationY) arrow.setTranslationY(translationY) } override fun onAnimateChange( oldHolder: ViewHolder, newHolder: ViewHolder, duration: Long ): Animator? { if (oldHolder !is AlarmItemViewHolder || newHolder !is AlarmItemViewHolder) { return null } val isExpanding = this == newHolder AnimatorUtils.setBackgroundAlpha(itemView, if (isExpanding) 0 else 255) setChangingViewsAlpha(if (isExpanding) 0f else 1f) val changeAnimatorSet: Animator = if (isExpanding) { createExpandingAnimator(oldHolder, duration) } else { createCollapsingAnimator(newHolder, duration) } changeAnimatorSet.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animator: Animator?) { AnimatorUtils.setBackgroundAlpha(itemView, 255) clock.visibility = View.VISIBLE onOff.visibility = View.VISIBLE arrow.visibility = View.VISIBLE arrow.setTranslationY(0f) setChangingViewsAlpha(1f) arrow.jumpDrawablesToCurrentState() } }) return changeAnimatorSet } private fun createCollapsingAnimator(newHolder: AlarmItemViewHolder, duration: Long): Animator { arrow.visibility = View.INVISIBLE clock.visibility = View.INVISIBLE onOff.visibility = View.INVISIBLE val daysVisible = repeatDays.getVisibility() == View.VISIBLE val numberOfItems = countNumberOfItems() val oldView: View = itemView val newView: View = newHolder.itemView val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(oldView, PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 255, 0)) backgroundAnimator.duration = duration val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView) boundsAnimator.duration = duration boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN val shortDuration = (duration * ANIM_SHORT_DURATION_MULTIPLIER).toLong() val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 0f) .setDuration(shortDuration) val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 0f) .setDuration(shortDuration) val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 0f) .setDuration(shortDuration) val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f) .setDuration(shortDuration) val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 0f) .setDuration(shortDuration) val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 0f).setDuration(shortDuration) val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 0f) .setDuration(shortDuration) val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f) .setDuration(shortDuration) // Set the staggered delays; use the first portion (duration * (1 - 1/4 - 1/6)) of the time, // so that the final animation, with a duration of 1/4 the total duration, finishes exactly // before the collapsed holder begins expanding. var startDelay = 0L val delayIncrement = (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER).toLong() / (numberOfItems - 1) deleteAnimation.setStartDelay(startDelay) if (preemptiveDismissButton.getVisibility() == View.VISIBLE) { startDelay += delayIncrement dismissAnimation.setStartDelay(startDelay) } hairLineAnimation.setStartDelay(startDelay) startDelay += delayIncrement editLabelAnimation.setStartDelay(startDelay) startDelay += delayIncrement vibrateAnimation.setStartDelay(startDelay) ringtoneAnimation.setStartDelay(startDelay) startDelay += delayIncrement if (daysVisible) { repeatDaysAnimation.setStartDelay(startDelay) startDelay += delayIncrement } repeatAnimation.setStartDelay(startDelay) val animatorSet = AnimatorSet() animatorSet.playTogether(backgroundAnimator, boundsAnimator, repeatAnimation, repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation, deleteAnimation, hairLineAnimation, dismissAnimation) return animatorSet } private fun createExpandingAnimator(oldHolder: AlarmItemViewHolder, duration: Long): Animator { val oldView: View = oldHolder.itemView val newView: View = itemView val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView) boundsAnimator.duration = duration boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(newView, PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255)) backgroundAnimator.duration = duration val oldArrow: View = oldHolder.arrow val oldArrowRect = Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight()) val newArrowRect = Rect(0, 0, arrow.getWidth(), arrow.getHeight()) (newView as ViewGroup).offsetDescendantRectToMyCoords(arrow, newArrowRect) (oldView as ViewGroup).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect) val arrowTranslationY: Float = (oldArrowRect.bottom - newArrowRect.bottom).toFloat() arrow.setTranslationY(arrowTranslationY) arrow.visibility = View.VISIBLE clock.visibility = View.VISIBLE onOff.visibility = View.VISIBLE val longDuration = (duration * ANIM_LONG_DURATION_MULTIPLIER).toLong() val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 1f) .setDuration(longDuration) val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 1f) .setDuration(longDuration) val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 1f) .setDuration(longDuration) val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 1f).setDuration(longDuration) val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f) .setDuration(longDuration) val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 1f) .setDuration(longDuration) val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f) .setDuration(longDuration) val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 1f) .setDuration(longDuration) val arrowAnimation: Animator = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f) .setDuration(duration) arrowAnimation.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN // Set the stagger delays; delay the first by the amount of time it takes for the collapse // to complete, then stagger the expansion with the remaining time. var startDelay = (duration * ANIM_STANDARD_DELAY_MULTIPLIER).toLong() val numberOfItems = countNumberOfItems() val delayIncrement = (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER).toLong() / (numberOfItems - 1) repeatAnimation.setStartDelay(startDelay) startDelay += delayIncrement val daysVisible = repeatDays.getVisibility() == View.VISIBLE if (daysVisible) { repeatDaysAnimation.setStartDelay(startDelay) startDelay += delayIncrement } ringtoneAnimation.setStartDelay(startDelay) vibrateAnimation.setStartDelay(startDelay) startDelay += delayIncrement editLabelAnimation.setStartDelay(startDelay) startDelay += delayIncrement hairLineAnimation.setStartDelay(startDelay) if (preemptiveDismissButton.getVisibility() == View.VISIBLE) { dismissAnimation.setStartDelay(startDelay) startDelay += delayIncrement } deleteAnimation.setStartDelay(startDelay) val animatorSet = AnimatorSet() animatorSet.playTogether(backgroundAnimator, repeatAnimation, boundsAnimator, repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation, deleteAnimation, hairLineAnimation, dismissAnimation, arrowAnimation) animatorSet.addListener(object : AnimatorListenerAdapter() { override fun onAnimationStart(animator: Animator?) { AnimatorUtils.startDrawableAnimation(arrow) } }) return animatorSet } private fun countNumberOfItems(): Int { // Always between 4 and 6 items. var numberOfItems = 4 if (preemptiveDismissButton.getVisibility() == View.VISIBLE) { numberOfItems++ } if (repeatDays.getVisibility() == View.VISIBLE) { numberOfItems++ } return numberOfItems } private fun setChangingViewsAlpha(alpha: Float) { repeat.alpha = alpha editLabel.alpha = alpha repeatDays.alpha = alpha vibrate.alpha = alpha ringtone.alpha = alpha hairLine.alpha = alpha delete.alpha = alpha preemptiveDismissButton.alpha = alpha } class Factory(context: Context) : ItemViewHolder.Factory { private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context) private val mHasVibrator: Boolean = (context.getSystemService(VIBRATOR_SERVICE) as Vibrator).hasVibrator() override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> { val itemView: View = mLayoutInflater.inflate(viewType, parent, false) return ExpandedAlarmViewHolder(itemView, mHasVibrator) } } companion object { @JvmField val VIEW_TYPE: Int = R.layout.alarm_time_expanded } }