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