1 /*
2  * Copyright (C) 2022 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.settingslib.spa.framework.compose
18 
19 import android.graphics.drawable.Animatable
20 import android.graphics.drawable.BitmapDrawable
21 import android.graphics.drawable.ColorDrawable
22 import android.graphics.drawable.Drawable
23 import android.os.Build
24 import android.os.Handler
25 import android.os.Looper
26 import android.view.View
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.RememberObserver
29 import androidx.compose.runtime.getValue
30 import androidx.compose.runtime.mutableStateOf
31 import androidx.compose.runtime.remember
32 import androidx.compose.runtime.setValue
33 import androidx.compose.ui.geometry.Size
34 import androidx.compose.ui.graphics.Color
35 import androidx.compose.ui.graphics.ColorFilter
36 import androidx.compose.ui.graphics.asAndroidColorFilter
37 import androidx.compose.ui.graphics.asImageBitmap
38 import androidx.compose.ui.graphics.drawscope.DrawScope
39 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
40 import androidx.compose.ui.graphics.nativeCanvas
41 import androidx.compose.ui.graphics.painter.BitmapPainter
42 import androidx.compose.ui.graphics.painter.ColorPainter
43 import androidx.compose.ui.graphics.painter.Painter
44 import androidx.compose.ui.graphics.withSave
45 import androidx.compose.ui.unit.LayoutDirection
46 import kotlin.math.roundToInt
47 
48 /**
49  * *************************************************************************************************
50  * This file was forked from
51  * https://github.com/google/accompanist/blob/main/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt
52  * and will be removed once it lands in AndroidX.
53  */
54 
55 private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) {
56     Handler(Looper.getMainLooper())
57 }
58 
59 /**
60  * A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances
61  * should be remembered to be able to start and stop [Animatable] animations.
62  *
63  * Instances are usually retrieved from [rememberDrawablePainter].
64  */
65 class DrawablePainter(
66     val drawable: Drawable
67 ) : Painter(), RememberObserver {
68     private var drawInvalidateTick by mutableStateOf(0)
69     private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize)
70 
71     private val callback: Drawable.Callback by lazy {
72         object : Drawable.Callback {
73             override fun invalidateDrawable(d: Drawable) {
74                 // Update the tick so that we get re-drawn
75                 drawInvalidateTick++
76                 // Update our intrinsic size too
77                 drawableIntrinsicSize = drawable.intrinsicSize
78             }
79 
80             override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) {
81                 MAIN_HANDLER.postAtTime(what, time)
82             }
83 
84             override fun unscheduleDrawable(d: Drawable, what: Runnable) {
85                 MAIN_HANDLER.removeCallbacks(what)
86             }
87         }
88     }
89 
90     init {
91         if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) {
92             // Update the drawable's bounds to match the intrinsic size
93             drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
94         }
95     }
96 
97     override fun onRemembered() {
98         drawable.callback = callback
99         drawable.setVisible(true, true)
100         if (drawable is Animatable) drawable.start()
101     }
102 
103     override fun onAbandoned() = onForgotten()
104 
105     override fun onForgotten() {
106         if (drawable is Animatable) drawable.stop()
107         drawable.setVisible(false, false)
108         drawable.callback = null
109     }
110 
111     override fun applyAlpha(alpha: Float): Boolean {
112         drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255)
113         return true
114     }
115 
116     override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
117         drawable.colorFilter = colorFilter?.asAndroidColorFilter()
118         return true
119     }
120 
121     override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean {
122         if (Build.VERSION.SDK_INT >= 23) {
123             return drawable.setLayoutDirection(
124                 when (layoutDirection) {
125                     LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR
126                     LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL
127                 }
128             )
129         }
130         return false
131     }
132 
133     override val intrinsicSize: Size get() = drawableIntrinsicSize
134 
135     override fun DrawScope.onDraw() {
136         drawIntoCanvas { canvas ->
137             // Reading this ensures that we invalidate when invalidateDrawable() is called
138             drawInvalidateTick
139 
140             // Update the Drawable's bounds
141             drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
142 
143             canvas.withSave {
144                 drawable.draw(canvas.nativeCanvas)
145             }
146         }
147     }
148 }
149 
150 /**
151  * Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the
152  * drawable contents and use Compose primitives where possible.
153  *
154  * If the provided [drawable] is `null`, an empty no-op painter is returned.
155  *
156  * This function tries to dispatch lifecycle events to [drawable] as much as possible from
157  * within Compose.
158  *
159  * @sample com.google.accompanist.sample.drawablepainter.BasicSample
160  */
161 @Composable
162 fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) {
163     when (drawable) {
164         null -> EmptyPainter
165         is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap())
166         is ColorDrawable -> ColorPainter(Color(drawable.color))
167         // Since the DrawablePainter will be remembered and it implements RememberObserver, it
168         // will receive the necessary events
169         else -> DrawablePainter(drawable.mutate())
170     }
171 }
172 
173 private val Drawable.intrinsicSize: Size
174     get() = when {
175         // Only return a finite size if the drawable has an intrinsic size
176         intrinsicWidth >= 0 && intrinsicHeight >= 0 -> {
177             Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat())
178         }
179         else -> Size.Unspecified
180     }
181 
182 internal object EmptyPainter : Painter() {
183     override val intrinsicSize: Size get() = Size.Unspecified
184     override fun DrawScope.onDraw() {}
185 }
186