ViewTimeCycle.java

/*
 * Copyright (C) 2020 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.motion.utils;

import android.os.Build;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;

import androidx.constraintlayout.core.motion.utils.CurveFit;
import androidx.constraintlayout.core.motion.utils.KeyCache;
import androidx.constraintlayout.core.motion.utils.TimeCycleSplineSet;
import androidx.constraintlayout.motion.widget.Key;
import androidx.constraintlayout.motion.widget.MotionLayout;
import androidx.constraintlayout.widget.ConstraintAttribute;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * This engine allows manipulation of attributes by wave shapes oscillating in time
 *
 * @DoNotShow
 */
public abstract class ViewTimeCycle extends TimeCycleSplineSet {
    private static final String TAG = "ViewTimeCycle";

    /**
     * Set the time cycle parameters
     * @param view
     * @param t
     * @param time
     * @param cache
     * @return
     */
    public abstract boolean setProperty(View view, float t, long time, KeyCache cache);

    /**
     * get a value from the time cycle
     * @param pos
     * @param time
     * @param view
     * @param cache
     * @return
     */
    public float get(float pos, long time, View view, KeyCache cache) {
        mCurveFit.getPos(pos, mCache);
        float period = mCache[CURVE_PERIOD];
        if (period == 0) {
            mContinue = false;
            return mCache[CURVE_OFFSET];
        }
        if (Float.isNaN(mLastCycle)) { // it has not been set
            mLastCycle = cache.getFloatValue(view, mType, 0); // check the cache
            if (Float.isNaN(mLastCycle)) {  // not in cache so set to 0 (start)
                mLastCycle = 0;
            }
        }
        long delta_time = time - mLastTime;
        mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0);
        cache.setFloatValue(view, mType, 0, mLastCycle);
        mLastTime = time;
        float v = mCache[CURVE_VALUE];
        float wave = calcWave(mLastCycle);
        float offset = mCache[CURVE_OFFSET];
        float value = v * wave + offset;
        mContinue = v != 0.0f || period != 0.0f;
        return value;
    }

    /**
     * make a custom time cycle
     * @param str
     * @param attrList
     * @return
     */
    public static ViewTimeCycle makeCustomSpline(String str,
                                                 SparseArray<ConstraintAttribute> attrList) {
        return new CustomSet(str, attrList);
    }

    /**
     * Make a time cycle spline
     * @param str
     * @param currentTime
     * @return
     */
    public static ViewTimeCycle makeSpline(String str, long currentTime) {
        ViewTimeCycle timeCycle;
        switch (str) {
            case Key.ALPHA:
                timeCycle = new AlphaSet();
                break;
            case Key.ELEVATION:
                timeCycle = new ElevationSet();
                break;
            case Key.ROTATION:
                timeCycle = new RotationSet();
                break;
            case Key.ROTATION_X:
                timeCycle = new RotationXset();
                break;
            case Key.ROTATION_Y:
                timeCycle = new RotationYset();
                break;
            case Key.TRANSITION_PATH_ROTATE:
                timeCycle = new PathRotate();
                break;
            case Key.SCALE_X:
                timeCycle = new ScaleXset();
                break;
            case Key.SCALE_Y:
                timeCycle = new ScaleYset();
                break;
            case Key.TRANSLATION_X:
                timeCycle = new TranslationXset();
                break;
            case Key.TRANSLATION_Y:
                timeCycle = new TranslationYset();
                break;
            case Key.TRANSLATION_Z:
                timeCycle = new TranslationZset();
                break;
            case Key.PROGRESS:
                timeCycle = new ProgressSet();
                break;
            default:
                return null;
        }
        timeCycle.setStartTime(currentTime);
        return timeCycle;
    }

    static class ElevationSet extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                view.setElevation(get(t, time, view, cache));
            }
            return mContinue;
        }
    }

    static class AlphaSet extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setAlpha(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class RotationSet extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setRotation(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class RotationXset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setRotationX(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class RotationYset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setRotationY(get(t, time, view, cache));
            return mContinue;
        }
    }

    public static class PathRotate extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            return mContinue;
        }

        /**
         *  DO NOT CALL
         * @param view
         * @param cache
         * @param t
         * @param time
         * @param dx
         * @param dy
         * @return
         */
        public boolean setPathRotate(View view,
                                     KeyCache cache,
                                     float t,
                                     long time,
                                     double dx,
                                     double dy) {
            view.setRotation(get(t, time, view, cache)
                    + (float) Math.toDegrees(Math.atan2(dy, dx)));
            return mContinue;
        }
    }

    static class ScaleXset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setScaleX(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class ScaleYset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setScaleY(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class TranslationXset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setTranslationX(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class TranslationYset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            view.setTranslationY(get(t, time, view, cache));
            return mContinue;
        }
    }

    static class TranslationZset extends ViewTimeCycle {
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                view.setTranslationZ(get(t, time, view, cache));
            }
            return mContinue;
        }
    }

    public static class CustomSet extends ViewTimeCycle {
        String mAttributeName;
        SparseArray<ConstraintAttribute> mConstraintAttributeList;
        SparseArray<float[]> mWaveProperties = new SparseArray<>();
        float[] mTempValues;
        float[] mCache;

        public CustomSet(String attribute, SparseArray<ConstraintAttribute> attrList) {
            mAttributeName = attribute.split(",")[1];
            mConstraintAttributeList = attrList;
        }

        /**
         * Setup the curve
         * @param curveType
         */
        public void setup(int curveType) {
            int size = mConstraintAttributeList.size();
            int dimensionality =
                    mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues();
            double[] time = new double[size];
            mTempValues = new float[dimensionality + 2];
            mCache = new float[dimensionality];
            double[][] values = new double[size][dimensionality + 2];
            for (int i = 0; i < size; i++) {
                int key = mConstraintAttributeList.keyAt(i);
                ConstraintAttribute ca = mConstraintAttributeList.valueAt(i);
                float[] waveProp = mWaveProperties.valueAt(i);
                time[i] = key * 1E-2;
                ca.getValuesToInterpolate(mTempValues);
                for (int k = 0; k < mTempValues.length; k++) {
                    values[i][k] = mTempValues[k];
                }
                values[i][dimensionality] = waveProp[0];
                values[i][dimensionality + 1] = waveProp[1];
            }
            mCurveFit = CurveFit.get(curveType, time, values);
        }

        /**
         * @TODO: add description
         * @param position
         * @param value
         * @param period
         * @param shape
         * @param offset
         */
        public void setPoint(int position, float value, float period, int shape, float offset) {
            throw new RuntimeException("Wrong call for custom attribute");
        }

        /**
         * set the keyTimePoint
         * @param position
         * @param value
         * @param period
         * @param shape
         * @param offset
         */
        public void setPoint(int position, ConstraintAttribute value,
                             float period,
                             int shape,
                             float offset) {
            mConstraintAttributeList.append(position, value);
            mWaveProperties.append(position, new float[]{period, offset});
            mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen
        }

        /**
         * Set the property of the view given the position the current time
         * @param view the view to set the property of
         * @param t the position on the curve
         * @param time the current time
         * @param cache the cache used to keep the previous position
         * @return true if it will need repaint
         */
        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            mCurveFit.getPos(t, mTempValues);
            float period = mTempValues[mTempValues.length - 2];
            float offset = mTempValues[mTempValues.length - 1];
            long delta_time = time - mLastTime;

            if (Float.isNaN(mLastCycle)) { // it has not been set
                mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache
                if (Float.isNaN(mLastCycle)) {  // not in cache so set to 0 (start)
                    mLastCycle = 0;
                }
            }

            mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0);
            mLastTime = time;
            float wave = calcWave(mLastCycle);
            mContinue = false;
            for (int i = 0; i < mCache.length; i++) {
                mContinue |= mTempValues[i] != 0.0;
                mCache[i] = mTempValues[i] * wave + offset;
            }
            CustomSupport.setInterpolatedValue(mConstraintAttributeList.valueAt(0),
                        view, mCache);
            if (period != 0.0f) {
                mContinue = true;
            }
            return mContinue;
        }
    }

    static class ProgressSet extends ViewTimeCycle {
        boolean mNoMethod = false;

        @Override
        public boolean setProperty(View view, float t, long time, KeyCache cache) {
            if (view instanceof MotionLayout) {
                ((MotionLayout) view).setProgress(get(t, time, view, cache));
            } else {
                if (mNoMethod) {
                    return false;
                }
                Method method = null;
                try {
                    method = view.getClass().getMethod("setProgress", Float.TYPE);
                } catch (NoSuchMethodException e) {
                    mNoMethod = true;
                }
                if (method != null) {
                    try {
                        method.invoke(view, get(t, time, view, cache));
                    } catch (IllegalAccessException e) {
                        Log.e(TAG, "unable to setProgress", e);
                    } catch (InvocationTargetException e) {
                        Log.e(TAG, "unable to setProgress", e);
                    }
                }
            }
            return mContinue;
        }
    }
}