1 /*
2  * Copyright 2020 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 @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress")
18 
19 package androidx.compose.ui.layout
20 
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.geometry.Size
24 import kotlin.math.max
25 import kotlin.math.min
26 
27 /** Represents a rule to apply to scale a source rectangle to be inscribed into a destination */
28 @Stable
29 interface ContentScale {
30 
31     /**
32      * Computes the scale factor to apply to the horizontal and vertical axes independently of one
33      * another to fit the source appropriately with the given destination
34      */
computeScaleFactornull35     fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor
36 
37     /** Companion object containing commonly used [ContentScale] implementations */
38     companion object {
39 
40         /**
41          * Scale the source uniformly (maintaining the source's aspect ratio) so that both
42          * dimensions (width and height) of the source will be equal to or larger than the
43          * corresponding dimension of the destination.
44          *
45          * This [ContentScale] implementation in combination with usage of [Alignment.Center]
46          * provides similar behavior to [android.widget.ImageView.ScaleType.CENTER_CROP]
47          */
48         @Stable
49         val Crop =
50             object : ContentScale {
51                 override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
52                     computeFillMaxDimension(srcSize, dstSize).let { ScaleFactor(it, it) }
53             }
54 
55         /**
56          * Scale the source uniformly (maintaining the source's aspect ratio) so that both
57          * dimensions (width and height) of the source will be equal to or less than the
58          * corresponding dimension of the destination
59          *
60          * This [ContentScale] implementation in combination with usage of [Alignment.Center]
61          * provides similar behavior to [android.widget.ImageView.ScaleType.FIT_CENTER]
62          */
63         @Stable
64         val Fit =
65             object : ContentScale {
66                 override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
67                     computeFillMinDimension(srcSize, dstSize).let { ScaleFactor(it, it) }
68             }
69 
70         /**
71          * Scale the source maintaining the aspect ratio so that the bounds match the destination
72          * height. This can cover a larger area than the destination if the height is larger than
73          * the width.
74          */
75         @Stable
76         val FillHeight =
77             object : ContentScale {
78                 override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
79                     computeFillHeight(srcSize, dstSize).let { ScaleFactor(it, it) }
80             }
81 
82         /**
83          * Scale the source maintaining the aspect ratio so that the bounds match the destination
84          * width. This can cover a larger area than the destination if the width is larger than the
85          * height.
86          */
87         @Stable
88         val FillWidth =
89             object : ContentScale {
90                 override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
91                     computeFillWidth(srcSize, dstSize).let { ScaleFactor(it, it) }
92             }
93 
94         /**
95          * Scale the source to maintain the aspect ratio to be inside the destination bounds if the
96          * source is larger than the destination. If the source is smaller than or equal to the
97          * destination in both dimensions, this behaves similarly to [None]. This will always be
98          * contained within the bounds of the destination.
99          *
100          * This [ContentScale] implementation in combination with usage of [Alignment.Center]
101          * provides similar behavior to [android.widget.ImageView.ScaleType.CENTER_INSIDE]
102          */
103         @Stable
104         val Inside =
105             object : ContentScale {
106 
107                 override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor {
108                     return if (srcSize.width <= dstSize.width && srcSize.height <= dstSize.height) {
109                         ScaleFactor(1.0f, 1.0f)
110                     } else {
111                         computeFillMinDimension(srcSize, dstSize).let { ScaleFactor(it, it) }
112                     }
113                 }
114             }
115 
116         /** Do not apply any scaling to the source */
117         @Stable val None = FixedScale(1.0f)
118 
119         /** Scale horizontal and vertically non-uniformly to fill the destination bounds. */
120         @Stable
121         val FillBounds =
122             object : ContentScale {
123                 override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
124                     ScaleFactor(
125                         computeFillWidth(srcSize, dstSize),
126                         computeFillHeight(srcSize, dstSize)
127                     )
128             }
129     }
130 }
131 
132 /**
133  * [ContentScale] implementation that always scales the dimension by the provided fixed floating
134  * point value
135  */
136 @Immutable
137 @Suppress("DataClassDefinition")
138 data class FixedScale(val value: Float) : ContentScale {
computeScaleFactornull139     override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor =
140         ScaleFactor(value, value)
141 }
142 
143 private fun computeFillMaxDimension(srcSize: Size, dstSize: Size): Float {
144     val widthScale = computeFillWidth(srcSize, dstSize)
145     val heightScale = computeFillHeight(srcSize, dstSize)
146     return max(widthScale, heightScale)
147 }
148 
computeFillMinDimensionnull149 private fun computeFillMinDimension(srcSize: Size, dstSize: Size): Float {
150     val widthScale = computeFillWidth(srcSize, dstSize)
151     val heightScale = computeFillHeight(srcSize, dstSize)
152     return min(widthScale, heightScale)
153 }
154 
computeFillWidthnull155 private inline fun computeFillWidth(srcSize: Size, dstSize: Size): Float =
156     dstSize.width / srcSize.width
157 
158 private inline fun computeFillHeight(srcSize: Size, dstSize: Size): Float =
159     dstSize.height / srcSize.height
160