1 /*
2  * Copyright (C) 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 package androidx.constraintlayout.compose
17 
18 import android.graphics.Canvas
19 import android.graphics.DashPathEffect
20 import android.graphics.Paint
21 import android.graphics.Path
22 import android.graphics.Rect
23 import androidx.constraintlayout.core.motion.Motion
24 import androidx.constraintlayout.core.motion.MotionPaths
25 
26 internal class MotionRenderDebug(textSize: Float) {
27     var mPoints: FloatArray? = null
28     var mPathMode: IntArray
29     var mKeyFramePoints: FloatArray
30     var mPath: Path? = null
31     var mPaint: Paint
32     var mPaintKeyframes: Paint
33     var mPaintGraph: Paint
34     var mTextPaint: Paint
35     var mFillPaint: Paint
36     private val mRectangle: FloatArray
37     val mRedColor = -0x55cd
38     val mKeyframeColor = -0x1f8a66
39     val mGraphColor = -0xcc5600
40     val mShadowColor = 0x77000000
41     val mDiamondSize = 10
42     var mDashPathEffect: DashPathEffect
43     var mKeyFrameCount = 0
44     var mBounds = Rect()
45     var mPresentationMode = false
46     var mShadowTranslate = 1
47 
48     init {
49         mPaint = Paint()
50         mPaint.isAntiAlias = true
51         mPaint.color = mRedColor
52         mPaint.strokeWidth = 2f
53         mPaint.style = Paint.Style.STROKE
54         mPaintKeyframes = Paint()
55         mPaintKeyframes.isAntiAlias = true
56         mPaintKeyframes.color = mKeyframeColor
57         mPaintKeyframes.strokeWidth = 2f
58         mPaintKeyframes.style = Paint.Style.STROKE
59         mPaintGraph = Paint()
60         mPaintGraph.isAntiAlias = true
61         mPaintGraph.color = mGraphColor
62         mPaintGraph.strokeWidth = 2f
63         mPaintGraph.style = Paint.Style.STROKE
64         mTextPaint = Paint()
65         mTextPaint.isAntiAlias = true
66         mTextPaint.color = mGraphColor
67         mTextPaint.textSize = textSize
68         mRectangle = FloatArray(8)
69         mFillPaint = Paint()
70         mFillPaint.isAntiAlias = true
71         mDashPathEffect = DashPathEffect(floatArrayOf(4f, 8f), 0f)
72         mPaintGraph.pathEffect = mDashPathEffect
73         mKeyFramePoints = FloatArray(MAX_KEY_FRAMES * 2)
74         mPathMode = IntArray(MAX_KEY_FRAMES)
75         if (mPresentationMode) {
76             mPaint.strokeWidth = 8f
77             mFillPaint.strokeWidth = 8f
78             mPaintKeyframes.strokeWidth = 8f
79             mShadowTranslate = 4
80         }
81     }
82 
drawnull83     fun draw(
84         canvas: Canvas,
85         frameArrayList: HashMap<String?, Motion>?,
86         duration: Int,
87         debugPath: Int,
88         layoutWidth: Int,
89         layoutHeight: Int
90     ) {
91         if (frameArrayList == null || frameArrayList.size == 0) {
92             return
93         }
94         canvas.save()
95         for (motionController in frameArrayList.values) {
96             draw(canvas, motionController, duration, debugPath, layoutWidth, layoutHeight)
97         }
98         canvas.restore()
99     }
100 
drawnull101     fun draw(
102         canvas: Canvas,
103         motionController: Motion,
104         duration: Int,
105         debugPath: Int,
106         layoutWidth: Int,
107         layoutHeight: Int
108     ) {
109         var mode = motionController.drawPath
110         if (debugPath > 0 && mode == Motion.DRAW_PATH_NONE) {
111             mode = Motion.DRAW_PATH_BASIC
112         }
113         if (mode == Motion.DRAW_PATH_NONE) { // do not draw path
114             return
115         }
116         mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode, null)
117         if (mode >= Motion.DRAW_PATH_BASIC) {
118             val frames = duration / DEBUG_PATH_TICKS_PER_MS
119             if (mPoints == null || mPoints!!.size != frames * 2) {
120                 mPoints = FloatArray(frames * 2)
121                 mPath = Path()
122             }
123             canvas.translate(mShadowTranslate.toFloat(), mShadowTranslate.toFloat())
124             mPaint.color = mShadowColor
125             mFillPaint.color = mShadowColor
126             mPaintKeyframes.color = mShadowColor
127             mPaintGraph.color = mShadowColor
128             motionController.buildPath(mPoints, frames)
129             drawAll(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight)
130             mPaint.color = mRedColor
131             mPaintKeyframes.color = mKeyframeColor
132             mFillPaint.color = mKeyframeColor
133             mPaintGraph.color = mGraphColor
134             canvas.translate(-mShadowTranslate.toFloat(), -mShadowTranslate.toFloat())
135             drawAll(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight)
136             if (mode == Motion.DRAW_PATH_RECTANGLE) {
137                 drawRectangle(canvas, motionController)
138             }
139         }
140     }
141 
drawAllnull142     fun drawAll(
143         canvas: Canvas,
144         mode: Int,
145         keyFrames: Int,
146         motionController: Motion,
147         layoutWidth: Int,
148         layoutHeight: Int
149     ) {
150         if (mode == Motion.DRAW_PATH_AS_CONFIGURED) {
151             drawPathAsConfigured(canvas)
152         }
153         if (mode == Motion.DRAW_PATH_RELATIVE) {
154             drawPathRelative(canvas)
155         }
156         if (mode == Motion.DRAW_PATH_CARTESIAN) {
157             drawPathCartesian(canvas)
158         }
159         drawBasicPath(canvas)
160         drawTicks(canvas, mode, keyFrames, motionController, layoutWidth, layoutHeight)
161     }
162 
163     /**
164      * Draws the paths of the given [motionController][Motion], forcing the drawing mode
165      * [Motion.DRAW_PATH_BASIC].
166      *
167      * @param canvas Canvas instance used to draw on
168      * @param motionController Controller containing path information
169      * @param duration Defined in milliseconds, sets the amount of ticks used to draw the path based
170      *   on [.DEBUG_PATH_TICKS_PER_MS]
171      * @param layoutWidth Width of the containing MotionLayout
172      * @param layoutHeight Height of the containing MotionLayout
173      * @param drawPath Whether to draw the path, paths are drawn using dashed lines
174      * @param drawTicks Whether to draw diamond shaped ticks that indicate KeyPositions along a path
175      */
basicDrawnull176     fun basicDraw(
177         canvas: Canvas,
178         motionController: Motion,
179         duration: Int,
180         layoutWidth: Int,
181         layoutHeight: Int,
182         drawPath: Boolean,
183         drawTicks: Boolean
184     ) {
185         val mode = Motion.DRAW_PATH_BASIC
186         mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode, null)
187         val frames = duration / DEBUG_PATH_TICKS_PER_MS
188         if (mPoints == null || mPoints!!.size != frames * 2) {
189             mPoints = FloatArray(frames * 2)
190             mPath = Path()
191         }
192         canvas.translate(mShadowTranslate.toFloat(), mShadowTranslate.toFloat())
193         mPaint.color = mShadowColor
194         mFillPaint.color = mShadowColor
195         mPaintKeyframes.color = mShadowColor
196         mPaintGraph.color = mShadowColor
197         motionController.buildPath(mPoints, frames)
198         if (drawPath) {
199             drawBasicPath(canvas)
200         }
201         if (drawTicks) {
202             drawTicks(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight)
203         }
204         mPaint.color = mRedColor
205         mPaintKeyframes.color = mKeyframeColor
206         mFillPaint.color = mKeyframeColor
207         mPaintGraph.color = mGraphColor
208         canvas.translate(-mShadowTranslate.toFloat(), -mShadowTranslate.toFloat())
209         if (drawPath) {
210             drawBasicPath(canvas)
211         }
212         if (drawTicks) {
213             drawTicks(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight)
214         }
215     }
216 
drawBasicPathnull217     private fun drawBasicPath(canvas: Canvas) {
218         canvas.drawLines(mPoints!!, mPaint)
219     }
220 
drawTicksnull221     private fun drawTicks(
222         canvas: Canvas,
223         mode: Int,
224         keyFrames: Int,
225         motionController: Motion,
226         layoutWidth: Int,
227         layoutHeight: Int
228     ) {
229         var viewWidth = 0
230         var viewHeight = 0
231         if (motionController.view != null) {
232             viewWidth = motionController.view.width
233             viewHeight = motionController.view.height
234         }
235         for (i in 1 until keyFrames - 1) {
236             if (
237                 mode == Motion.DRAW_PATH_AS_CONFIGURED && mPathMode[i - 1] == Motion.DRAW_PATH_NONE
238             ) {
239                 continue
240             }
241             val x = mKeyFramePoints[i * 2]
242             val y = mKeyFramePoints[i * 2 + 1]
243             mPath!!.reset()
244             mPath!!.moveTo(x, y + mDiamondSize)
245             mPath!!.lineTo(x + mDiamondSize, y)
246             mPath!!.lineTo(x, y - mDiamondSize)
247             mPath!!.lineTo(x - mDiamondSize, y)
248             mPath!!.close()
249             val dx = 0f // framePoint.translationX
250             val dy = 0f // framePoint.translationY
251             if (mode == Motion.DRAW_PATH_AS_CONFIGURED) {
252                 if (mPathMode[i - 1] == MotionPaths.PERPENDICULAR) {
253                     drawPathRelativeTicks(canvas, x - dx, y - dy)
254                 } else if (mPathMode[i - 1] == MotionPaths.CARTESIAN) {
255                     drawPathCartesianTicks(canvas, x - dx, y - dy)
256                 } else if (mPathMode[i - 1] == MotionPaths.SCREEN) {
257                     drawPathScreenTicks(
258                         canvas,
259                         x - dx,
260                         y - dy,
261                         viewWidth,
262                         viewHeight,
263                         layoutWidth,
264                         layoutHeight
265                     )
266                 }
267                 canvas.drawPath(mPath!!, mFillPaint)
268             }
269             if (mode == Motion.DRAW_PATH_RELATIVE) {
270                 drawPathRelativeTicks(canvas, x - dx, y - dy)
271             }
272             if (mode == Motion.DRAW_PATH_CARTESIAN) {
273                 drawPathCartesianTicks(canvas, x - dx, y - dy)
274             }
275             if (mode == Motion.DRAW_PATH_SCREEN) {
276                 drawPathScreenTicks(
277                     canvas,
278                     x - dx,
279                     y - dy,
280                     viewWidth,
281                     viewHeight,
282                     layoutWidth,
283                     layoutHeight
284                 )
285             }
286             if (dx != 0f || dy != 0f) {
287                 drawTranslation(canvas, x - dx, y - dy, x, y)
288             } else {
289                 canvas.drawPath(mPath!!, mFillPaint)
290             }
291         }
292         if (mPoints!!.size > 1) {
293             // Draw the starting and ending circle
294             canvas.drawCircle(mPoints!![0], mPoints!![1], 8f, mPaintKeyframes)
295             canvas.drawCircle(
296                 mPoints!![mPoints!!.size - 2],
297                 mPoints!![mPoints!!.size - 1],
298                 8f,
299                 mPaintKeyframes
300             )
301         }
302     }
303 
drawTranslationnull304     private fun drawTranslation(canvas: Canvas, x1: Float, y1: Float, x2: Float, y2: Float) {
305         canvas.drawRect(x1, y1, x2, y2, mPaintGraph)
306         canvas.drawLine(x1, y1, x2, y2, mPaintGraph)
307     }
308 
drawPathRelativenull309     private fun drawPathRelative(canvas: Canvas) {
310         canvas.drawLine(
311             mPoints!![0],
312             mPoints!![1],
313             mPoints!![mPoints!!.size - 2],
314             mPoints!![mPoints!!.size - 1],
315             mPaintGraph
316         )
317     }
318 
drawPathAsConfigurednull319     private fun drawPathAsConfigured(canvas: Canvas) {
320         var path = false
321         var cart = false
322         for (i in 0 until mKeyFrameCount) {
323             if (mPathMode[i] == MotionPaths.PERPENDICULAR) {
324                 path = true
325             }
326             if (mPathMode[i] == MotionPaths.CARTESIAN) {
327                 cart = true
328             }
329         }
330         if (path) {
331             drawPathRelative(canvas)
332         }
333         if (cart) {
334             drawPathCartesian(canvas)
335         }
336     }
337 
drawPathRelativeTicksnull338     private fun drawPathRelativeTicks(canvas: Canvas, x: Float, y: Float) {
339         val x1 = mPoints!![0]
340         val y1 = mPoints!![1]
341         val x2 = mPoints!![mPoints!!.size - 2]
342         val y2 = mPoints!![mPoints!!.size - 1]
343         val dist = Math.hypot((x1 - x2).toDouble(), (y1 - y2).toDouble()).toFloat()
344         val t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / (dist * dist)
345         val xp = x1 + t * (x2 - x1)
346         val yp = y1 + t * (y2 - y1)
347         val path = Path()
348         path.moveTo(x, y)
349         path.lineTo(xp, yp)
350         val len = Math.hypot((xp - x).toDouble(), (yp - y).toDouble()).toFloat()
351         val text = "" + (100 * len / dist).toInt() / 100.0f
352         getTextBounds(text, mTextPaint)
353         val off = len / 2 - mBounds.width() / 2
354         canvas.drawTextOnPath(text, path, off, -20f, mTextPaint)
355         canvas.drawLine(x, y, xp, yp, mPaintGraph)
356     }
357 
getTextBoundsnull358     fun getTextBounds(text: String, paint: Paint) {
359         paint.getTextBounds(text, 0, text.length, mBounds)
360     }
361 
drawPathCartesiannull362     private fun drawPathCartesian(canvas: Canvas) {
363         val x1 = mPoints!![0]
364         val y1 = mPoints!![1]
365         val x2 = mPoints!![mPoints!!.size - 2]
366         val y2 = mPoints!![mPoints!!.size - 1]
367         canvas.drawLine(
368             Math.min(x1, x2),
369             Math.max(y1, y2),
370             Math.max(x1, x2),
371             Math.max(y1, y2),
372             mPaintGraph
373         )
374         canvas.drawLine(
375             Math.min(x1, x2),
376             Math.min(y1, y2),
377             Math.min(x1, x2),
378             Math.max(y1, y2),
379             mPaintGraph
380         )
381     }
382 
drawPathCartesianTicksnull383     private fun drawPathCartesianTicks(canvas: Canvas, x: Float, y: Float) {
384         val x1 = mPoints!![0]
385         val y1 = mPoints!![1]
386         val x2 = mPoints!![mPoints!!.size - 2]
387         val y2 = mPoints!![mPoints!!.size - 1]
388         val minx = Math.min(x1, x2)
389         val maxy = Math.max(y1, y2)
390         val xgap = x - Math.min(x1, x2)
391         val ygap = Math.max(y1, y2) - y
392         // Horizontal line
393         var text = "" + (0.5 + 100 * xgap / Math.abs(x2 - x1)).toInt() / 100.0f
394         getTextBounds(text, mTextPaint)
395         var off = xgap / 2 - mBounds.width() / 2
396         canvas.drawText(text, off + minx, y - 20, mTextPaint)
397         canvas.drawLine(x, y, Math.min(x1, x2), y, mPaintGraph)
398 
399         // Vertical line
400         text = "" + (0.5 + 100 * ygap / Math.abs(y2 - y1)).toInt() / 100.0f
401         getTextBounds(text, mTextPaint)
402         off = ygap / 2 - mBounds.height() / 2
403         canvas.drawText(text, x + 5, maxy - off, mTextPaint)
404         canvas.drawLine(x, y, x, Math.max(y1, y2), mPaintGraph)
405     }
406 
drawPathScreenTicksnull407     private fun drawPathScreenTicks(
408         canvas: Canvas,
409         x: Float,
410         y: Float,
411         viewWidth: Int,
412         viewHeight: Int,
413         layoutWidth: Int,
414         layoutHeight: Int
415     ) {
416         val x1 = 0f
417         val y1 = 0f
418         val x2 = 1f
419         val y2 = 1f
420         val minx = 0f
421         val maxy = 0f
422         // Horizontal line
423         var text =
424             "" + (0.5 + 100 * (x - viewWidth / 2) / (layoutWidth - viewWidth)).toInt() / 100.0f
425         getTextBounds(text, mTextPaint)
426         var off = x / 2 - mBounds.width() / 2
427         canvas.drawText(text, off + minx, y - 20, mTextPaint)
428         canvas.drawLine(x, y, Math.min(x1, x2), y, mPaintGraph)
429 
430         // Vertical line
431         text =
432             "" + (0.5 + 100 * (y - viewHeight / 2) / (layoutHeight - viewHeight)).toInt() / 100.0f
433         getTextBounds(text, mTextPaint)
434         off = y / 2 - mBounds.height() / 2
435         canvas.drawText(text, x + 5, maxy - off, mTextPaint)
436         canvas.drawLine(x, y, x, Math.max(y1, y2), mPaintGraph)
437     }
438 
drawRectanglenull439     private fun drawRectangle(canvas: Canvas, motionController: Motion) {
440         mPath!!.reset()
441         val rectFrames = 50
442         for (i in 0..rectFrames) {
443             val p = i / rectFrames.toFloat()
444             motionController.buildRect(p, mRectangle, 0)
445             mPath!!.moveTo(mRectangle[0], mRectangle[1])
446             mPath!!.lineTo(mRectangle[2], mRectangle[3])
447             mPath!!.lineTo(mRectangle[4], mRectangle[5])
448             mPath!!.lineTo(mRectangle[6], mRectangle[7])
449             mPath!!.close()
450         }
451         mPaint.color = 0x44000000
452         canvas.translate(2f, 2f)
453         canvas.drawPath(mPath!!, mPaint)
454         canvas.translate(-2f, -2f)
455         mPaint.color = -0x10000
456         canvas.drawPath(mPath!!, mPaint)
457     }
458 
459     companion object {
460         const val DEBUG_SHOW_NONE = 0
461         const val DEBUG_SHOW_PROGRESS = 1
462         const val DEBUG_SHOW_PATH = 2
463         const val MAX_KEY_FRAMES = 50
464         private const val DEBUG_PATH_TICKS_PER_MS = 16
465     }
466 }
467