1 /* 2 * Copyright (C) 2020 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.core.motion.utils; 17 18 import androidx.constraintlayout.core.motion.CustomAttribute; 19 import androidx.constraintlayout.core.motion.CustomVariable; 20 import androidx.constraintlayout.core.motion.MotionWidget; 21 22 import java.text.DecimalFormat; 23 24 /** 25 * This engine allows manipulation of attributes by wave shapes oscillating in time 26 * 27 * 28 */ 29 public abstract class TimeCycleSplineSet { 30 private static final String TAG = "SplineSet"; 31 protected CurveFit mCurveFit; 32 protected int mWaveShape = 0; 33 protected int[] mTimePoints = new int[10]; 34 protected float[][] mValues = new float[10][3]; 35 protected int mCount; 36 protected String mType; 37 protected float[] mCache = new float[3]; 38 protected static final int CURVE_VALUE = 0; 39 protected static final int CURVE_PERIOD = 1; 40 protected static final int CURVE_OFFSET = 2; 41 protected static float sVal2PI = (float) (2 * Math.PI); 42 protected boolean mContinue = false; 43 protected long mLastTime; 44 protected float mLastCycle = Float.NaN; 45 46 @Override toString()47 public String toString() { 48 String str = mType; 49 DecimalFormat df = new DecimalFormat("##.##"); 50 for (int i = 0; i < mCount; i++) { 51 str += "[" + mTimePoints[i] + " , " + df.format(mValues[i]) + "] "; 52 } 53 return str; 54 } 55 setType(String type)56 public void setType(String type) { 57 mType = type; 58 } 59 60 /** 61 * @param period cycles per second 62 */ calcWave(float period)63 protected float calcWave(float period) { 64 float p = period; 65 switch (mWaveShape) { 66 default: 67 case Oscillator.SIN_WAVE: 68 return (float) Math.sin(p * sVal2PI); 69 case Oscillator.SQUARE_WAVE: 70 return (float) Math.signum(p * sVal2PI); 71 case Oscillator.TRIANGLE_WAVE: 72 return 1 - Math.abs(p); 73 case Oscillator.SAW_WAVE: 74 return ((p * 2 + 1) % 2) - 1; 75 case Oscillator.REVERSE_SAW_WAVE: 76 return (1 - ((p * 2 + 1) % 2)); 77 case Oscillator.COS_WAVE: 78 return (float) Math.cos(p * sVal2PI); 79 case Oscillator.BOUNCE: 80 float x = 1 - Math.abs((p * 4) % 4 - 2); 81 return 1 - x * x; 82 } 83 } 84 getCurveFit()85 public CurveFit getCurveFit() { 86 return mCurveFit; 87 } 88 setStartTime(long currentTime)89 protected void setStartTime(long currentTime) { 90 mLastTime = currentTime; 91 } 92 93 // @TODO: add description setPoint(int position, float value, float period, int shape, float offset)94 public void setPoint(int position, float value, float period, int shape, float offset) { 95 mTimePoints[mCount] = position; 96 mValues[mCount][CURVE_VALUE] = value; 97 mValues[mCount][CURVE_PERIOD] = period; 98 mValues[mCount][CURVE_OFFSET] = offset; 99 mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen 100 mCount++; 101 } 102 103 public static class CustomSet extends TimeCycleSplineSet { 104 String mAttributeName; 105 KeyFrameArray.CustomArray mConstraintAttributeList; 106 KeyFrameArray.FloatArray mWaveProperties = new KeyFrameArray.FloatArray(); 107 float[] mTempValues; 108 float[] mCustomCache; 109 CustomSet(String attribute, KeyFrameArray.CustomArray attrList)110 public CustomSet(String attribute, KeyFrameArray.CustomArray attrList) { 111 mAttributeName = attribute.split(",")[1]; 112 mConstraintAttributeList = attrList; 113 } 114 115 // @TODO: add description 116 @Override setup(int curveType)117 public void setup(int curveType) { 118 int size = mConstraintAttributeList.size(); 119 int dimensionality = mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues(); 120 double[] time = new double[size]; 121 mTempValues = new float[dimensionality + 2]; 122 mCustomCache = new float[dimensionality]; 123 double[][] values = new double[size][dimensionality + 2]; 124 for (int i = 0; i < size; i++) { 125 int key = mConstraintAttributeList.keyAt(i); 126 CustomAttribute ca = mConstraintAttributeList.valueAt(i); 127 float[] waveProp = mWaveProperties.valueAt(i); 128 time[i] = key * 1E-2; 129 ca.getValuesToInterpolate(mTempValues); 130 for (int k = 0; k < mTempValues.length; k++) { 131 values[i][k] = mTempValues[k]; 132 } 133 values[i][dimensionality] = waveProp[0]; 134 values[i][dimensionality + 1] = waveProp[1]; 135 } 136 mCurveFit = CurveFit.get(curveType, time, values); 137 } 138 139 // @TODO: add description 140 @Override setPoint(int position, float value, float period, int shape, float offset)141 public void setPoint(int position, float value, float period, int shape, float offset) { 142 throw new RuntimeException("don't call for custom attribute " 143 + "call setPoint(pos, ConstraintAttribute,...)"); 144 } 145 146 // @TODO: add description setPoint(int position, CustomAttribute value, float period, int shape, float offset)147 public void setPoint(int position, 148 CustomAttribute value, 149 float period, 150 int shape, 151 float offset) { 152 mConstraintAttributeList.append(position, value); 153 mWaveProperties.append(position, new float[]{period, offset}); 154 mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen 155 } 156 157 // @TODO: add description setProperty(MotionWidget view, float t, long time, KeyCache cache)158 public boolean setProperty(MotionWidget view, float t, long time, KeyCache cache) { 159 mCurveFit.getPos(t, mTempValues); 160 float period = mTempValues[mTempValues.length - 2]; 161 float offset = mTempValues[mTempValues.length - 1]; 162 long delta_time = time - mLastTime; 163 164 if (Float.isNaN(mLastCycle)) { // it has not been set 165 mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache 166 if (Float.isNaN(mLastCycle)) { // not in cache so set to 0 (start) 167 mLastCycle = 0; 168 } 169 } 170 171 mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0); 172 mLastTime = time; 173 float wave = calcWave(mLastCycle); 174 mContinue = false; 175 for (int i = 0; i < mCustomCache.length; i++) { 176 mContinue |= mTempValues[i] != 0.0; 177 mCustomCache[i] = mTempValues[i] * wave + offset; 178 } 179 view.setInterpolatedValue(mConstraintAttributeList.valueAt(0), mCustomCache); 180 if (period != 0.0f) { 181 mContinue = true; 182 } 183 return mContinue; 184 } 185 } 186 187 // @TODO: add description setup(int curveType)188 public void setup(int curveType) { 189 if (mCount == 0) { 190 System.err.println("Error no points added to " + mType); 191 return; 192 } 193 Sort.doubleQuickSort(mTimePoints, mValues, 0, mCount - 1); 194 int unique = 0; 195 for (int i = 1; i < mTimePoints.length; i++) { 196 if (mTimePoints[i] != mTimePoints[i - 1]) { 197 unique++; 198 } 199 } 200 if (unique == 0) { 201 unique = 1; 202 } 203 double[] time = new double[unique]; 204 double[][] values = new double[unique][3]; 205 int k = 0; 206 207 for (int i = 0; i < mCount; i++) { 208 if (i > 0 && mTimePoints[i] == mTimePoints[i - 1]) { 209 continue; 210 } 211 time[k] = mTimePoints[i] * 1E-2; 212 values[k][0] = mValues[i][0]; 213 values[k][1] = mValues[i][1]; 214 values[k][2] = mValues[i][2]; 215 k++; 216 } 217 mCurveFit = CurveFit.get(curveType, time, values); 218 } 219 220 protected static class Sort { doubleQuickSort(int[] key, float[][] value, int low, int hi)221 static void doubleQuickSort(int[] key, float[][] value, int low, int hi) { 222 int[] stack = new int[key.length + 10]; 223 int count = 0; 224 stack[count++] = hi; 225 stack[count++] = low; 226 while (count > 0) { 227 low = stack[--count]; 228 hi = stack[--count]; 229 if (low < hi) { 230 int p = partition(key, value, low, hi); 231 stack[count++] = p - 1; 232 stack[count++] = low; 233 stack[count++] = hi; 234 stack[count++] = p + 1; 235 } 236 } 237 } 238 partition(int[] array, float[][] value, int low, int hi)239 private static int partition(int[] array, float[][] value, int low, int hi) { 240 int pivot = array[hi]; 241 int i = low; 242 for (int j = low; j < hi; j++) { 243 if (array[j] <= pivot) { 244 swap(array, value, i, j); 245 i++; 246 } 247 } 248 swap(array, value, i, hi); 249 return i; 250 } 251 swap(int[] array, float[][] value, int a, int b)252 private static void swap(int[] array, float[][] value, int a, int b) { 253 int tmp = array[a]; 254 array[a] = array[b]; 255 array[b] = tmp; 256 float[] tmpv = value[a]; 257 value[a] = value[b]; 258 value[b] = tmpv; 259 } 260 } 261 262 263 public static class CustomVarSet extends TimeCycleSplineSet { 264 String mAttributeName; 265 KeyFrameArray.CustomVar mConstraintAttributeList; 266 KeyFrameArray.FloatArray mWaveProperties = new KeyFrameArray.FloatArray(); 267 float[] mTempValues; 268 float[] mCustomCache; 269 CustomVarSet(String attribute, KeyFrameArray.CustomVar attrList)270 public CustomVarSet(String attribute, KeyFrameArray.CustomVar attrList) { 271 mAttributeName = attribute.split(",")[1]; 272 mConstraintAttributeList = attrList; 273 } 274 275 // @TODO: add description 276 @Override setup(int curveType)277 public void setup(int curveType) { 278 int size = mConstraintAttributeList.size(); 279 int dimensionality = mConstraintAttributeList.valueAt(0).numberOfInterpolatedValues(); 280 double[] time = new double[size]; 281 mTempValues = new float[dimensionality + 2]; 282 mCustomCache = new float[dimensionality]; 283 double[][] values = new double[size][dimensionality + 2]; 284 for (int i = 0; i < size; i++) { 285 int key = mConstraintAttributeList.keyAt(i); 286 CustomVariable ca = mConstraintAttributeList.valueAt(i); 287 float[] waveProp = mWaveProperties.valueAt(i); 288 time[i] = key * 1E-2; 289 ca.getValuesToInterpolate(mTempValues); 290 for (int k = 0; k < mTempValues.length; k++) { 291 values[i][k] = mTempValues[k]; 292 } 293 values[i][dimensionality] = waveProp[0]; 294 values[i][dimensionality + 1] = waveProp[1]; 295 } 296 mCurveFit = CurveFit.get(curveType, time, values); 297 } 298 299 // @TODO: add description 300 @Override setPoint(int position, float value, float period, int shape, float offset)301 public void setPoint(int position, float value, float period, int shape, float offset) { 302 throw new RuntimeException("don't call for custom attribute " 303 + "call setPoint(pos, ConstraintAttribute,...)"); 304 } 305 306 // @TODO: add description setPoint(int position, CustomVariable value, float period, int shape, float offset)307 public void setPoint(int position, 308 CustomVariable value, 309 float period, 310 int shape, 311 float offset) { 312 mConstraintAttributeList.append(position, value); 313 mWaveProperties.append(position, new float[]{period, offset}); 314 mWaveShape = Math.max(mWaveShape, shape); // the highest value shape is chosen 315 } 316 317 // @TODO: add description setProperty(MotionWidget view, float t, long time, KeyCache cache)318 public boolean setProperty(MotionWidget view, float t, long time, KeyCache cache) { 319 mCurveFit.getPos(t, mTempValues); 320 float period = mTempValues[mTempValues.length - 2]; 321 float offset = mTempValues[mTempValues.length - 1]; 322 long delta_time = time - mLastTime; 323 324 if (Float.isNaN(mLastCycle)) { // it has not been set 325 mLastCycle = cache.getFloatValue(view, mAttributeName, 0); // check the cache 326 if (Float.isNaN(mLastCycle)) { // not in cache so set to 0 (start) 327 mLastCycle = 0; 328 } 329 } 330 331 mLastCycle = (float) ((mLastCycle + delta_time * 1E-9 * period) % 1.0); 332 mLastTime = time; 333 float wave = calcWave(mLastCycle); 334 mContinue = false; 335 for (int i = 0; i < mCustomCache.length; i++) { 336 mContinue |= mTempValues[i] != 0.0; 337 mCustomCache[i] = mTempValues[i] * wave + offset; 338 } 339 mConstraintAttributeList.valueAt(0).setInterpolatedValue(view, mCustomCache); 340 if (period != 0.0f) { 341 mContinue = true; 342 } 343 return mContinue; 344 } 345 } 346 } 347