MotionConstrainedPoint.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.core.motion;

import androidx.constraintlayout.core.motion.utils.Easing;
import androidx.constraintlayout.core.motion.utils.Rect;
import androidx.constraintlayout.core.motion.utils.SplineSet;
import androidx.constraintlayout.core.motion.utils.TypedValues;
import androidx.constraintlayout.core.motion.utils.Utils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;

/**
 * All the parameter it extracts from a ConstraintSet/View
 *
 *
 */
class MotionConstrainedPoint implements Comparable<MotionConstrainedPoint> {
    public static final String TAG = "MotionPaths";
    public static final boolean DEBUG = false;

    private float mAlpha = 1;
    int mVisibilityMode = MotionWidget.VISIBILITY_MODE_NORMAL;
    int mVisibility;
    @SuppressWarnings("unused") private boolean mApplyElevation = false;
    private float mElevation = 0;
    private float mRotation = 0;
    private float mRotationX = 0;
    public float rotationY = 0;
    private float mScaleX = 1;
    private float mScaleY = 1;
    private float mPivotX = Float.NaN;
    private float mPivotY = Float.NaN;
    private float mTranslationX = 0;
    private float mTranslationY = 0;
    private float mTranslationZ = 0;
    @SuppressWarnings("unused") private Easing mKeyFrameEasing;
    @SuppressWarnings("unused") private int mDrawPath = 0;
    private float mPosition;
    private float mX;
    private float mY;
    private float mWidth;
    private float mHeight;
    private float mPathRotate = Float.NaN;
    private float mProgress = Float.NaN;
    @SuppressWarnings("unused") private int mAnimateRelativeTo = -1;

    static final int PERPENDICULAR = 1;
    static final int CARTESIAN = 2;
    static String[] sNames = {"position", "x", "y", "width", "height", "pathRotate"};

    LinkedHashMap<String, CustomVariable> mCustomVariable = new LinkedHashMap<>();
    int mMode = 0; // how was this point computed 1=perpendicular 2=deltaRelative

    MotionConstrainedPoint() {

    }

    private boolean diff(float a, float b) {
        if (Float.isNaN(a) || Float.isNaN(b)) {
            return Float.isNaN(a) != Float.isNaN(b);
        }
        return Math.abs(a - b) > 0.000001f;
    }

    /**
     * Given the start and end points define Keys that need to be built
     */
    void different(MotionConstrainedPoint points, HashSet<String> keySet) {
        if (diff(mAlpha, points.mAlpha)) {
            keySet.add(TypedValues.AttributesType.S_ALPHA);
        }
        if (diff(mElevation, points.mElevation)) {
            keySet.add(TypedValues.AttributesType.S_TRANSLATION_Z);
        }
        if (mVisibility != points.mVisibility
                && mVisibilityMode == MotionWidget.VISIBILITY_MODE_NORMAL
                && (mVisibility == MotionWidget.VISIBLE
                || points.mVisibility == MotionWidget.VISIBLE)) {
            keySet.add(TypedValues.AttributesType.S_ALPHA);
        }
        if (diff(mRotation, points.mRotation)) {
            keySet.add(TypedValues.AttributesType.S_ROTATION_Z);
        }
        if (!(Float.isNaN(mPathRotate) && Float.isNaN(points.mPathRotate))) {
            keySet.add(TypedValues.AttributesType.S_PATH_ROTATE);
        }
        if (!(Float.isNaN(mProgress) && Float.isNaN(points.mProgress))) {
            keySet.add(TypedValues.AttributesType.S_PROGRESS);
        }
        if (diff(mRotationX, points.mRotationX)) {
            keySet.add(TypedValues.AttributesType.S_ROTATION_X);
        }
        if (diff(rotationY, points.rotationY)) {
            keySet.add(TypedValues.AttributesType.S_ROTATION_Y);
        }
        if (diff(mPivotX, points.mPivotX)) {
            keySet.add(TypedValues.AttributesType.S_PIVOT_X);
        }
        if (diff(mPivotY, points.mPivotY)) {
            keySet.add(TypedValues.AttributesType.S_PIVOT_Y);
        }
        if (diff(mScaleX, points.mScaleX)) {
            keySet.add(TypedValues.AttributesType.S_SCALE_X);
        }
        if (diff(mScaleY, points.mScaleY)) {
            keySet.add(TypedValues.AttributesType.S_SCALE_Y);
        }
        if (diff(mTranslationX, points.mTranslationX)) {
            keySet.add(TypedValues.AttributesType.S_TRANSLATION_X);
        }
        if (diff(mTranslationY, points.mTranslationY)) {
            keySet.add(TypedValues.AttributesType.S_TRANSLATION_Y);
        }
        if (diff(mTranslationZ, points.mTranslationZ)) {
            keySet.add(TypedValues.AttributesType.S_TRANSLATION_Z);
        }
        if (diff(mElevation, points.mElevation)) {
            keySet.add(TypedValues.AttributesType.S_ELEVATION);
        }
    }

    void different(MotionConstrainedPoint points, boolean[] mask, String[] custom) {
        int c = 0;
        mask[c++] |= diff(mPosition, points.mPosition);
        mask[c++] |= diff(mX, points.mX);
        mask[c++] |= diff(mY, points.mY);
        mask[c++] |= diff(mWidth, points.mWidth);
        mask[c++] |= diff(mHeight, points.mHeight);
    }

    double[] mTempValue = new double[18];
    double[] mTempDelta = new double[18];

    void fillStandard(double[] data, int[] toUse) {
        float[] set = {mPosition, mX, mY, mWidth, mHeight, mAlpha, mElevation,
                mRotation, mRotationX, rotationY, mScaleX, mScaleY, mPivotX,
                mPivotY, mTranslationX, mTranslationY, mTranslationZ, mPathRotate};
        int c = 0;
        for (int i = 0; i < toUse.length; i++) {
            if (toUse[i] < set.length) {
                data[c++] = set[toUse[i]];
            }
        }
    }

    boolean hasCustomData(String name) {
        return mCustomVariable.containsKey(name);
    }

    int getCustomDataCount(String name) {
        return mCustomVariable.get(name).numberOfInterpolatedValues();
    }

    int getCustomData(String name, double[] value, int offset) {
        CustomVariable a = mCustomVariable.get(name);
        if (a.numberOfInterpolatedValues() == 1) {
            value[offset] = a.getValueToInterpolate();
            return 1;
        } else {
            int n = a.numberOfInterpolatedValues();
            float[] f = new float[n];
            a.getValuesToInterpolate(f);
            for (int i = 0; i < n; i++) {
                value[offset++] = f[i];
            }
            return n;
        }
    }

    void setBounds(float x, float y, float w, float h) {
        this.mX = x;
        this.mY = y;
        mWidth = w;
        mHeight = h;
    }

    @Override
    public int compareTo(MotionConstrainedPoint o) {
        return Float.compare(mPosition, o.mPosition);
    }

    public void applyParameters(MotionWidget view) {

        this.mVisibility = view.getVisibility();
        this.mAlpha = (view.getVisibility() != MotionWidget.VISIBLE) ? 0.0f : view.getAlpha();
        this.mApplyElevation = false; // TODO figure a way to cache parameters

        this.mRotation = view.getRotationZ();
        this.mRotationX = view.getRotationX();
        this.rotationY = view.getRotationY();
        this.mScaleX = view.getScaleX();
        this.mScaleY = view.getScaleY();
        this.mPivotX = view.getPivotX();
        this.mPivotY = view.getPivotY();
        this.mTranslationX = view.getTranslationX();
        this.mTranslationY = view.getTranslationY();
        this.mTranslationZ = view.getTranslationZ();
        Set<String> at = view.getCustomAttributeNames();
        for (String s : at) {
            CustomVariable attr = view.getCustomAttribute(s);
            if (attr != null && attr.isContinuous()) {
                this.mCustomVariable.put(s, attr);
            }
        }


    }

    public void addValues(HashMap<String, SplineSet> splines, int mFramePosition) {
        for (String s : splines.keySet()) {
            SplineSet ViewSpline = splines.get(s);
            if (DEBUG) {
                Utils.log(TAG, "setPoint" + mFramePosition + "  spline set = " + s);
            }
            switch (s) {
                case TypedValues.AttributesType.S_ALPHA:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mAlpha) ? 1 : mAlpha);
                    break;
                case TypedValues.AttributesType.S_ROTATION_Z:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mRotation) ? 0 : mRotation);
                    break;
                case TypedValues.AttributesType.S_ROTATION_X:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mRotationX) ? 0 : mRotationX);
                    break;
                case TypedValues.AttributesType.S_ROTATION_Y:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(rotationY) ? 0 : rotationY);
                    break;
                case TypedValues.AttributesType.S_PIVOT_X:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mPivotX) ? 0 : mPivotX);
                    break;
                case TypedValues.AttributesType.S_PIVOT_Y:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mPivotY) ? 0 : mPivotY);
                    break;
                case TypedValues.AttributesType.S_PATH_ROTATE:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mPathRotate) ? 0 : mPathRotate);
                    break;
                case TypedValues.AttributesType.S_PROGRESS:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mProgress) ? 0 : mProgress);
                    break;
                case TypedValues.AttributesType.S_SCALE_X:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mScaleX) ? 1 : mScaleX);
                    break;
                case TypedValues.AttributesType.S_SCALE_Y:
                    ViewSpline.setPoint(mFramePosition, Float.isNaN(mScaleY) ? 1 : mScaleY);
                    break;
                case TypedValues.AttributesType.S_TRANSLATION_X:
                    ViewSpline.setPoint(mFramePosition,
                            Float.isNaN(mTranslationX) ? 0 : mTranslationX);
                    break;
                case TypedValues.AttributesType.S_TRANSLATION_Y:
                    ViewSpline.setPoint(mFramePosition,
                            Float.isNaN(mTranslationY) ? 0 : mTranslationY);
                    break;
                case TypedValues.AttributesType.S_TRANSLATION_Z:
                    ViewSpline.setPoint(mFramePosition,
                            Float.isNaN(mTranslationZ) ? 0 : mTranslationZ);
                    break;
                default:
                    if (s.startsWith("CUSTOM")) {
                        String customName = s.split(",")[1];
                        if (mCustomVariable.containsKey(customName)) {
                            CustomVariable custom = mCustomVariable.get(customName);
                            if (ViewSpline instanceof SplineSet.CustomSpline) {
                                ((SplineSet.CustomSpline) ViewSpline)
                                        .setPoint(mFramePosition, custom);
                            } else {
                                Utils.loge(TAG, s + " ViewSpline not a CustomSet frame = "
                                        + mFramePosition + ", value"
                                        + custom.getValueToInterpolate() + ViewSpline);

                            }

                        }
                    } else {
                        Utils.loge(TAG, "UNKNOWN spline " + s);
                    }
            }
        }

    }

    public void setState(MotionWidget view) {
        setBounds(view.getX(), view.getY(), view.getWidth(), view.getHeight());
        applyParameters(view);
    }

    /**
     * @param rect     assumes pre rotated
     * @param rotation mode Surface.ROTATION_0,Surface.ROTATION_90...
     */
    public void setState(Rect rect, MotionWidget view, int rotation, float prevous) {
        setBounds(rect.left, rect.top, rect.width(), rect.height());
        applyParameters(view);
        mPivotX = Float.NaN;
        mPivotY = Float.NaN;

        switch (rotation) {
            case MotionWidget.ROTATE_PORTRATE_OF_LEFT:
                this.mRotation = prevous + 90;
                break;
            case MotionWidget.ROTATE_PORTRATE_OF_RIGHT:
                this.mRotation = prevous - 90;
                break;
        }
    }

//   TODO support Screen Rotation
//    /**
//     * Sets the state of the position given a rect, constraintset, rotation and viewid
//     *
//     * @param cw
//     * @param constraintSet
//     * @param rotation
//     * @param viewId
//     */
//    public void setState(Rect cw, ConstraintSet constraintSet, int rotation, int viewId) {
//        setBounds(cw.left, cw.top, cw.width(), cw.height());
//        applyParameters(constraintSet.getParameters(viewId));
//        switch (rotation) {
//            case ConstraintSet.ROTATE_PORTRATE_OF_RIGHT:
//            case ConstraintSet.ROTATE_RIGHT_OF_PORTRATE:
//                this.rotation -= 90;
//                break;
//            case ConstraintSet.ROTATE_PORTRATE_OF_LEFT:
//            case ConstraintSet.ROTATE_LEFT_OF_PORTRATE:
//                this.rotation += 90;
//                if (this.rotation > 180) this.rotation -= 360;
//                break;
//        }
//    }
}