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