MotionRenderDebug.java

/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.constraintlayout.compose;

import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;

import androidx.constraintlayout.core.motion.Motion;
import androidx.constraintlayout.core.motion.MotionPaths;

import java.util.HashMap;

class MotionRenderDebug {
    public static final int DEBUG_SHOW_NONE = 0;
    public static final int DEBUG_SHOW_PROGRESS = 1;
    public static final int DEBUG_SHOW_PATH = 2;

    static final int MAX_KEY_FRAMES = 50;
    private static final int DEBUG_PATH_TICKS_PER_MS = 16;
    float[] mPoints;
    int[] mPathMode;
    float[] mKeyFramePoints;
    Path mPath;
    Paint mPaint;
    Paint mPaintKeyframes;
    Paint mPaintGraph;
    Paint mTextPaint;
    Paint mFillPaint;
    private float[] mRectangle;
    final int RED_COLOR = 0xFFFFAA33;
    final int KEYFRAME_COLOR = 0xffe0759a;
    final int GRAPH_COLOR = 0xFF33AA00;
    final int SHADOW_COLOR = 0x77000000;
    final int DIAMOND_SIZE = 10;
    DashPathEffect mDashPathEffect;
    int mKeyFrameCount;
    Rect mBounds = new Rect();
    boolean mPresentationMode = false;
    int mShadowTranslate = 1;

    public MotionRenderDebug(float textSize) {

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(RED_COLOR);
        mPaint.setStrokeWidth(2);
        mPaint.setStyle(Paint.Style.STROKE);

        mPaintKeyframes = new Paint();
        mPaintKeyframes.setAntiAlias(true);
        mPaintKeyframes.setColor(KEYFRAME_COLOR);
        mPaintKeyframes.setStrokeWidth(2);
        mPaintKeyframes.setStyle(Paint.Style.STROKE);

        mPaintGraph = new Paint();
        mPaintGraph.setAntiAlias(true);
        mPaintGraph.setColor(GRAPH_COLOR);
        mPaintGraph.setStrokeWidth(2);
        mPaintGraph.setStyle(Paint.Style.STROKE);

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(GRAPH_COLOR);
        mTextPaint.setTextSize(textSize);
        mRectangle = new float[8];
        mFillPaint = new Paint();
        mFillPaint.setAntiAlias(true);
        mDashPathEffect = new DashPathEffect(new float[]{4, 8}, 0);
        mPaintGraph.setPathEffect(mDashPathEffect);
        mKeyFramePoints = new float[MAX_KEY_FRAMES * 2];
        mPathMode = new int[MAX_KEY_FRAMES];

        if (mPresentationMode) {
            mPaint.setStrokeWidth(8);
            mFillPaint.setStrokeWidth(8);
            mPaintKeyframes.setStrokeWidth(8);
            mShadowTranslate = 4;
        }
    }

    public void draw(Canvas canvas,
                     HashMap<String, Motion> frameArrayList,
                     int duration, int debugPath,
                     int layoutWidth, int layoutHeight) {
        if (frameArrayList == null || frameArrayList.size() == 0) {
            return;
        }
        canvas.save();

        for (Motion motionController : frameArrayList.values()) {
            draw(canvas, motionController, duration, debugPath,
                    layoutWidth, layoutHeight);
        }
        canvas.restore();
    }

    public void draw(Canvas canvas,
                     Motion motionController,
                     int duration, int debugPath,
                     int layoutWidth, int layoutHeight) {
        int mode = motionController.getDrawPath();
        if (debugPath > 0 && mode == Motion.DRAW_PATH_NONE) {
            mode = Motion.DRAW_PATH_BASIC;
        }
        if (mode == Motion.DRAW_PATH_NONE) { // do not draw path
            return;
        }

        mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode, null);

        if (mode >= Motion.DRAW_PATH_BASIC) {

            int frames = duration / DEBUG_PATH_TICKS_PER_MS;
            if (mPoints == null || mPoints.length != frames * 2) {
                mPoints = new float[frames * 2];
                mPath = new Path();
            }

            canvas.translate(mShadowTranslate, mShadowTranslate);

            mPaint.setColor(SHADOW_COLOR);
            mFillPaint.setColor(SHADOW_COLOR);
            mPaintKeyframes.setColor(SHADOW_COLOR);
            mPaintGraph.setColor(SHADOW_COLOR);
            motionController.buildPath(mPoints, frames);
            drawAll(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight);
            mPaint.setColor(RED_COLOR);
            mPaintKeyframes.setColor(KEYFRAME_COLOR);
            mFillPaint.setColor(KEYFRAME_COLOR);
            mPaintGraph.setColor(GRAPH_COLOR);

            canvas.translate(-mShadowTranslate, -mShadowTranslate);
            drawAll(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight);
            if (mode == Motion.DRAW_PATH_RECTANGLE) {
                drawRectangle(canvas, motionController);
            }
        }

    }


    public void drawAll(Canvas canvas, int mode, int keyFrames, Motion motionController,
                        int layoutWidth, int layoutHeight) {
        if (mode == Motion.DRAW_PATH_AS_CONFIGURED) {
            drawPathAsConfigured(canvas);
        }
        if (mode == Motion.DRAW_PATH_RELATIVE) {
            drawPathRelative(canvas);
        }
        if (mode == Motion.DRAW_PATH_CARTESIAN) {
            drawPathCartesian(canvas);
        }
        drawBasicPath(canvas);
        drawTicks(canvas, mode, keyFrames, motionController, layoutWidth, layoutHeight);
    }

    private void drawBasicPath(Canvas canvas) {
        canvas.drawLines(mPoints, mPaint);
    }

    private void drawTicks(Canvas canvas, int mode, int keyFrames, Motion motionController,
                           int layoutWidth, int layoutHeight) {
        int viewWidth = 0;
        int viewHeight = 0;
        if (motionController.getView() != null) {
            viewWidth = motionController.getView().getWidth();
            viewHeight = motionController.getView().getHeight();
        }
        for (int i = 1; i < keyFrames - 1; i++) {
            if (mode == Motion.DRAW_PATH_AS_CONFIGURED
                    && mPathMode[i - 1] == Motion.DRAW_PATH_NONE) {
                continue;

            }
            float x = mKeyFramePoints[i * 2];
            float y = mKeyFramePoints[i * 2 + 1];
            mPath.reset();
            mPath.moveTo(x, y + DIAMOND_SIZE);
            mPath.lineTo(x + DIAMOND_SIZE, y);
            mPath.lineTo(x, y - DIAMOND_SIZE);
            mPath.lineTo(x - DIAMOND_SIZE, y);
            mPath.close();

            MotionPaths framePoint = motionController.getKeyFrame(i - 1);
            float dx = 0;//framePoint.translationX;
            float dy = 0;//framePoint.translationY;
            if (mode == Motion.DRAW_PATH_AS_CONFIGURED) {

                if (mPathMode[i - 1] == MotionPaths.PERPENDICULAR) {
                    drawPathRelativeTicks(canvas, x - dx, y - dy);
                } else if (mPathMode[i - 1] == MotionPaths.CARTESIAN) {
                    drawPathCartesianTicks(canvas, x - dx, y - dy);
                } else if (mPathMode[i - 1] == MotionPaths.SCREEN) {
                    drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight, layoutWidth, layoutHeight);
                }

                canvas.drawPath(mPath, mFillPaint);
            }
            if (mode == Motion.DRAW_PATH_RELATIVE) {
                drawPathRelativeTicks(canvas, x - dx, y - dy);
            }
            if (mode == Motion.DRAW_PATH_CARTESIAN) {
                drawPathCartesianTicks(canvas, x - dx, y - dy);
            }
            if (mode == Motion.DRAW_PATH_SCREEN) {
                drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight, layoutWidth, layoutHeight);
            }
            if (dx != 0 || dy != 0) {
                drawTranslation(canvas, x - dx, y - dy, x, y);
            } else {
                canvas.drawPath(mPath, mFillPaint);
            }
        }
        if (mPoints.length > 1) {
            // Draw the starting and ending circle
            canvas.drawCircle(mPoints[0], mPoints[1], 8, mPaintKeyframes);
            canvas.drawCircle(mPoints[mPoints.length - 2],
                    mPoints[mPoints.length - 1], 8, mPaintKeyframes);
        }
    }

    private void drawTranslation(Canvas canvas, float x1, float y1, float x2, float y2) {
        canvas.drawRect(x1, y1, x2, y2, mPaintGraph);
        canvas.drawLine(x1, y1, x2, y2, mPaintGraph);
    }

    private void drawPathRelative(Canvas canvas) {
        canvas.drawLine(mPoints[0], mPoints[1],
                mPoints[mPoints.length - 2], mPoints[mPoints.length - 1], mPaintGraph);
    }

    private void drawPathAsConfigured(Canvas canvas) {
        boolean path = false;
        boolean cart = false;
        for (int i = 0; i < mKeyFrameCount; i++) {
            if (mPathMode[i] == MotionPaths.PERPENDICULAR) {
                path = true;
            }
            if (mPathMode[i] == MotionPaths.CARTESIAN) {
                cart = true;
            }
        }
        if (path) {
            drawPathRelative(canvas);
        }
        if (cart) {
            drawPathCartesian(canvas);
        }
    }

    private void drawPathRelativeTicks(Canvas canvas, float x, float y) {
        float x1 = mPoints[0];
        float y1 = mPoints[1];
        float x2 = mPoints[mPoints.length - 2];
        float y2 = mPoints[mPoints.length - 1];
        float dist = (float) Math.hypot(x1 - x2, y1 - y2);
        float t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / (dist * dist);
        float xp = x1 + t * (x2 - x1);
        float yp = y1 + t * (y2 - y1);

        Path path = new Path();
        path.moveTo(x, y);
        path.lineTo(xp, yp);
        float len = (float) Math.hypot(xp - x, yp - y);
        String text = "" + ((int) (100 * len / dist)) / 100.0f;
        getTextBounds(text, mTextPaint);
        float off = len / 2 - mBounds.width() / 2;
        canvas.drawTextOnPath(text, path, off, -20, mTextPaint);
        canvas.drawLine(x, y, xp, yp, mPaintGraph);
    }

    void getTextBounds(String text, Paint paint) {
        paint.getTextBounds(text, 0, text.length(), mBounds);
    }

    private void drawPathCartesian(Canvas canvas) {
        float x1 = mPoints[0];
        float y1 = mPoints[1];
        float x2 = mPoints[mPoints.length - 2];
        float y2 = mPoints[mPoints.length - 1];

        canvas.drawLine(Math.min(x1, x2), Math.max(y1, y2),
                Math.max(x1, x2), Math.max(y1, y2), mPaintGraph);
        canvas.drawLine(Math.min(x1, x2), Math.min(y1, y2),
                Math.min(x1, x2), Math.max(y1, y2), mPaintGraph);
    }

    private void drawPathCartesianTicks(Canvas canvas, float x, float y) {
        float x1 = mPoints[0];
        float y1 = mPoints[1];
        float x2 = mPoints[mPoints.length - 2];
        float y2 = mPoints[mPoints.length - 1];
        float minx = Math.min(x1, x2);
        float maxy = Math.max(y1, y2);
        float xgap = x - Math.min(x1, x2);
        float ygap = Math.max(y1, y2) - y;
        // Horizontal line
        String text = "" + ((int) (0.5 + 100 * xgap / Math.abs(x2 - x1))) / 100.0f;
        getTextBounds(text, mTextPaint);
        float off = xgap / 2 - mBounds.width() / 2;
        canvas.drawText(text, off + minx, y - 20, mTextPaint);
        canvas.drawLine(x, y,
                Math.min(x1, x2), y, mPaintGraph);

        // Vertical line
        text = "" + ((int) (0.5 + 100 * ygap / Math.abs(y2 - y1))) / 100.0f;
        getTextBounds(text, mTextPaint);
        off = ygap / 2 - mBounds.height() / 2;
        canvas.drawText(text, x + 5, maxy - off, mTextPaint);
        canvas.drawLine(x, y,
                x, Math.max(y1, y2), mPaintGraph);
    }

    private void drawPathScreenTicks(Canvas canvas, float x, float y, int viewWidth, int viewHeight,
                                     int layoutWidth, int layoutHeight) {
        float x1 = 0;
        float y1 = 0;
        float x2 = 1;
        float y2 = 1;
        float minx = 0;
        float maxy = 0;
        float xgap = x;
        float ygap = y;
        // Horizontal line
        String text = "" + ((int) (0.5 + 100 * (xgap - viewWidth / 2) / (layoutWidth - viewWidth))) / 100.0f;
        getTextBounds(text, mTextPaint);
        float off = xgap / 2 - mBounds.width() / 2;
        canvas.drawText(text, off + minx, y - 20, mTextPaint);
        canvas.drawLine(x, y,
                Math.min(x1, x2), y, mPaintGraph);

        // Vertical line
        text = "" + ((int) (0.5 + 100 * (ygap - viewHeight / 2) / (layoutHeight - viewHeight))) / 100.0f;
        getTextBounds(text, mTextPaint);
        off = ygap / 2 - mBounds.height() / 2;
        canvas.drawText(text, x + 5, maxy - off, mTextPaint);
        canvas.drawLine(x, y,
                x, Math.max(y1, y2), mPaintGraph);
    }

    private void drawRectangle(Canvas canvas, Motion motionController) {
        mPath.reset();
        int rectFrames = 50;
        for (int i = 0; i <= rectFrames; i++) {
            float p = i / (float) rectFrames;
            motionController.buildRect(p, mRectangle, 0);
            mPath.moveTo(mRectangle[0], mRectangle[1]);
            mPath.lineTo(mRectangle[2], mRectangle[3]);
            mPath.lineTo(mRectangle[4], mRectangle[5]);
            mPath.lineTo(mRectangle[6], mRectangle[7]);
            mPath.close();
        }
        mPaint.setColor(0x44000000);
        canvas.translate(2, 2);
        canvas.drawPath(mPath, mPaint);

        canvas.translate(-2, -2);
        mPaint.setColor(0xFFFF0000);
        canvas.drawPath(mPath, mPaint);
    }

}