1 /* 2 * Copyright (C) 2016 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 17 package androidx.leanback.widget; 18 19 import android.animation.PropertyValuesHolder; 20 import android.util.Property; 21 22 import androidx.annotation.RestrictTo; 23 import androidx.leanback.widget.Parallax.FloatProperty; 24 import androidx.leanback.widget.Parallax.FloatPropertyMarkerValue; 25 import androidx.leanback.widget.Parallax.IntProperty; 26 import androidx.leanback.widget.Parallax.PropertyMarkerValue; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 31 /** 32 * ParallaxEffect class drives changes in {@link ParallaxTarget} in response to changes in 33 * variables defined in {@link Parallax}. 34 * <p> 35 * ParallaxEffect has a list of {@link Parallax.PropertyMarkerValue}s which represents the range of 36 * values that source variables can take. The main function is 37 * {@link ParallaxEffect#performMapping(Parallax)} which computes a fraction between 0 and 1 38 * based on the current values of variables in {@link Parallax}. As the parallax effect goes 39 * on, the fraction increases from 0 at beginning to 1 at the end. Then the fraction is passed on 40 * to {@link ParallaxTarget#update(float)}. 41 * <p> 42 * App use {@link Parallax#addEffect(PropertyMarkerValue...)} to create a ParallaxEffect. 43 */ 44 public abstract class ParallaxEffect { 45 46 final List<Parallax.PropertyMarkerValue> mMarkerValues = new ArrayList(2); 47 final List<Float> mWeights = new ArrayList<Float>(2); 48 final List<Float> mTotalWeights = new ArrayList<Float>(2); 49 final List<ParallaxTarget> mTargets = new ArrayList<ParallaxTarget>(4); 50 51 /** 52 * Only accessible from package 53 */ 54 ParallaxEffect() { 55 } 56 57 /** 58 * Returns the list of {@link PropertyMarkerValue}s, which represents the range of values that 59 * source variables can take. 60 * 61 * @return A list of {@link Parallax.PropertyMarkerValue}s. 62 * @see #performMapping(Parallax) 63 */ 64 public final List<Parallax.PropertyMarkerValue> getPropertyRanges() { 65 return mMarkerValues; 66 } 67 68 /** 69 * Returns a list of Float objects that represents weight associated with each variable range. 70 * Weights are used when there are three or more marker values. 71 * 72 * @return A list of Float objects that represents weight associated with each variable range. 73 * @hide 74 */ 75 @RestrictTo(RestrictTo.Scope.LIBRARY) 76 public final List<Float> getWeights() { 77 return mWeights; 78 } 79 80 /** 81 * Sets the list of {@link PropertyMarkerValue}s, which represents the range of values that 82 * source variables can take. 83 * 84 * @param markerValues A list of {@link PropertyMarkerValue}s. 85 * @see #performMapping(Parallax) 86 */ 87 public final void setPropertyRanges(Parallax.PropertyMarkerValue... markerValues) { 88 mMarkerValues.clear(); 89 for (Parallax.PropertyMarkerValue markerValue : markerValues) { 90 mMarkerValues.add(markerValue); 91 } 92 } 93 94 /** 95 * Sets a list of Float objects that represents weight associated with each variable range. 96 * Weights are used when there are three or more marker values. 97 * 98 * @param weights A list of Float objects that represents weight associated with each variable 99 * range. 100 * @hide 101 */ 102 @RestrictTo(RestrictTo.Scope.LIBRARY) 103 public final void setWeights(float... weights) { 104 for (float weight : weights) { 105 if (weight <= 0) { 106 throw new IllegalArgumentException(); 107 } 108 } 109 mWeights.clear(); 110 mTotalWeights.clear(); 111 float totalWeight = 0f; 112 for (float weight : weights) { 113 mWeights.add(weight); 114 totalWeight += weight; 115 mTotalWeights.add(totalWeight); 116 } 117 } 118 119 /** 120 * Sets a list of Float objects that represents weight associated with each variable range. 121 * Weights are used when there are three or more marker values. 122 * 123 * @param weights A list of Float objects that represents weight associated with each variable 124 * range. 125 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 126 * @hide 127 */ 128 @RestrictTo(RestrictTo.Scope.LIBRARY) 129 public final ParallaxEffect weights(float... weights) { 130 setWeights(weights); 131 return this; 132 } 133 134 /** 135 * Add a ParallaxTarget to run parallax effect. 136 * 137 * @param target ParallaxTarget to add. 138 */ 139 public final void addTarget(ParallaxTarget target) { 140 mTargets.add(target); 141 } 142 143 /** 144 * Add a ParallaxTarget to run parallax effect. 145 * 146 * @param target ParallaxTarget to add. 147 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 148 */ 149 public final ParallaxEffect target(ParallaxTarget target) { 150 mTargets.add(target); 151 return this; 152 } 153 154 /** 155 * Creates a {@link ParallaxTarget} from {@link PropertyValuesHolder} and adds it to the list 156 * of targets. 157 * 158 * @param targetObject Target object for PropertyValuesHolderTarget. 159 * @param values PropertyValuesHolder for PropertyValuesHolderTarget. 160 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 161 */ 162 public final ParallaxEffect target(Object targetObject, PropertyValuesHolder values) { 163 mTargets.add(new ParallaxTarget.PropertyValuesHolderTarget(targetObject, values)); 164 return this; 165 } 166 167 /** 168 * Creates a {@link ParallaxTarget} using direct mapping from source property into target 169 * property, the new {@link ParallaxTarget} will be added to its list of targets. 170 * 171 * @param targetObject Target object for property. 172 * @param targetProperty The target property that will receive values. 173 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 174 * @param <T> Type of target object. 175 * @param <V> Type of target property value, either Integer or Float. 176 * @see ParallaxTarget#isDirectMapping() 177 */ 178 public final <T, V extends Number> ParallaxEffect target(T targetObject, 179 Property<T, V> targetProperty) { 180 mTargets.add(new ParallaxTarget.DirectPropertyTarget(targetObject, targetProperty)); 181 return this; 182 } 183 184 /** 185 * Returns the list of {@link ParallaxTarget} objects. 186 * 187 * @return The list of {@link ParallaxTarget} objects. 188 */ 189 public final List<ParallaxTarget> getTargets() { 190 return mTargets; 191 } 192 193 /** 194 * Remove a {@link ParallaxTarget} object from the list. 195 * @param target The {@link ParallaxTarget} object to be removed. 196 */ 197 public final void removeTarget(ParallaxTarget target) { 198 mTargets.remove(target); 199 } 200 201 /** 202 * Perform mapping from {@link Parallax} to list of {@link ParallaxTarget}. 203 */ 204 public final void performMapping(Parallax source) { 205 if (mMarkerValues.size() < 2) { 206 return; 207 } 208 if (this instanceof IntEffect) { 209 source.verifyIntProperties(); 210 } else { 211 source.verifyFloatProperties(); 212 } 213 boolean fractionCalculated = false; 214 float fraction = 0; 215 Number directValue = null; 216 for (int i = 0; i < mTargets.size(); i++) { 217 ParallaxTarget target = mTargets.get(i); 218 if (target.isDirectMapping()) { 219 if (directValue == null) { 220 directValue = calculateDirectValue(source); 221 } 222 target.directUpdate(directValue); 223 } else { 224 if (!fractionCalculated) { 225 fractionCalculated = true; 226 fraction = calculateFraction(source); 227 } 228 target.update(fraction); 229 } 230 } 231 } 232 233 /** 234 * This method is expected to compute a fraction between 0 and 1 based on the current values of 235 * variables in {@link Parallax}. As the parallax effect goes on, the fraction increases 236 * from 0 at beginning to 1 at the end. 237 * 238 * @return Float value between 0 and 1. 239 */ 240 abstract float calculateFraction(Parallax source); 241 242 /** 243 * This method is expected to get the current value of the single {@link IntProperty} or 244 * {@link FloatProperty}. 245 * 246 * @return Current value of the single {@link IntProperty} or {@link FloatProperty}. 247 */ 248 abstract Number calculateDirectValue(Parallax source); 249 250 /** 251 * When there are multiple ranges (aka three or more markerValues), this method adjust the 252 * fraction inside a range to fraction of whole range. 253 * e.g. four marker values, three weight values: 6, 2, 2. totalWeights are 6, 8, 10 254 * When markerValueIndex is 3, the fraction is inside last range. 255 * adjusted_fraction = 8 / 10 + 2 / 10 * fraction. 256 */ 257 final float getFractionWithWeightAdjusted(float fraction, int markerValueIndex) { 258 // when there are three or more markerValues, take weight into consideration. 259 if (mMarkerValues.size() >= 3) { 260 final boolean hasWeightsDefined = mWeights.size() == mMarkerValues.size() - 1; 261 if (hasWeightsDefined) { 262 // use weights user defined 263 final float allWeights = mTotalWeights.get(mTotalWeights.size() - 1); 264 fraction = fraction * mWeights.get(markerValueIndex - 1) / allWeights; 265 if (markerValueIndex >= 2) { 266 fraction += mTotalWeights.get(markerValueIndex - 2) / allWeights; 267 } 268 } else { 269 // assume each range has same weight. 270 final float allWeights = mMarkerValues.size() - 1; 271 fraction = fraction / allWeights; 272 if (markerValueIndex >= 2) { 273 fraction += (float) (markerValueIndex - 1) / allWeights; 274 } 275 } 276 } 277 return fraction; 278 } 279 280 /** 281 * Implementation of {@link ParallaxEffect} for integer type. 282 */ 283 static final class IntEffect extends ParallaxEffect { 284 285 @Override 286 Number calculateDirectValue(Parallax source) { 287 if (mMarkerValues.size() != 2) { 288 throw new RuntimeException("Must use two marker values for direct mapping"); 289 } 290 if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) { 291 throw new RuntimeException( 292 "Marker value must use same Property for direct mapping"); 293 } 294 int value1 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(0)) 295 .getMarkerValue(source); 296 int value2 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(1)) 297 .getMarkerValue(source); 298 if (value1 > value2) { 299 int swapValue = value2; 300 value2 = value1; 301 value1 = swapValue; 302 } 303 304 Number currentValue = ((IntProperty) mMarkerValues.get(0).getProperty()).get(source); 305 if (currentValue.intValue() < value1) { 306 currentValue = value1; 307 } else if (currentValue.intValue() > value2) { 308 currentValue = value2; 309 } 310 return currentValue; 311 } 312 313 @Override 314 float calculateFraction(Parallax source) { 315 int lastIndex = 0; 316 int lastValue = 0; 317 int lastMarkerValue = 0; 318 // go through all markerValues, find first markerValue that current value is less than. 319 for (int i = 0; i < mMarkerValues.size(); i++) { 320 Parallax.IntPropertyMarkerValue k = (Parallax.IntPropertyMarkerValue) 321 mMarkerValues.get(i); 322 int index = k.getProperty().getIndex(); 323 int markerValue = k.getMarkerValue(source); 324 int currentValue = source.getIntPropertyValue(index); 325 326 float fraction; 327 if (i == 0) { 328 if (currentValue >= markerValue) { 329 return 0f; 330 } 331 } else { 332 if (lastIndex == index && lastMarkerValue < markerValue) { 333 throw new IllegalStateException("marker value of same variable must be " 334 + "descendant order"); 335 } 336 if (currentValue == IntProperty.UNKNOWN_AFTER) { 337 // Implies lastValue is less than lastMarkerValue and lastValue is not 338 // UNKNWON_AFTER. Estimates based on distance of two variables is screen 339 // size. 340 fraction = (float) (lastMarkerValue - lastValue) 341 / source.getMaxValue(); 342 return getFractionWithWeightAdjusted(fraction, i); 343 } else if (currentValue >= markerValue) { 344 if (lastIndex == index) { 345 // same variable index, same UI element at two different MarkerValues, 346 // e.g. UI element moves from lastMarkerValue=500 to markerValue=0, 347 // fraction moves from 0 to 1. 348 fraction = (float) (lastMarkerValue - currentValue) 349 / (lastMarkerValue - markerValue); 350 } else if (lastValue != IntProperty.UNKNOWN_BEFORE) { 351 // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when 352 // UIElement_1 is at markerValue=300, markerValue of UIElement_2 by 353 // adding delta of values to markerValue of UIElement_2. 354 lastMarkerValue = lastMarkerValue + (currentValue - lastValue); 355 fraction = (float) (lastMarkerValue - currentValue) 356 / (lastMarkerValue - markerValue); 357 } else { 358 // Last variable is UNKNOWN_BEFORE. Estimates based on assumption total 359 // travel distance from last variable to this variable is screen visible 360 // size. 361 fraction = 1f - (float) (currentValue - markerValue) 362 / source.getMaxValue(); 363 } 364 return getFractionWithWeightAdjusted(fraction, i); 365 } 366 } 367 lastValue = currentValue; 368 lastIndex = index; 369 lastMarkerValue = markerValue; 370 } 371 return 1f; 372 } 373 } 374 375 /** 376 * Implementation of {@link ParallaxEffect} for float type. 377 */ 378 static final class FloatEffect extends ParallaxEffect { 379 380 @Override 381 Number calculateDirectValue(Parallax source) { 382 if (mMarkerValues.size() != 2) { 383 throw new RuntimeException("Must use two marker values for direct mapping"); 384 } 385 if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) { 386 throw new RuntimeException( 387 "Marker value must use same Property for direct mapping"); 388 } 389 float value1 = ((FloatPropertyMarkerValue) mMarkerValues.get(0)) 390 .getMarkerValue(source); 391 float value2 = ((FloatPropertyMarkerValue) mMarkerValues.get(1)) 392 .getMarkerValue(source); 393 if (value1 > value2) { 394 float swapValue = value2; 395 value2 = value1; 396 value1 = swapValue; 397 } 398 399 Number currentValue = ((FloatProperty) mMarkerValues.get(0).getProperty()).get(source); 400 if (currentValue.floatValue() < value1) { 401 currentValue = value1; 402 } else if (currentValue.floatValue() > value2) { 403 currentValue = value2; 404 } 405 return currentValue; 406 } 407 408 @Override 409 float calculateFraction(Parallax source) { 410 int lastIndex = 0; 411 float lastValue = 0; 412 float lastMarkerValue = 0; 413 // go through all markerValues, find first markerValue that current value is less than. 414 for (int i = 0; i < mMarkerValues.size(); i++) { 415 FloatPropertyMarkerValue k = (FloatPropertyMarkerValue) mMarkerValues.get(i); 416 int index = k.getProperty().getIndex(); 417 float markerValue = k.getMarkerValue(source); 418 float currentValue = source.getFloatPropertyValue(index); 419 420 float fraction; 421 if (i == 0) { 422 if (currentValue >= markerValue) { 423 return 0f; 424 } 425 } else { 426 if (lastIndex == index && lastMarkerValue < markerValue) { 427 throw new IllegalStateException("marker value of same variable must be " 428 + "descendant order"); 429 } 430 if (currentValue == FloatProperty.UNKNOWN_AFTER) { 431 // Implies lastValue is less than lastMarkerValue and lastValue is not 432 // UNKNOWN_AFTER. Estimates based on distance of two variables is screen 433 // size. 434 fraction = (float) (lastMarkerValue - lastValue) 435 / source.getMaxValue(); 436 return getFractionWithWeightAdjusted(fraction, i); 437 } else if (currentValue >= markerValue) { 438 if (lastIndex == index) { 439 // same variable index, same UI element at two different MarkerValues, 440 // e.g. UI element moves from lastMarkerValue=500 to markerValue=0, 441 // fraction moves from 0 to 1. 442 fraction = (float) (lastMarkerValue - currentValue) 443 / (lastMarkerValue - markerValue); 444 } else if (lastValue != FloatProperty.UNKNOWN_BEFORE) { 445 // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when 446 // UIElement_1 is at markerValue=300, markerValue of UIElement_2 by 447 // adding delta of values to markerValue of UIElement_2. 448 lastMarkerValue = lastMarkerValue + (currentValue - lastValue); 449 fraction = (float) (lastMarkerValue - currentValue) 450 / (lastMarkerValue - markerValue); 451 } else { 452 // Last variable is UNKNOWN_BEFORE. Estimates based on assumption total 453 // travel distance from last variable to this variable is screen visible 454 // size. 455 fraction = 1f - (float) (currentValue - markerValue) 456 / source.getMaxValue(); 457 } 458 return getFractionWithWeightAdjusted(fraction, i); 459 } 460 } 461 lastValue = currentValue; 462 lastIndex = index; 463 lastMarkerValue = markerValue; 464 } 465 return 1f; 466 } 467 } 468 469 } 470 471