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