1 /*
2  * Copyright 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 androidx.glance
18 
19 import android.graphics.Bitmap
20 import android.graphics.drawable.Icon
21 import android.os.Build
22 import androidx.annotation.DrawableRes
23 import androidx.annotation.FloatRange
24 import androidx.annotation.RequiresApi
25 import androidx.annotation.RestrictTo
26 import androidx.compose.runtime.Composable
27 import androidx.glance.layout.ContentScale
28 import androidx.glance.semantics.SemanticsModifier
29 import androidx.glance.semantics.SemanticsProperties
30 import androidx.glance.semantics.contentDescription
31 import androidx.glance.semantics.semantics
32 import androidx.glance.unit.ColorProvider
33 
34 /** Interface representing an Image source which can be used with a Glance [Image] element. */
35 interface ImageProvider
36 
37 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
38 class AndroidResourceImageProvider(@DrawableRes val resId: Int) : ImageProvider {
toStringnull39     override fun toString() = "AndroidResourceImageProvider(resId=$resId)"
40 }
41 
42 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
43 class BitmapImageProvider(val bitmap: Bitmap) : ImageProvider {
44     override fun toString() =
45         "BitmapImageProvider(bitmap=Bitmap(${bitmap.width}px x ${bitmap.height}px))"
46 }
47 
48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
49 class IconImageProvider(val icon: Icon) : ImageProvider {
toStringnull50     override fun toString() = "IconImageProvider(icon=$icon)"
51 }
52 
53 /**
54  * Image resource from an Android Drawable resource.
55  *
56  * @param resId The resource ID of the Drawable resource to be used.
57  */
58 fun ImageProvider(@DrawableRes resId: Int): ImageProvider = AndroidResourceImageProvider(resId)
59 
60 /**
61  * Image resource from a bitmap.
62  *
63  * @param bitmap The bitmap to be displayed.
64  */
65 fun ImageProvider(bitmap: Bitmap): ImageProvider = BitmapImageProvider(bitmap)
66 
67 /**
68  * Image resource from an icon.
69  *
70  * @param icon The icon to be displayed.
71  */
72 @RequiresApi(Build.VERSION_CODES.M)
73 fun ImageProvider(icon: Icon): ImageProvider = IconImageProvider(icon)
74 
75 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) interface ColorFilterParams
76 
77 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
78 class TintColorFilterParams(val colorProvider: ColorProvider) : ColorFilterParams {
79     override fun toString() = "TintColorFilterParams(colorProvider=$colorProvider))"
80 }
81 
82 /** Effects used to modify the color of an image. */
83 class ColorFilter
84 internal constructor(
85     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val colorFilterParams: ColorFilterParams
86 ) {
87     companion object {
88         /**
89          * Set a tinting option for the image using the platform-specific default blending mode.
90          *
91          * @param colorProvider Provider used to get the color for blending the source content.
92          */
tintnull93         fun tint(colorProvider: ColorProvider): ColorFilter =
94             ColorFilter(TintColorFilterParams(colorProvider))
95     }
96 }
97 
98 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
99 class EmittableImage : Emittable {
100     override var modifier: GlanceModifier = GlanceModifier
101     var provider: ImageProvider? = null
102     var colorFilterParams: ColorFilterParams? = null
103     var alpha: Float? = null // null retains the source image's alpha
104     var contentScale: ContentScale = ContentScale.Fit
105 
106     override fun copy(): Emittable =
107         EmittableImage().also {
108             it.modifier = modifier
109             it.provider = provider
110             it.colorFilterParams = colorFilterParams
111             it.alpha = alpha
112             it.contentScale = contentScale
113         }
114 
115     override fun toString(): String =
116         "EmittableImage(" +
117             "modifier=$modifier, " +
118             "provider=$provider, " +
119             "colorFilterParams=$colorFilterParams, " +
120             "alpha=$alpha, " +
121             "contentScale=$contentScale" +
122             ")"
123 }
124 
125 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
isDecorativenull126 fun EmittableImage.isDecorative(): Boolean {
127     val semanticsConfiguration = modifier.findModifier<SemanticsModifier>()?.configuration
128     return semanticsConfiguration
129         ?.getOrNull(SemanticsProperties.ContentDescription)
130         ?.get(0)
131         .isNullOrEmpty()
132 }
133 
134 /**
135  * A composable which lays out and draws the image specified in [provider]. This will attempt to lay
136  * out the image using the intrinsic width and height of the provided image, but this can be
137  * overridden by using a modifier to set the width or height of this element.
138  *
139  * @param provider The image provider to use to draw the image
140  * @param contentDescription text used by accessibility services to describe what this image
141  *   represents. This should always be provided unless this image is used for decorative purposes,
142  *   and does not represent a meaningful action that a user can take. This text should be localized.
143  * @param modifier Modifier used to adjust the layout algorithm or draw decoration content.
144  * @param contentScale How to lay the image out with respect to its bounds, if the bounds are
145  *   smaller than the image.
146  * @param colorFilter The effects to use to modify the color of an image.
147  */
148 @Composable
Imagenull149 fun Image(
150     provider: ImageProvider,
151     contentDescription: String?,
152     modifier: GlanceModifier = GlanceModifier,
153     contentScale: ContentScale = ContentScale.Fit,
154     colorFilter: ColorFilter? = null,
155 ) = ImageElement(provider, contentDescription, modifier, contentScale, colorFilter, alpha = null)
156 
157 /**
158  * A composable which lays out and draws the image specified in [provider]. This will attempt to lay
159  * out the image using the intrinsic width and height of the provided image, but this can be
160  * overridden by using a modifier to set the width or height of this element.
161  *
162  * @param provider The image provider to use to draw the image
163  * @param contentDescription text used by accessibility services to describe what this image
164  *   represents. This should always be provided unless this image is used for decorative purposes,
165  *   and does not represent a meaningful action that a user can take. This text should be localized.
166  * @param alpha Opacity (0f to 1f) to apply to the image.
167  * @param modifier Modifier used to adjust the layout algorithm or draw decoration content.
168  * @param contentScale How to lay the image out with respect to its bounds, if the bounds are
169  *   smaller than the image.
170  * @param colorFilter The effects to use to modify the color of an image.
171  */
172 @Composable
173 fun Image(
174     provider: ImageProvider,
175     contentDescription: String?,
176     @FloatRange(from = 0.0, to = 1.0) alpha: Float,
177     modifier: GlanceModifier = GlanceModifier,
178     contentScale: ContentScale = ContentScale.Fit,
179     colorFilter: ColorFilter? = null,
180 ) = ImageElement(provider, contentDescription, modifier, contentScale, colorFilter, alpha)
181 
182 @Composable
183 internal fun ImageElement(
184     provider: ImageProvider,
185     contentDescription: String?,
186     modifier: GlanceModifier = GlanceModifier,
187     contentScale: ContentScale = ContentScale.Fit,
188     colorFilter: ColorFilter? = null,
189     alpha: Float? = null,
190 ) {
191     val finalModifier =
192         if (contentDescription != null) {
193             modifier.semantics { this.contentDescription = contentDescription }
194         } else {
195             modifier
196         }
197 
198     GlanceNode(
199         factory = ::EmittableImage,
200         update = {
201             this.set(provider) { this.provider = it }
202             this.set(finalModifier) { this.modifier = it }
203             this.set(contentScale) { this.contentScale = it }
204             this.set(colorFilter) { this.colorFilterParams = it?.colorFilterParams }
205             this.set(alpha) { this.alpha = it }
206         }
207     )
208 }
209