1 /*
<lambda>null2 * Copyright (C) 2021 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.systemui.qs.tileimpl
18
19 import android.animation.ArgbEvaluator
20 import android.animation.PropertyValuesHolder
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.ColorStateList
24 import android.content.res.Configuration
25 import android.content.res.Resources.ID_NULL
26 import android.graphics.drawable.Drawable
27 import android.graphics.drawable.RippleDrawable
28 import android.os.Trace
29 import android.service.quicksettings.Tile
30 import android.text.TextUtils
31 import android.util.Log
32 import android.util.TypedValue
33 import android.view.Gravity
34 import android.view.LayoutInflater
35 import android.view.View
36 import android.view.ViewGroup
37 import android.view.accessibility.AccessibilityEvent
38 import android.view.accessibility.AccessibilityNodeInfo
39 import android.widget.Button
40 import android.widget.ImageView
41 import android.widget.LinearLayout
42 import android.widget.Switch
43 import android.widget.TextView
44 import androidx.annotation.VisibleForTesting
45 import com.android.settingslib.Utils
46 import com.android.systemui.FontSizeUtils
47 import com.android.systemui.R
48 import com.android.systemui.animation.LaunchableView
49 import com.android.systemui.animation.LaunchableViewDelegate
50 import com.android.systemui.plugins.qs.QSIconView
51 import com.android.systemui.plugins.qs.QSTile
52 import com.android.systemui.plugins.qs.QSTile.BooleanState
53 import com.android.systemui.plugins.qs.QSTileView
54 import com.android.systemui.qs.logging.QSLogger
55 import com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH
56 import java.util.Objects
57
58 private const val TAG = "QSTileViewImpl"
59 open class QSTileViewImpl @JvmOverloads constructor(
60 context: Context,
61 private val _icon: QSIconView,
62 private val collapsed: Boolean = false
63 ) : QSTileView(context), HeightOverrideable, LaunchableView {
64
65 companion object {
66 private const val INVALID = -1
67 private const val BACKGROUND_NAME = "background"
68 private const val LABEL_NAME = "label"
69 private const val SECONDARY_LABEL_NAME = "secondaryLabel"
70 private const val CHEVRON_NAME = "chevron"
71 const val UNAVAILABLE_ALPHA = 0.3f
72 @VisibleForTesting
73 internal const val TILE_STATE_RES_PREFIX = "tile_states_"
74 }
75
76 private var _position: Int = INVALID
77
78 override fun setPosition(position: Int) {
79 _position = position
80 }
81
82 override var heightOverride: Int = HeightOverrideable.NO_OVERRIDE
83 set(value) {
84 if (field == value) return
85 field = value
86 updateHeight()
87 }
88
89 override var squishinessFraction: Float = 1f
90 set(value) {
91 if (field == value) return
92 field = value
93 updateHeight()
94 }
95
96 private val colorActive = Utils.getColorAttrDefaultColor(context, R.attr.shadeActive)
97 private val colorInactive = Utils.getColorAttrDefaultColor(context, R.attr.shadeInactive)
98 private val colorUnavailable = Utils.getColorAttrDefaultColor(context, R.attr.shadeDisabled)
99
100 private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
101 private val colorLabelInactive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
102 private val colorLabelUnavailable =
103 Utils.getColorAttrDefaultColor(context, R.attr.outline)
104
105 private val colorSecondaryLabelActive =
106 Utils.getColorAttrDefaultColor(context, R.attr.onShadeActiveVariant)
107 private val colorSecondaryLabelInactive =
108 Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactiveVariant)
109 private val colorSecondaryLabelUnavailable =
110 Utils.getColorAttrDefaultColor(context, R.attr.outline)
111
112 private lateinit var label: TextView
113 protected lateinit var secondaryLabel: TextView
114 private lateinit var labelContainer: IgnorableChildLinearLayout
115 protected lateinit var sideView: ViewGroup
116 private lateinit var customDrawableView: ImageView
117 private lateinit var chevronView: ImageView
118 private var mQsLogger: QSLogger? = null
119
120 /**
121 * Controls if tile background is set to a [RippleDrawable] see [setClickable]
122 */
123 protected var showRippleEffect = true
124
125 private lateinit var ripple: RippleDrawable
126 private lateinit var colorBackgroundDrawable: Drawable
127 private var paintColor: Int = 0
128 private val singleAnimator: ValueAnimator = ValueAnimator().apply {
129 setDuration(QS_ANIM_LENGTH)
130 addUpdateListener { animation ->
131 setAllColors(
132 // These casts will throw an exception if some property is missing. We should
133 // always have all properties.
134 animation.getAnimatedValue(BACKGROUND_NAME) as Int,
135 animation.getAnimatedValue(LABEL_NAME) as Int,
136 animation.getAnimatedValue(SECONDARY_LABEL_NAME) as Int,
137 animation.getAnimatedValue(CHEVRON_NAME) as Int
138 )
139 }
140 }
141
142 private var accessibilityClass: String? = null
143 private var stateDescriptionDeltas: CharSequence? = null
144 private var lastStateDescription: CharSequence? = null
145 private var tileState = false
146 private var lastState = INVALID
147 private val launchableViewDelegate = LaunchableViewDelegate(
148 this,
149 superSetVisibility = { super.setVisibility(it) },
150 )
151 private var lastDisabledByPolicy = false
152
153 private val locInScreen = IntArray(2)
154
155 init {
156 val typedValue = TypedValue()
157 if (!getContext().theme.resolveAttribute(R.attr.isQsTheme, typedValue, true)) {
158 throw IllegalStateException("QSViewImpl must be inflated with a theme that contains " +
159 "Theme.SystemUI.QuickSettings")
160 }
161 setId(generateViewId())
162 orientation = LinearLayout.HORIZONTAL
163 gravity = Gravity.CENTER_VERTICAL or Gravity.START
164 importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
165 clipChildren = false
166 clipToPadding = false
167 isFocusable = true
168 background = createTileBackground()
169 setColor(getBackgroundColorForState(QSTile.State.DEFAULT_STATE))
170
171 val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
172 val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
173 setPaddingRelative(startPadding, padding, padding, padding)
174
175 val iconSize = resources.getDimensionPixelSize(R.dimen.qs_icon_size)
176 addView(_icon, LayoutParams(iconSize, iconSize))
177
178 createAndAddLabels()
179 createAndAddSideView()
180 }
181
182 override fun onConfigurationChanged(newConfig: Configuration?) {
183 super.onConfigurationChanged(newConfig)
184 updateResources()
185 }
186
187 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
188 Trace.traceBegin(Trace.TRACE_TAG_APP, "QSTileViewImpl#onMeasure")
189 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
190 Trace.endSection()
191 }
192
193 override fun resetOverride() {
194 heightOverride = HeightOverrideable.NO_OVERRIDE
195 updateHeight()
196 }
197
198 fun setQsLogger(qsLogger: QSLogger) {
199 mQsLogger = qsLogger
200 }
201
202 fun updateResources() {
203 FontSizeUtils.updateFontSize(label, R.dimen.qs_tile_text_size)
204 FontSizeUtils.updateFontSize(secondaryLabel, R.dimen.qs_tile_text_size)
205
206 val iconSize = context.resources.getDimensionPixelSize(R.dimen.qs_icon_size)
207 _icon.layoutParams.apply {
208 height = iconSize
209 width = iconSize
210 }
211
212 val padding = resources.getDimensionPixelSize(R.dimen.qs_tile_padding)
213 val startPadding = resources.getDimensionPixelSize(R.dimen.qs_tile_start_padding)
214 setPaddingRelative(startPadding, padding, padding, padding)
215
216 val labelMargin = resources.getDimensionPixelSize(R.dimen.qs_label_container_margin)
217 (labelContainer.layoutParams as MarginLayoutParams).apply {
218 marginStart = labelMargin
219 }
220
221 (sideView.layoutParams as MarginLayoutParams).apply {
222 marginStart = labelMargin
223 }
224 (chevronView.layoutParams as MarginLayoutParams).apply {
225 height = iconSize
226 width = iconSize
227 }
228
229 val endMargin = resources.getDimensionPixelSize(R.dimen.qs_drawable_end_margin)
230 (customDrawableView.layoutParams as MarginLayoutParams).apply {
231 height = iconSize
232 marginEnd = endMargin
233 }
234 }
235
236 private fun createAndAddLabels() {
237 labelContainer = LayoutInflater.from(context)
238 .inflate(R.layout.qs_tile_label, this, false) as IgnorableChildLinearLayout
239 label = labelContainer.requireViewById(R.id.tile_label)
240 secondaryLabel = labelContainer.requireViewById(R.id.app_label)
241 if (collapsed) {
242 labelContainer.ignoreLastView = true
243 // Ideally, it'd be great if the parent could set this up when measuring just this child
244 // instead of the View class having to support this. However, due to the mysteries of
245 // LinearLayout's double measure pass, we cannot overwrite `measureChild` or any of its
246 // sibling methods to have special behavior for labelContainer.
247 labelContainer.forceUnspecifiedMeasure = true
248 secondaryLabel.alpha = 0f
249 }
250 setLabelColor(getLabelColorForState(QSTile.State.DEFAULT_STATE))
251 setSecondaryLabelColor(getSecondaryLabelColorForState(QSTile.State.DEFAULT_STATE))
252 addView(labelContainer)
253 }
254
255 private fun createAndAddSideView() {
256 sideView = LayoutInflater.from(context)
257 .inflate(R.layout.qs_tile_side_icon, this, false) as ViewGroup
258 customDrawableView = sideView.requireViewById(R.id.customDrawable)
259 chevronView = sideView.requireViewById(R.id.chevron)
260 setChevronColor(getChevronColorForState(QSTile.State.DEFAULT_STATE))
261 addView(sideView)
262 }
263
264 fun createTileBackground(): Drawable {
265 ripple = mContext.getDrawable(R.drawable.qs_tile_background) as RippleDrawable
266 colorBackgroundDrawable = ripple.findDrawableByLayerId(R.id.background)
267 return ripple
268 }
269
270 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
271 super.onLayout(changed, l, t, r, b)
272 updateHeight()
273 }
274
275 private fun updateHeight() {
276 val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
277 heightOverride
278 } else {
279 measuredHeight
280 }
281 // Limit how much we affect the height, so we don't have rounding artifacts when the tile
282 // is too short.
283 val constrainedSquishiness = constrainSquishiness(squishinessFraction)
284 bottom = top + (actualHeight * constrainedSquishiness).toInt()
285 scrollY = (actualHeight - height) / 2
286 }
287
288 override fun updateAccessibilityOrder(previousView: View?): View {
289 accessibilityTraversalAfter = previousView?.id ?: ID_NULL
290 return this
291 }
292
293 override fun getIcon(): QSIconView {
294 return _icon
295 }
296
297 override fun getIconWithBackground(): View {
298 return icon
299 }
300
301 override fun init(tile: QSTile) {
302 init(
303 { v: View? -> tile.click(this) },
304 { view: View? ->
305 tile.longClick(this)
306 true
307 }
308 )
309 }
310
311 private fun init(
312 click: OnClickListener?,
313 longClick: OnLongClickListener?
314 ) {
315 setOnClickListener(click)
316 onLongClickListener = longClick
317 }
318
319 override fun onStateChanged(state: QSTile.State) {
320 // We cannot use the handler here because sometimes, the views are not attached (if they
321 // are in a page that the ViewPager hasn't attached). Instead, we use a runnable where
322 // all its instances are `equal` to each other, so they can be used to remove them from the
323 // queue.
324 // This means that at any given time there's at most one enqueued runnable to change state.
325 // However, as we only ever care about the last state posted, this is fine.
326 val runnable = StateChangeRunnable(state.copy())
327 removeCallbacks(runnable)
328 post(runnable)
329 }
330
331 override fun getDetailY(): Int {
332 return top + height / 2
333 }
334
335 override fun hasOverlappingRendering(): Boolean {
336 // Avoid layers for this layout - we don't need them.
337 return false
338 }
339
340 override fun setClickable(clickable: Boolean) {
341 super.setClickable(clickable)
342 background = if (clickable && showRippleEffect) {
343 ripple.also {
344 // In case that the colorBackgroundDrawable was used as the background, make sure
345 // it has the correct callback instead of null
346 colorBackgroundDrawable.callback = it
347 }
348 } else {
349 colorBackgroundDrawable
350 }
351 }
352
353 override fun getLabelContainer(): View {
354 return labelContainer
355 }
356
357 override fun getLabel(): View {
358 return label
359 }
360
361 override fun getSecondaryLabel(): View {
362 return secondaryLabel
363 }
364
365 override fun getSecondaryIcon(): View {
366 return sideView
367 }
368
369 override fun setShouldBlockVisibilityChanges(block: Boolean) {
370 launchableViewDelegate.setShouldBlockVisibilityChanges(block)
371 }
372
373 override fun setVisibility(visibility: Int) {
374 launchableViewDelegate.setVisibility(visibility)
375 }
376
377 // Accessibility
378
379 override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
380 super.onInitializeAccessibilityEvent(event)
381 if (!TextUtils.isEmpty(accessibilityClass)) {
382 event.className = accessibilityClass
383 }
384 if (event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION &&
385 stateDescriptionDeltas != null) {
386 event.text.add(stateDescriptionDeltas)
387 stateDescriptionDeltas = null
388 }
389 }
390
391 override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
392 super.onInitializeAccessibilityNodeInfo(info)
393 // Clear selected state so it is not announce by talkback.
394 info.isSelected = false
395 info.text = if (TextUtils.isEmpty(secondaryLabel.text)) {
396 "${label.text}"
397 } else {
398 "${label.text}, ${secondaryLabel.text}"
399 }
400 if (lastDisabledByPolicy) {
401 info.addAction(
402 AccessibilityNodeInfo.AccessibilityAction(
403 AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
404 resources.getString(
405 R.string.accessibility_tile_disabled_by_policy_action_description
406 )
407 )
408 )
409 }
410 if (!TextUtils.isEmpty(accessibilityClass)) {
411 info.className = if (lastDisabledByPolicy) {
412 Button::class.java.name
413 } else {
414 accessibilityClass
415 }
416 if (Switch::class.java.name == accessibilityClass) {
417 info.isChecked = tileState
418 info.isCheckable = true
419 if (isLongClickable) {
420 info.addAction(
421 AccessibilityNodeInfo.AccessibilityAction(
422 AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
423 resources.getString(
424 R.string.accessibility_long_click_tile)))
425 }
426 }
427 }
428 if (_position != INVALID) {
429 info.collectionItemInfo =
430 AccessibilityNodeInfo.CollectionItemInfo(_position, 1, 0, 1, false)
431 }
432 }
433
434 override fun toString(): String {
435 val sb = StringBuilder(javaClass.simpleName).append('[')
436 sb.append("locInScreen=(${locInScreen[0]}, ${locInScreen[1]})")
437 sb.append(", iconView=$_icon")
438 sb.append(", tileState=$tileState")
439 sb.append("]")
440 return sb.toString()
441 }
442
443 // HANDLE STATE CHANGES RELATED METHODS
444
445 protected open fun handleStateChanged(state: QSTile.State) {
446 val allowAnimations = animationsEnabled()
447 isClickable = state.state != Tile.STATE_UNAVAILABLE
448 isLongClickable = state.handlesLongClick
449 icon.setIcon(state, allowAnimations)
450 contentDescription = state.contentDescription
451
452 // State handling and description
453 val stateDescription = StringBuilder()
454 val arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
455 val stateText = state.getStateText(arrayResId, resources)
456 state.secondaryLabel = state.getSecondaryLabel(stateText)
457 if (!TextUtils.isEmpty(stateText)) {
458 stateDescription.append(stateText)
459 }
460 if (state.disabledByPolicy && state.state != Tile.STATE_UNAVAILABLE) {
461 stateDescription.append(", ")
462 stateDescription.append(getUnavailableText(state.spec))
463 }
464 if (!TextUtils.isEmpty(state.stateDescription)) {
465 stateDescription.append(", ")
466 stateDescription.append(state.stateDescription)
467 if (lastState != INVALID && state.state == lastState &&
468 state.stateDescription != lastStateDescription) {
469 stateDescriptionDeltas = state.stateDescription
470 }
471 }
472
473 setStateDescription(stateDescription.toString())
474 lastStateDescription = state.stateDescription
475
476 accessibilityClass = if (state.state == Tile.STATE_UNAVAILABLE) {
477 null
478 } else {
479 state.expandedAccessibilityClassName
480 }
481
482 if (state is BooleanState) {
483 val newState = state.value
484 if (tileState != newState) {
485 tileState = newState
486 }
487 }
488 //
489
490 // Labels
491 if (!Objects.equals(label.text, state.label)) {
492 label.text = state.label
493 }
494 if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
495 secondaryLabel.text = state.secondaryLabel
496 secondaryLabel.visibility = if (TextUtils.isEmpty(state.secondaryLabel)) {
497 GONE
498 } else {
499 VISIBLE
500 }
501 }
502
503 // Colors
504 if (state.state != lastState || state.disabledByPolicy != lastDisabledByPolicy) {
505 singleAnimator.cancel()
506 mQsLogger?.logTileBackgroundColorUpdateIfInternetTile(
507 state.spec,
508 state.state,
509 state.disabledByPolicy,
510 getBackgroundColorForState(state.state, state.disabledByPolicy))
511 if (allowAnimations) {
512 singleAnimator.setValues(
513 colorValuesHolder(
514 BACKGROUND_NAME,
515 paintColor,
516 getBackgroundColorForState(state.state, state.disabledByPolicy)
517 ),
518 colorValuesHolder(
519 LABEL_NAME,
520 label.currentTextColor,
521 getLabelColorForState(state.state, state.disabledByPolicy)
522 ),
523 colorValuesHolder(
524 SECONDARY_LABEL_NAME,
525 secondaryLabel.currentTextColor,
526 getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
527 ),
528 colorValuesHolder(
529 CHEVRON_NAME,
530 chevronView.imageTintList?.defaultColor ?: 0,
531 getChevronColorForState(state.state, state.disabledByPolicy)
532 )
533 )
534 singleAnimator.start()
535 } else {
536 setAllColors(
537 getBackgroundColorForState(state.state, state.disabledByPolicy),
538 getLabelColorForState(state.state, state.disabledByPolicy),
539 getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
540 getChevronColorForState(state.state, state.disabledByPolicy)
541 )
542 }
543 }
544
545 // Right side icon
546 loadSideViewDrawableIfNecessary(state)
547
548 label.isEnabled = !state.disabledByPolicy
549
550 lastState = state.state
551 lastDisabledByPolicy = state.disabledByPolicy
552 }
553
554 private fun setAllColors(
555 backgroundColor: Int,
556 labelColor: Int,
557 secondaryLabelColor: Int,
558 chevronColor: Int
559 ) {
560 setColor(backgroundColor)
561 setLabelColor(labelColor)
562 setSecondaryLabelColor(secondaryLabelColor)
563 setChevronColor(chevronColor)
564 }
565
566 private fun setColor(color: Int) {
567 colorBackgroundDrawable.mutate().setTint(color)
568 paintColor = color
569 }
570
571 private fun setLabelColor(color: Int) {
572 label.setTextColor(color)
573 }
574
575 private fun setSecondaryLabelColor(color: Int) {
576 secondaryLabel.setTextColor(color)
577 }
578
579 private fun setChevronColor(color: Int) {
580 chevronView.imageTintList = ColorStateList.valueOf(color)
581 }
582
583 private fun loadSideViewDrawableIfNecessary(state: QSTile.State) {
584 if (state.sideViewCustomDrawable != null) {
585 customDrawableView.setImageDrawable(state.sideViewCustomDrawable)
586 customDrawableView.visibility = VISIBLE
587 chevronView.visibility = GONE
588 } else if (state !is BooleanState || state.forceExpandIcon) {
589 customDrawableView.setImageDrawable(null)
590 customDrawableView.visibility = GONE
591 chevronView.visibility = VISIBLE
592 } else {
593 customDrawableView.setImageDrawable(null)
594 customDrawableView.visibility = GONE
595 chevronView.visibility = GONE
596 }
597 }
598
599 private fun getUnavailableText(spec: String?): String {
600 val arrayResId = SubtitleArrayMapping.getSubtitleId(spec)
601 return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE]
602 }
603
604 /*
605 * The view should not be animated if it's not on screen and no part of it is visible.
606 */
607 protected open fun animationsEnabled(): Boolean {
608 if (!isShown) {
609 return false
610 }
611 if (alpha != 1f) {
612 return false
613 }
614 getLocationOnScreen(locInScreen)
615 return locInScreen.get(1) >= -height
616 }
617
618 private fun getBackgroundColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
619 return when {
620 state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorUnavailable
621 state == Tile.STATE_ACTIVE -> colorActive
622 state == Tile.STATE_INACTIVE -> colorInactive
623 else -> {
624 Log.e(TAG, "Invalid state $state")
625 0
626 }
627 }
628 }
629
630 private fun getLabelColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
631 return when {
632 state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorLabelUnavailable
633 state == Tile.STATE_ACTIVE -> colorLabelActive
634 state == Tile.STATE_INACTIVE -> colorLabelInactive
635 else -> {
636 Log.e(TAG, "Invalid state $state")
637 0
638 }
639 }
640 }
641
642 private fun getSecondaryLabelColorForState(state: Int, disabledByPolicy: Boolean = false): Int {
643 return when {
644 state == Tile.STATE_UNAVAILABLE || disabledByPolicy -> colorSecondaryLabelUnavailable
645 state == Tile.STATE_ACTIVE -> colorSecondaryLabelActive
646 state == Tile.STATE_INACTIVE -> colorSecondaryLabelInactive
647 else -> {
648 Log.e(TAG, "Invalid state $state")
649 0
650 }
651 }
652 }
653
654 private fun getChevronColorForState(state: Int, disabledByPolicy: Boolean = false): Int =
655 getSecondaryLabelColorForState(state, disabledByPolicy)
656
657 @VisibleForTesting
658 internal fun getCurrentColors(): List<Int> = listOf(
659 paintColor,
660 label.currentTextColor,
661 secondaryLabel.currentTextColor,
662 chevronView.imageTintList?.defaultColor ?: 0
663 )
664
665 inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
666 override fun run() {
667 handleStateChanged(state)
668 }
669
670 // We want all instances of this runnable to be equal to each other, so they can be used to
671 // remove previous instances from the Handler/RunQueue of this view
672 override fun equals(other: Any?): Boolean {
673 return other is StateChangeRunnable
674 }
675
676 // This makes sure that all instances have the same hashcode (because they are `equal`)
677 override fun hashCode(): Int {
678 return StateChangeRunnable::class.hashCode()
679 }
680 }
681 }
682
constrainSquishinessnull683 fun constrainSquishiness(squish: Float): Float {
684 return 0.1f + squish * 0.9f
685 }
686
colorValuesHoldernull687 private fun colorValuesHolder(name: String, vararg values: Int): PropertyValuesHolder {
688 return PropertyValuesHolder.ofInt(name, *values).apply {
689 setEvaluator(ArgbEvaluator.getInstance())
690 }
691 }
692