1 /*
<lambda>null2  * 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 androidx.constraintlayout.compose
18 
19 import android.graphics.Matrix
20 import androidx.compose.ui.geometry.Offset
21 import androidx.compose.ui.geometry.Size
22 import androidx.compose.ui.graphics.Color
23 import androidx.compose.ui.graphics.Path
24 import androidx.compose.ui.graphics.PathEffect
25 import androidx.compose.ui.graphics.drawscope.DrawScope
26 import androidx.compose.ui.graphics.drawscope.Stroke
27 import androidx.compose.ui.graphics.drawscope.translate
28 import androidx.compose.ui.graphics.nativeCanvas
29 import androidx.compose.ui.layout.Measurable
30 import androidx.compose.ui.layout.Placeable
31 import androidx.compose.ui.layout.layoutId
32 import androidx.compose.ui.unit.Constraints
33 import androidx.compose.ui.unit.Density
34 import androidx.compose.ui.unit.IntSize
35 import androidx.compose.ui.unit.LayoutDirection
36 import androidx.compose.ui.unit.dp
37 import androidx.compose.ui.util.fastForEach
38 import androidx.constraintlayout.core.motion.Motion
39 import androidx.constraintlayout.core.state.Dimension
40 import androidx.constraintlayout.core.state.Transition
41 import androidx.constraintlayout.core.state.WidgetFrame
42 import androidx.constraintlayout.core.widgets.Optimizer
43 
44 @ExperimentalMotionApi
45 internal class MotionMeasurer(density: Density) : Measurer2(density) {
46     private val DEBUG = false
47     private var lastProgressInInterpolation = 0f
48     val transition = Transition { with(density) { it.dp.toPx() } }
49 
50     // TODO: Explicitly declare `getDesignInfo` so that studio tooling can identify the method, also
51     //  make sure that the constraints/dimensions returned are for the start/current ConstraintSet
52 
53     private fun measureConstraintSet(
54         optimizationLevel: Int,
55         constraintSet: ConstraintSet,
56         measurables: List<Measurable>,
57         constraints: Constraints
58     ) {
59         state.reset()
60         constraintSet.applyTo(state, measurables)
61         buildMapping(state, measurables)
62         state.apply(root)
63         root.children.fastForEach { it.isAnimated = true }
64         applyRootSize(constraints)
65         root.updateHierarchy()
66 
67         if (DEBUG) {
68             root.debugName = "ConstraintLayout"
69             root.children.fastForEach { child ->
70                 child.debugName =
71                     (child.companionWidget as? Measurable)?.layoutId?.toString() ?: "NOTAG"
72             }
73         }
74 
75         root.optimizationLevel = optimizationLevel
76         // No need to set sizes and size modes as we passed them to the state above.
77         root.measure(Optimizer.OPTIMIZATION_NONE, 0, 0, 0, 0, 0, 0, 0, 0)
78     }
79 
80     @Suppress("UnavailableSymbol")
81     fun performInterpolationMeasure(
82         constraints: Constraints,
83         layoutDirection: LayoutDirection,
84         constraintSetStart: ConstraintSet,
85         constraintSetEnd: ConstraintSet,
86         @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl,
87         measurables: List<Measurable>,
88         placeableMap: MutableMap<Measurable, Placeable>, // Initialized by caller, filled by us
89         optimizationLevel: Int,
90         progress: Float,
91         compositionSource: CompositionSource,
92         invalidateOnConstraintsCallback: ShouldInvalidateCallback?
93     ): IntSize {
94         placeables = placeableMap
95         val needsRemeasure =
96             needsRemeasure(
97                 constraints = constraints,
98                 source = compositionSource,
99                 invalidateOnConstraintsCallback = invalidateOnConstraintsCallback
100             )
101 
102         if (
103             lastProgressInInterpolation != progress ||
104                 (layoutInformationReceiver?.getForcedWidth() != Int.MIN_VALUE &&
105                     layoutInformationReceiver?.getForcedHeight() != Int.MIN_VALUE) ||
106                 needsRemeasure
107         ) {
108             recalculateInterpolation(
109                 constraints = constraints,
110                 layoutDirection = layoutDirection,
111                 constraintSetStart = constraintSetStart,
112                 constraintSetEnd = constraintSetEnd,
113                 transition = transition,
114                 measurables = measurables,
115                 optimizationLevel = optimizationLevel,
116                 progress = progress,
117                 remeasure = needsRemeasure
118             )
119         }
120         oldConstraints = constraints
121         return IntSize(root.width, root.height)
122     }
123 
124     /**
125      * Nullable reference of [Constraints] used for the `invalidateOnConstraintsCallback`.
126      *
127      * Helps us to indicate when we can start calling the callback, as we need at least one measure
128      * pass to populate this reference.
129      */
130     private var oldConstraints: Constraints? = null
131 
132     /**
133      * Indicates if the layout requires measuring before computing the interpolation.
134      *
135      * This might happen if the size of MotionLayout or any of its children changed.
136      *
137      * MotionLayout size might change from its parent Layout, and in some cases the children size
138      * might change (eg: A Text layout has a longer string appended).
139      */
140     private fun needsRemeasure(
141         constraints: Constraints,
142         source: CompositionSource,
143         invalidateOnConstraintsCallback: ShouldInvalidateCallback?
144     ): Boolean {
145         if (this.transition.isEmpty || frameCache.isEmpty()) {
146             // Nothing measured (by MotionMeasurer)
147             return true
148         }
149 
150         if (oldConstraints != null && invalidateOnConstraintsCallback != null) {
151             // User is deciding when to invalidate on measuring constraints
152             if (invalidateOnConstraintsCallback(oldConstraints!!, constraints)) {
153                 return true
154             }
155         } else {
156             // Default behavior, only take this path if there's no user logic to invalidate
157             if (
158                 (constraints.hasFixedHeight && !state.sameFixedHeight(constraints.maxHeight)) ||
159                     (constraints.hasFixedWidth && !state.sameFixedWidth(constraints.maxWidth))
160             ) {
161                 // Layout size changed
162                 return true
163             }
164         }
165 
166         // Content recomposed. Or marked as such by InvalidationStrategy.onObservedStateChange.
167         return source == CompositionSource.Content
168     }
169 
170     /**
171      * Remeasures based on [constraintSetStart] and [constraintSetEnd] if needed.
172      *
173      * Runs the interpolation for the given [progress].
174      *
175      * Finally, updates the [Measurable]s dimension if they changed during interpolation.
176      */
177     private fun recalculateInterpolation(
178         constraints: Constraints,
179         layoutDirection: LayoutDirection,
180         constraintSetStart: ConstraintSet,
181         constraintSetEnd: ConstraintSet,
182         transition: TransitionImpl?,
183         measurables: List<Measurable>,
184         optimizationLevel: Int,
185         progress: Float,
186         remeasure: Boolean
187     ) {
188         lastProgressInInterpolation = progress
189         if (remeasure) {
190             this.transition.clear()
191             resetMeasureState()
192             // Define the size of the ConstraintLayout.
193             state.width(
194                 if (constraints.hasFixedWidth) {
195                     Dimension.createFixed(constraints.maxWidth)
196                 } else {
197                     Dimension.createWrap().min(constraints.minWidth)
198                 }
199             )
200             state.height(
201                 if (constraints.hasFixedHeight) {
202                     Dimension.createFixed(constraints.maxHeight)
203                 } else {
204                     Dimension.createWrap().min(constraints.minHeight)
205                 }
206             )
207             // Build constraint set and apply it to the state.
208             state.rootIncomingConstraints = constraints
209             state.isRtl = layoutDirection == LayoutDirection.Rtl
210 
211             measureConstraintSet(optimizationLevel, constraintSetStart, measurables, constraints)
212             this.transition.updateFrom(root, Transition.START)
213             measureConstraintSet(optimizationLevel, constraintSetEnd, measurables, constraints)
214             this.transition.updateFrom(root, Transition.END)
215             transition?.applyKeyFramesTo(this.transition)
216         } else {
217             // Have to remap even if there's no reason to remeasure
218             buildMapping(state, measurables)
219         }
220         this.transition.interpolate(root.width, root.height, progress)
221         root.width = this.transition.interpolatedWidth
222         root.height = this.transition.interpolatedHeight
223         // Update measurables to interpolated dimensions
224         root.children.fastForEach { child ->
225             // Update measurables to the interpolated dimensions
226             val measurable = (child.companionWidget as? Measurable) ?: return@fastForEach
227             val interpolatedFrame = this.transition.getInterpolated(child) ?: return@fastForEach
228             placeables[measurable] =
229                 measurable.measure(
230                     Constraints.fixed(interpolatedFrame.width(), interpolatedFrame.height())
231                 )
232             frameCache[measurable.anyOrNullId] = interpolatedFrame
233         }
234 
235         if (layoutInformationReceiver?.getLayoutInformationMode() == LayoutInfoFlags.BOUNDS) {
236             computeLayoutResult()
237         }
238     }
239 
240     private fun encodeKeyFrames(
241         json: StringBuilder,
242         location: FloatArray,
243         types: IntArray,
244         progress: IntArray,
245         count: Int
246     ) {
247         if (count == 0) {
248             return
249         }
250         json.append("keyTypes : [")
251         for (i in 0 until count) {
252             val m = types[i]
253             json.append(" $m,")
254         }
255         json.append("],\n")
256 
257         json.append("keyPos : [")
258         for (i in 0 until count * 2) {
259             val f = location[i]
260             json.append(" $f,")
261         }
262         json.append("],\n ")
263 
264         json.append("keyFrames : [")
265         for (i in 0 until count) {
266             val f = progress[i]
267             json.append(" $f,")
268         }
269         json.append("],\n ")
270     }
271 
272     fun encodeRoot(json: StringBuilder) {
273         json.append("  root: {")
274         json.append("interpolated: { left:  0,")
275         json.append("  top:  0,")
276         json.append("  right:   ${root.width} ,")
277         json.append("  bottom:  ${root.height} ,")
278         json.append(" } }")
279     }
280 
281     override fun computeLayoutResult() {
282         val json = StringBuilder()
283         json.append("{ ")
284         encodeRoot(json)
285         val mode = IntArray(50)
286         val pos = IntArray(50)
287         val key = FloatArray(100)
288 
289         root.children.fastForEach { child ->
290             val start = transition.getStart(child.stringId)
291             val end = transition.getEnd(child.stringId)
292             val interpolated = transition.getInterpolated(child.stringId)
293             val path = transition.getPath(child.stringId)
294             val count = transition.getKeyFrames(child.stringId, key, mode, pos)
295 
296             json.append(" ${child.stringId}: {")
297             json.append(" interpolated : ")
298             interpolated.serialize(json, true)
299 
300             json.append(", start : ")
301             start.serialize(json)
302 
303             json.append(", end : ")
304             end.serialize(json)
305             encodeKeyFrames(json, key, mode, pos, count)
306             json.append(" path : [")
307             for (point in path) {
308                 json.append(" $point ,")
309             }
310             json.append(" ] ")
311             json.append("}, ")
312         }
313         json.append(" }")
314         layoutInformationReceiver?.setLayoutInformation(json.toString())
315     }
316 
317     /**
318      * Draws debug information related to the current Transition.
319      *
320      * Typically, this means drawing the bounds of each widget at the start/end positions, the path
321      * they take and indicators for KeyPositions.
322      */
323     fun DrawScope.drawDebug(
324         drawBounds: Boolean = true,
325         drawPaths: Boolean = true,
326         drawKeyPositions: Boolean = true,
327     ) {
328         val pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
329 
330         root.children.fastForEach { child ->
331             val startFrame = transition.getStart(child)
332             val endFrame = transition.getEnd(child)
333             if (drawBounds) {
334                 // Draw widget bounds at the start and end
335                 drawFrame(frame = startFrame, pathEffect = pathEffect, color = Color.Blue)
336                 drawFrame(frame = endFrame, pathEffect = pathEffect, color = Color.Blue)
337                 translate(2f, 2f) {
338                     // Do an additional offset draw in case the bounds are not visible/obstructed
339                     drawFrame(frame = startFrame, pathEffect = pathEffect, color = Color.White)
340                     drawFrame(frame = endFrame, pathEffect = pathEffect, color = Color.White)
341                 }
342             }
343             drawPaths(
344                 parentWidth = size.width,
345                 parentHeight = size.height,
346                 startFrame = startFrame,
347                 drawPath = drawPaths,
348                 drawKeyPositions = drawKeyPositions
349             )
350         }
351     }
352 
353     private fun DrawScope.drawPaths(
354         parentWidth: Float,
355         parentHeight: Float,
356         startFrame: WidgetFrame,
357         drawPath: Boolean,
358         drawKeyPositions: Boolean
359     ) {
360         val debugRender = MotionRenderDebug(23f)
361         debugRender.basicDraw(
362             drawContext.canvas.nativeCanvas,
363             transition.getMotion(startFrame.widget.stringId),
364             1000,
365             parentWidth.toInt(),
366             parentHeight.toInt(),
367             drawPath,
368             drawKeyPositions
369         )
370     }
371 
372     private fun DrawScope.drawFrameDebug(
373         parentWidth: Float,
374         parentHeight: Float,
375         startFrame: WidgetFrame,
376         endFrame: WidgetFrame,
377         pathEffect: PathEffect,
378         color: Color
379     ) {
380         drawFrame(startFrame, pathEffect, color)
381         drawFrame(endFrame, pathEffect, color)
382         val numKeyPositions = transition.getNumberKeyPositions(startFrame)
383         val debugRender = MotionRenderDebug(23f)
384 
385         debugRender.draw(
386             drawContext.canvas.nativeCanvas,
387             transition.getMotion(startFrame.widget.stringId),
388             1000,
389             Motion.DRAW_PATH_BASIC,
390             parentWidth.toInt(),
391             parentHeight.toInt()
392         )
393         if (numKeyPositions == 0) {
394             //            drawLine(
395             //                start = Offset(startFrame.centerX(), startFrame.centerY()),
396             //                end = Offset(endFrame.centerX(), endFrame.centerY()),
397             //                color = color,
398             //                strokeWidth = 3f,
399             //                pathEffect = pathEffect
400             //            )
401         } else {
402             val x = FloatArray(numKeyPositions)
403             val y = FloatArray(numKeyPositions)
404             val pos = FloatArray(numKeyPositions)
405             transition.fillKeyPositions(startFrame, x, y, pos)
406 
407             for (i in 0..numKeyPositions - 1) {
408                 val keyFrameProgress = pos[i] / 100f
409                 val frameWidth =
410                     ((1 - keyFrameProgress) * startFrame.width()) +
411                         (keyFrameProgress * endFrame.width())
412                 val frameHeight =
413                     ((1 - keyFrameProgress) * startFrame.height()) +
414                         (keyFrameProgress * endFrame.height())
415                 val curX = x[i] * parentWidth + frameWidth / 2f
416                 val curY = y[i] * parentHeight + frameHeight / 2f
417                 //                drawLine(
418                 //                    start = Offset(prex, prey),
419                 //                    end = Offset(curX, curY),
420                 //                    color = color,
421                 //                    strokeWidth = 3f,
422                 //                    pathEffect = pathEffect
423                 //                )
424                 val path = Path()
425                 val pathSize = 20f
426                 path.moveTo(curX - pathSize, curY)
427                 path.lineTo(curX, curY + pathSize)
428                 path.lineTo(curX + pathSize, curY)
429                 path.lineTo(curX, curY - pathSize)
430                 path.close()
431 
432                 val stroke = Stroke(width = 3f)
433                 drawPath(path, color, 1f, stroke)
434             }
435             //            drawLine(
436             //                start = Offset(prex, prey),
437             //                end = Offset(endFrame.centerX(), endFrame.centerY()),
438             //                color = color,
439             //                strokeWidth = 3f,
440             //                pathEffect = pathEffect
441             //            )
442         }
443     }
444 
445     private fun DrawScope.drawFrame(frame: WidgetFrame, pathEffect: PathEffect, color: Color) {
446         if (frame.isDefaultTransform) {
447             val drawStyle = Stroke(width = 3f, pathEffect = pathEffect)
448             drawRect(
449                 color,
450                 Offset(frame.left.toFloat(), frame.top.toFloat()),
451                 Size(frame.width().toFloat(), frame.height().toFloat()),
452                 style = drawStyle
453             )
454         } else {
455             val matrix = Matrix()
456             if (!frame.rotationZ.isNaN()) {
457                 matrix.preRotate(frame.rotationZ, frame.centerX(), frame.centerY())
458             }
459             val scaleX = if (frame.scaleX.isNaN()) 1f else frame.scaleX
460             val scaleY = if (frame.scaleY.isNaN()) 1f else frame.scaleY
461             matrix.preScale(scaleX, scaleY, frame.centerX(), frame.centerY())
462             val points =
463                 floatArrayOf(
464                     frame.left.toFloat(),
465                     frame.top.toFloat(),
466                     frame.right.toFloat(),
467                     frame.top.toFloat(),
468                     frame.right.toFloat(),
469                     frame.bottom.toFloat(),
470                     frame.left.toFloat(),
471                     frame.bottom.toFloat()
472                 )
473             matrix.mapPoints(points)
474             drawLine(
475                 start = Offset(points[0], points[1]),
476                 end = Offset(points[2], points[3]),
477                 color = color,
478                 strokeWidth = 3f,
479                 pathEffect = pathEffect
480             )
481             drawLine(
482                 start = Offset(points[2], points[3]),
483                 end = Offset(points[4], points[5]),
484                 color = color,
485                 strokeWidth = 3f,
486                 pathEffect = pathEffect
487             )
488             drawLine(
489                 start = Offset(points[4], points[5]),
490                 end = Offset(points[6], points[7]),
491                 color = color,
492                 strokeWidth = 3f,
493                 pathEffect = pathEffect
494             )
495             drawLine(
496                 start = Offset(points[6], points[7]),
497                 end = Offset(points[0], points[1]),
498                 color = color,
499                 strokeWidth = 3f,
500                 pathEffect = pathEffect
501             )
502         }
503     }
504 
505     /**
506      * Calculates and returns a [Color] value of the custom property given by [name] on the
507      * ConstraintWidget corresponding to [id], the value is calculated at the given [progress] value
508      * on the current Transition.
509      *
510      * Returns [Color.Unspecified] if the custom property doesn't exist.
511      */
512     fun getCustomColor(id: String, name: String, progress: Float): Color {
513         if (!transition.contains(id)) {
514             return Color.Unspecified
515         }
516         transition.interpolate(root.width, root.height, progress)
517 
518         val interpolatedFrame = transition.getInterpolated(id)
519 
520         if (!interpolatedFrame.containsCustom(name)) {
521             return Color.Unspecified
522         }
523         return Color(interpolatedFrame.getCustomColor(name))
524     }
525 
526     /**
527      * Calculates and returns a [Float] value of the custom property given by [name] on the
528      * ConstraintWidget corresponding to [id], the value is calculated at the given [progress] value
529      * on the current Transition.
530      *
531      * Returns [Float.NaN] if the custom property doesn't exist.
532      */
533     fun getCustomFloat(id: String, name: String, progress: Float): Float {
534         if (!transition.contains(id)) {
535             return Float.NaN
536         }
537         transition.interpolate(root.width, root.height, progress)
538 
539         val interpolatedFrame = transition.getInterpolated(id)
540         return interpolatedFrame.getCustomFloat(name)
541     }
542 
543     fun clearConstraintSets() {
544         transition.clear()
545         frameCache.clear()
546     }
547 
548     @Suppress("UnavailableSymbol")
549     fun initWith(
550         start: ConstraintSet,
551         end: ConstraintSet,
552         layoutDirection: LayoutDirection,
553         @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl,
554         progress: Float
555     ) {
556         clearConstraintSets()
557 
558         state.isRtl = layoutDirection == LayoutDirection.Rtl
559         start.applyTo(state, emptyList())
560         start.applyTo(this.transition, Transition.START)
561         state.apply(root)
562         this.transition.updateFrom(root, Transition.START)
563 
564         start.applyTo(state, emptyList())
565         end.applyTo(this.transition, Transition.END)
566         state.apply(root)
567         this.transition.updateFrom(root, Transition.END)
568 
569         this.transition.interpolate(0, 0, progress)
570         transition.applyAllTo(this.transition)
571     }
572 }
573 
574 /**
575  * Functional interface to represent the callback of type `(old: Constraints, new: Constraints) ->
576  * Boolean`
577  */
578 internal fun interface ShouldInvalidateCallback {
invokenull579     operator fun invoke(old: Constraints, new: Constraints): Boolean
580 }
581