@@ -31,6 +31,7 @@ import android.graphics.drawable.Drawable
31
31
import android.util.Property
32
32
import android.view.ViewGroup
33
33
import androidx.annotation.ColorInt
34
+ import androidx.annotation.FloatRange
34
35
import androidx.annotation.IdRes
35
36
import androidx.annotation.Px
36
37
import androidx.core.animation.doOnEnd
@@ -52,20 +53,31 @@ import com.materialstudies.owl.util.lerp
52
53
import com.materialstudies.owl.util.lerpArgb
53
54
import com.materialstudies.owl.util.toCornerRounding
54
55
import com.materialstudies.owl.util.transition.MaterialContainerTransitionDrawable.PROGRESS
56
+ import kotlin.math.roundToInt
55
57
56
58
@Px
57
59
private const val BITMAP_PADDING_BOTTOM = 1
58
60
private const val PROP_BOUNDS = " materialContainerTransition:bounds"
59
61
private const val PROP_BITMAP = " materialContainerTransition:bitmap"
60
62
private const val PROP_SHAPE_APPEARANCE = " materialContainerTransition:shapeAppearance"
61
- private val TRANSITION_PROPS = arrayOf(PROP_BOUNDS , PROP_BITMAP , PROP_SHAPE_APPEARANCE )
63
+ private const val PROP_CONTAINER_COLOR = " materialContainerTransition:containerColor"
64
+ private val TRANSITION_PROPS = arrayOf(
65
+ PROP_BOUNDS ,
66
+ PROP_BITMAP ,
67
+ PROP_SHAPE_APPEARANCE ,
68
+ PROP_CONTAINER_COLOR
69
+ )
62
70
63
71
/* *
64
72
* A [Transition] which implements the Material Container pattern from
65
73
* https://medium.com/google-design/motion-design-doesnt-have-to-be-hard-33089196e6c2
66
74
*/
67
75
class MaterialContainerTransition (
68
- @IdRes private val drawInId : Int = android.R .id.content
76
+ @IdRes private val drawInId : Int = android.R .id.content,
77
+ @FloatRange(from = 0.0 , fromInclusive = true , to = 1.0 , toInclusive = true )
78
+ private val crossfadeStartProgress : Float = 0.3f ,
79
+ @FloatRange(from = 0.0 , fromInclusive = true , to = 1.0 , toInclusive = true )
80
+ private val crossfadeEndProgress : Float = 0.8f
69
81
) : Transition() {
70
82
71
83
override fun getTransitionProperties () = TRANSITION_PROPS
@@ -102,12 +114,15 @@ class MaterialContainerTransition(
102
114
(startValues.values[PROP_SHAPE_APPEARANCE ] as ShapeAppearanceModel ? ).toCornerRounding(
103
115
startBounds
104
116
),
117
+ startValues.values[PROP_CONTAINER_COLOR ] as Int ,
118
+ crossfadeStartProgress,
105
119
endValues.values[PROP_BITMAP ] as Bitmap ,
106
120
endBounds,
107
121
(endValues.values[PROP_SHAPE_APPEARANCE ] as ShapeAppearanceModel ? ).toCornerRounding(
108
122
endBounds
109
123
),
110
- view.descendantBackgroundColor()
124
+ endValues.values[PROP_CONTAINER_COLOR ] as Int ,
125
+ crossfadeEndProgress
111
126
)
112
127
113
128
return ObjectAnimator .ofFloat(dr, PROGRESS , 0f , 1f ).apply {
@@ -145,7 +160,7 @@ class MaterialContainerTransition(
145
160
top + view.height
146
161
)
147
162
// Clear any foreground e.g. a ripple in progress
148
- view.foreground = null
163
+ view.jumpDrawablesToCurrentState()
149
164
// Add padding to the bitmap capture so that when we draw it later with a
150
165
// [BitmapShader] with CLAMP [TileMode], the transparency is repeated.
151
166
transitionValues.values[PROP_BITMAP ] = view.drawToBitmap(BITMAP_PADDING_BOTTOM )
@@ -168,6 +183,7 @@ class MaterialContainerTransition(
168
183
}
169
184
}
170
185
}
186
+ transitionValues.values[PROP_CONTAINER_COLOR ] = view.descendantBackgroundColor()
171
187
}
172
188
}
173
189
}
@@ -178,14 +194,22 @@ class MaterialContainerTransition(
178
194
*
179
195
* Additionally it draws a scrim over non-shared elements and a background to the container.
180
196
*/
197
+ private const val scrimAlpha = 102 // 40% opacity
198
+ private const val containerShadow = 0x1a000000
199
+ private const val containerNoShadow = 0x00000000
181
200
private class MaterialContainerTransitionDrawable (
182
201
private val startImage : Bitmap ,
183
202
private val startBounds : RectF ,
184
203
private val startRadii : CornerRounding ,
204
+ @ColorInt private val containerStartColor : Int ,
205
+ @FloatRange(from = 0.0 , fromInclusive = true , to = 1.0 , toInclusive = true )
206
+ private val crossfadeStartProgress : Float ,
185
207
private val endImage : Bitmap ,
186
208
private val endBounds : RectF ,
187
209
private val endRadii : CornerRounding ,
188
- @ColorInt containerColor : Int = 0xffffffff .toInt(),
210
+ @ColorInt private val containerEndColor : Int ,
211
+ @FloatRange(from = 0.0 , fromInclusive = true , to = 1.0 , toInclusive = true )
212
+ private val crossfadeEndProgress : Float ,
189
213
@ColorInt scrimColor : Int = 0xff000000 .toInt()
190
214
) : Drawable() {
191
215
@@ -198,7 +222,7 @@ private class MaterialContainerTransitionDrawable(
198
222
}
199
223
private val containerPaint = Paint ().apply {
200
224
style = Paint .Style .FILL
201
- color = containerColor
225
+ color = containerStartColor
202
226
}
203
227
private val currentBounds = RectF (startBounds)
204
228
private val entering = endBounds.height() > startBounds.height()
@@ -233,38 +257,56 @@ private class MaterialContainerTransitionDrawable(
233
257
}
234
258
235
259
override fun draw (canvas : Canvas ) {
236
- // Fade in/out 0–40% opaque scrim over non-shared elements
237
- // TODO make opacity configurable
260
+ // Fade in/out a scrim over non-shared elements
238
261
scrimPaint.alpha = if (entering) {
239
- lerp(0 , 102 , progress)
262
+ lerp(0 , scrimAlpha , progress)
240
263
} else {
241
- lerp(102 , 0 , progress)
264
+ lerp(scrimAlpha , 0 , progress)
242
265
}
243
266
if (scrimPaint.alpha > 0 ) canvas.drawRect(bounds, scrimPaint)
244
267
245
- // Animate corner radii changes over 0.3–0.8 of `progress` & use this when drawing the
268
+ // Animate corner radii over the crossfade range & use this when drawing the
246
269
// container background & images
247
- val cornerRadii = lerp(startRadii, endRadii, 0.3f , 0.8f , progress)
270
+ val cornerRadii = lerp(
271
+ startRadii,
272
+ endRadii,
273
+ crossfadeStartProgress,
274
+ crossfadeEndProgress,
275
+ progress
276
+ )
248
277
249
278
// Draw a background for the container, useful when the container size exceeds the image
250
279
// size which it can in large start/end size changes. Also fade in/out a shadow.
251
- // TODO make this configurable / density dependent
280
+ // TODO make radius configurable / density dependent
252
281
containerPaint.setShadowLayer(
253
282
12f , 0f , 12f ,
254
283
if (entering) {
255
- lerpArgb(0 , 0x1a000000 , progress)
284
+ lerpArgb(containerNoShadow, containerShadow , progress)
256
285
} else {
257
- lerpArgb(0x1a000000 , 0 , progress)
286
+ lerpArgb(containerShadow, containerNoShadow , progress)
258
287
}
259
288
)
289
+ containerPaint.color = lerpArgb(
290
+ containerStartColor,
291
+ containerEndColor,
292
+ crossfadeStartProgress,
293
+ crossfadeEndProgress,
294
+ progress
295
+ )
260
296
canvas.drawRoundedRect(
261
297
currentBounds,
262
298
cornerRadii,
263
299
containerPaint
264
300
)
265
301
266
- // Cross-fade images of the start/end states over 0.3–0.8 of `progress`
267
- imagePaint.alpha = lerp(255 , 0 , 0.3f , 0.8f , progress)
302
+ // Cross-fade images of the start/end states over the crossfade range
303
+ imagePaint.alpha = lerp(
304
+ 255 ,
305
+ 0 ,
306
+ crossfadeStartProgress,
307
+ crossfadeEndProgress,
308
+ progress
309
+ )
268
310
if (imagePaint.alpha > 0 ) {
269
311
imagePaint.shader = startImageShader
270
312
canvas.drawRoundedRect(
@@ -273,7 +315,13 @@ private class MaterialContainerTransitionDrawable(
273
315
imagePaint
274
316
)
275
317
}
276
- imagePaint.alpha = lerp(0 , 255 , 0.3f , 0.8f , progress)
318
+ imagePaint.alpha = lerp(
319
+ 0 ,
320
+ 255 ,
321
+ crossfadeStartProgress,
322
+ crossfadeEndProgress,
323
+ progress
324
+ )
277
325
if (imagePaint.alpha > 0 ) {
278
326
imagePaint.shader = endImageShader
279
327
canvas.drawRoundedRect(
0 commit comments