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 android.support.v17.leanback.widget; 18 19 import android.support.annotation.CallSuper; 20 import android.support.v17.leanback.widget.ParallaxEffect.FloatEffect; 21 import android.support.v17.leanback.widget.ParallaxEffect.IntEffect; 22 import android.util.Property; 23 24 import java.util.ArrayList; 25 import java.util.Collections; 26 import java.util.List; 27 28 /** 29 * Parallax tracks a list of dynamic {@link Property}s typically representing foreground UI 30 * element positions on screen. Parallax keeps a list of {@link ParallaxEffect} objects which define 31 * rules to mapping property values to {@link ParallaxTarget}. 32 * 33 * <p> 34 * Example: 35 * <code> 36 * // when Property "var1" changes from 15 to max value, perform parallax effect to 37 * // change myView's translationY from 0 to 100. 38 * Parallax<IntProperty> parallax = new Parallax<IntProperty>() {...}; 39 * p1 = parallax.addProperty("var1"); 40 * parallax.addEffect(p1.at(15), p1.atMax()) 41 * .target(myView, PropertyValuesHolder.ofFloat("translationY", 0, 100)); 42 * </code> 43 * </p> 44 * 45 * <p> 46 * To create a {@link ParallaxEffect}, user calls {@link #addEffect(PropertyMarkerValue[])} with a 47 * list of {@link PropertyMarkerValue} which defines the range of {@link Parallax.IntProperty} or 48 * {@link Parallax.FloatProperty}. Then user adds {@link ParallaxTarget} into 49 * {@link ParallaxEffect}. 50 * </p> 51 * <p> 52 * App may subclass {@link Parallax.IntProperty} or {@link Parallax.FloatProperty} to supply 53 * additional information about how to retrieve Property value. {@link RecyclerViewParallax} is 54 * a great example of Parallax implementation tracking child view positions on screen. 55 * </p> 56 * <p> 57 * <ul>Restrictions of properties 58 * <li>FloatProperty and IntProperty cannot be mixed in one Parallax</li> 59 * <li>Values must be in ascending order.</li> 60 * <li>If the UI element is unknown above screen, use UNKNOWN_BEFORE.</li> 61 * <li>if the UI element is unknown below screen, use UNKNOWN_AFTER.</li> 62 * <li>UNKNOWN_BEFORE and UNKNOWN_AFTER are not allowed to be next to each other.</li> 63 * </ul> 64 * These rules will be verified at runtime. 65 * </p> 66 * <p> 67 * Subclass must override {@link #updateValues()} to update property values and perform 68 * {@link ParallaxEffect}s. Subclass may call {@link #updateValues()} automatically e.g. 69 * {@link RecyclerViewParallax} calls {@link #updateValues()} in RecyclerView scrolling. App might 70 * call {@link #updateValues()} manually when Parallax is unaware of the value change. For example, 71 * when a slide transition is running, {@link RecyclerViewParallax} is unaware of translation value 72 * changes; it's the app's responsibility to call {@link #updateValues()} in every frame of 73 * animation. 74 * </p> 75 * @param <PropertyT> Subclass of {@link Parallax.IntProperty} or {@link Parallax.FloatProperty} 76 */ 77 public abstract class Parallax<PropertyT extends android.util.Property> { 78 79 /** 80 * Class holding a fixed value for a Property in {@link Parallax}. 81 * @param <PropertyT> Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}. 82 */ 83 public static class PropertyMarkerValue<PropertyT> { 84 private final PropertyT mProperty; 85 PropertyMarkerValue(PropertyT property)86 public PropertyMarkerValue(PropertyT property) { 87 mProperty = property; 88 } 89 90 /** 91 * @return Associated property. 92 */ getProperty()93 public PropertyT getProperty() { 94 return mProperty; 95 } 96 } 97 98 /** 99 * IntProperty provide access to an index based integer type property inside 100 * {@link Parallax}. The IntProperty typically represents UI element position inside 101 * {@link Parallax}. 102 */ 103 public static class IntProperty extends Property<Parallax, Integer> { 104 105 /** 106 * Property value is unknown and it's smaller than minimal value of Parallax. For 107 * example if a child is not created and before the first visible child of RecyclerView. 108 */ 109 public static final int UNKNOWN_BEFORE = Integer.MIN_VALUE; 110 111 /** 112 * Property value is unknown and it's larger than {@link Parallax#getMaxValue()}. For 113 * example if a child is not created and after the last visible child of RecyclerView. 114 */ 115 public static final int UNKNOWN_AFTER = Integer.MAX_VALUE; 116 117 private final int mIndex; 118 119 /** 120 * Constructor. 121 * 122 * @param name Name of this Property. 123 * @param index Index of this Property inside {@link Parallax}. 124 */ IntProperty(String name, int index)125 public IntProperty(String name, int index) { 126 super(Integer.class, name); 127 mIndex = index; 128 } 129 130 @Override get(Parallax object)131 public final Integer get(Parallax object) { 132 return object.getIntPropertyValue(mIndex); 133 } 134 135 @Override set(Parallax object, Integer value)136 public final void set(Parallax object, Integer value) { 137 object.setIntPropertyValue(mIndex, value); 138 } 139 140 /** 141 * @return Index of this Property in {@link Parallax}. 142 */ getIndex()143 public final int getIndex() { 144 return mIndex; 145 } 146 147 /** 148 * Fast version of get() method that returns a primitive int value of the Property. 149 * @param object The Parallax object that owns this Property. 150 * @return Int value of the Property. 151 */ getValue(Parallax object)152 public final int getValue(Parallax object) { 153 return object.getIntPropertyValue(mIndex); 154 } 155 156 /** 157 * Fast version of set() method that takes a primitive int value into the Property. 158 * 159 * @param object The Parallax object that owns this Property. 160 * @param value Int value of the Property. 161 */ setValue(Parallax object, int value)162 public final void setValue(Parallax object, int value) { 163 object.setIntPropertyValue(mIndex, value); 164 } 165 166 /** 167 * Creates an {@link PropertyMarkerValue} object for the absolute marker value. 168 * 169 * @param absoluteValue The integer marker value. 170 * @return A new {@link PropertyMarkerValue} object. 171 */ atAbsolute(int absoluteValue)172 public final PropertyMarkerValue atAbsolute(int absoluteValue) { 173 return new IntPropertyMarkerValue(this, absoluteValue, 0f); 174 } 175 176 /** 177 * Creates an {@link PropertyMarkerValue} object for the marker value representing 178 * {@link Parallax#getMaxValue()}. 179 * 180 * @return A new {@link PropertyMarkerValue} object. 181 */ atMax()182 public final PropertyMarkerValue atMax() { 183 return new IntPropertyMarkerValue(this, 0, 1f); 184 } 185 186 /** 187 * Creates an {@link PropertyMarkerValue} object for the marker value representing 0. 188 * 189 * @return A new {@link PropertyMarkerValue} object. 190 */ atMin()191 public final PropertyMarkerValue atMin() { 192 return new IntPropertyMarkerValue(this, 0); 193 } 194 195 /** 196 * Creates an {@link PropertyMarkerValue} object for a fraction of 197 * {@link Parallax#getMaxValue()}. 198 * 199 * @param fractionOfMaxValue 0 to 1 fraction to multiply with 200 * {@link Parallax#getMaxValue()} for 201 * the marker value. 202 * @return A new {@link PropertyMarkerValue} object. 203 */ atFraction(float fractionOfMaxValue)204 public final PropertyMarkerValue atFraction(float fractionOfMaxValue) { 205 return new IntPropertyMarkerValue(this, 0, fractionOfMaxValue); 206 } 207 208 /** 209 * Create an {@link PropertyMarkerValue} object by multiplying the fraction with 210 * {@link Parallax#getMaxValue()} and adding offsetValue to it. 211 * 212 * @param offsetValue An offset integer value to be added to marker 213 * value. 214 * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with 215 * {@link Parallax#getMaxValue()} for 216 * the marker value. 217 * @return A new {@link PropertyMarkerValue} object. 218 */ at(int offsetValue, float fractionOfMaxParentVisibleSize)219 public final PropertyMarkerValue at(int offsetValue, 220 float fractionOfMaxParentVisibleSize) { 221 return new IntPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize); 222 } 223 } 224 225 /** 226 * Implementation of {@link PropertyMarkerValue} for {@link IntProperty}. 227 */ 228 static class IntPropertyMarkerValue extends PropertyMarkerValue<IntProperty> { 229 private final int mValue; 230 private final float mFactionOfMax; 231 IntPropertyMarkerValue(IntProperty property, int value)232 IntPropertyMarkerValue(IntProperty property, int value) { 233 this(property, value, 0f); 234 } 235 IntPropertyMarkerValue(IntProperty property, int value, float fractionOfMax)236 IntPropertyMarkerValue(IntProperty property, int value, float fractionOfMax) { 237 super(property); 238 mValue = value; 239 mFactionOfMax = fractionOfMax; 240 } 241 242 /** 243 * @return The marker value of integer type. 244 */ getMarkerValue(Parallax source)245 final int getMarkerValue(Parallax source) { 246 return mFactionOfMax == 0 ? mValue : mValue + Math.round(source 247 .getMaxValue() * mFactionOfMax); 248 } 249 } 250 251 /** 252 * FloatProperty provide access to an index based integer type property inside 253 * {@link Parallax}. The FloatProperty typically represents UI element position inside 254 * {@link Parallax}. 255 */ 256 public static class FloatProperty extends Property<Parallax, Float> { 257 258 /** 259 * Property value is unknown and it's smaller than minimal value of Parallax. For 260 * example if a child is not created and before the first visible child of RecyclerView. 261 */ 262 public static final float UNKNOWN_BEFORE = -Float.MAX_VALUE; 263 264 /** 265 * Property value is unknown and it's larger than {@link Parallax#getMaxValue()}. For 266 * example if a child is not created and after the last visible child of RecyclerView. 267 */ 268 public static final float UNKNOWN_AFTER = Float.MAX_VALUE; 269 270 private final int mIndex; 271 272 /** 273 * Constructor. 274 * 275 * @param name Name of this Property. 276 * @param index Index of this Property inside {@link Parallax}. 277 */ FloatProperty(String name, int index)278 public FloatProperty(String name, int index) { 279 super(Float.class, name); 280 mIndex = index; 281 } 282 283 @Override get(Parallax object)284 public final Float get(Parallax object) { 285 return object.getFloatPropertyValue(mIndex); 286 } 287 288 @Override set(Parallax object, Float value)289 public final void set(Parallax object, Float value) { 290 object.setFloatPropertyValue(mIndex, value); 291 } 292 293 /** 294 * @return Index of this Property in {@link Parallax}. 295 */ getIndex()296 public final int getIndex() { 297 return mIndex; 298 } 299 300 /** 301 * Fast version of get() method that returns a primitive int value of the Property. 302 * @param object The Parallax object that owns this Property. 303 * @return Float value of the Property. 304 */ getValue(Parallax object)305 public final float getValue(Parallax object) { 306 return object.getFloatPropertyValue(mIndex); 307 } 308 309 /** 310 * Fast version of set() method that takes a primitive float value into the Property. 311 * 312 * @param object The Parallax object that owns this Property. 313 * @param value Float value of the Property. 314 */ setValue(Parallax object, float value)315 public final void setValue(Parallax object, float value) { 316 object.setFloatPropertyValue(mIndex, value); 317 } 318 319 /** 320 * Creates an {@link PropertyMarkerValue} object for the absolute marker value. 321 * 322 * @param markerValue The float marker value. 323 * @return A new {@link PropertyMarkerValue} object. 324 */ atAbsolute(float markerValue)325 public final PropertyMarkerValue atAbsolute(float markerValue) { 326 return new FloatPropertyMarkerValue(this, markerValue, 0f); 327 } 328 329 /** 330 * Creates an {@link PropertyMarkerValue} object for the marker value representing 331 * {@link Parallax#getMaxValue()}. 332 * 333 * @return A new {@link PropertyMarkerValue} object. 334 */ atMax()335 public final PropertyMarkerValue atMax() { 336 return new FloatPropertyMarkerValue(this, 0, 1f); 337 } 338 339 /** 340 * Creates an {@link PropertyMarkerValue} object for the marker value representing 0. 341 * 342 * @return A new {@link PropertyMarkerValue} object. 343 */ atMin()344 public final PropertyMarkerValue atMin() { 345 return new FloatPropertyMarkerValue(this, 0); 346 } 347 348 /** 349 * Creates an {@link PropertyMarkerValue} object for a fraction of 350 * {@link Parallax#getMaxValue()}. 351 * 352 * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with 353 * {@link Parallax#getMaxValue()} for 354 * the marker value. 355 * @return A new {@link PropertyMarkerValue} object. 356 */ atFraction(float fractionOfMaxParentVisibleSize)357 public final PropertyMarkerValue atFraction(float fractionOfMaxParentVisibleSize) { 358 return new FloatPropertyMarkerValue(this, 0, fractionOfMaxParentVisibleSize); 359 } 360 361 /** 362 * Create an {@link PropertyMarkerValue} object by multiplying the fraction with 363 * {@link Parallax#getMaxValue()} and adding offsetValue to it. 364 * 365 * @param offsetValue An offset float value to be added to marker value. 366 * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with 367 * {@link Parallax#getMaxValue()} for 368 * the marker value. 369 * @return A new {@link PropertyMarkerValue} object. 370 */ at(float offsetValue, float fractionOfMaxParentVisibleSize)371 public final PropertyMarkerValue at(float offsetValue, 372 float fractionOfMaxParentVisibleSize) { 373 return new FloatPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize); 374 } 375 } 376 377 /** 378 * Implementation of {@link PropertyMarkerValue} for {@link FloatProperty}. 379 */ 380 static class FloatPropertyMarkerValue extends PropertyMarkerValue<FloatProperty> { 381 private final float mValue; 382 private final float mFactionOfMax; 383 FloatPropertyMarkerValue(FloatProperty property, float value)384 FloatPropertyMarkerValue(FloatProperty property, float value) { 385 this(property, value, 0f); 386 } 387 FloatPropertyMarkerValue(FloatProperty property, float value, float fractionOfMax)388 FloatPropertyMarkerValue(FloatProperty property, float value, float fractionOfMax) { 389 super(property); 390 mValue = value; 391 mFactionOfMax = fractionOfMax; 392 } 393 394 /** 395 * @return The marker value. 396 */ getMarkerValue(Parallax source)397 final float getMarkerValue(Parallax source) { 398 return mFactionOfMax == 0 ? mValue : mValue + source.getMaxValue() 399 * mFactionOfMax; 400 } 401 } 402 403 final List<PropertyT> mProperties = new ArrayList<PropertyT>(); 404 final List<PropertyT> mPropertiesReadOnly = Collections.unmodifiableList(mProperties); 405 406 private int[] mValues = new int[4]; 407 private float[] mFloatValues = new float[4]; 408 409 private final List<ParallaxEffect> mEffects = new ArrayList<ParallaxEffect>(4); 410 411 /** 412 * Return the max value which is typically size of parent visible area, e.g. RecyclerView's 413 * height if we are tracking Y position of a child. The size can be used to calculate marker 414 * value using the provided fraction of FloatPropertyMarkerValue. 415 * 416 * @return Size of parent visible area. 417 * @see IntPropertyMarkerValue#IntPropertyMarkerValue(IntProperty, int, float) 418 * @see FloatPropertyMarkerValue#FloatPropertyMarkerValue(FloatProperty, float, float) 419 */ getMaxValue()420 public abstract float getMaxValue(); 421 422 /** 423 * Get index based property value. 424 * 425 * @param index Index of the property. 426 * @return Value of the property. 427 */ getIntPropertyValue(int index)428 final int getIntPropertyValue(int index) { 429 return mValues[index]; 430 } 431 432 /** 433 * Set index based property value. 434 * 435 * @param index Index of the property. 436 * @param value Value of the property. 437 */ setIntPropertyValue(int index, int value)438 final void setIntPropertyValue(int index, int value) { 439 if (index >= mProperties.size()) { 440 throw new ArrayIndexOutOfBoundsException(); 441 } 442 mValues[index] = value; 443 } 444 445 /** 446 * Add a new IntProperty in the Parallax object. App may override 447 * {@link #createProperty(String, int)}. 448 * 449 * @param name Name of the property. 450 * @return Newly created Property object. 451 * @see #createProperty(String, int) 452 */ addProperty(String name)453 public final PropertyT addProperty(String name) { 454 int newPropertyIndex = mProperties.size(); 455 PropertyT property = createProperty(name, newPropertyIndex); 456 if (property instanceof IntProperty) { 457 int size = mValues.length; 458 if (size == newPropertyIndex) { 459 int[] newValues = new int[size * 2]; 460 for (int i = 0; i < size; i++) { 461 newValues[i] = mValues[i]; 462 } 463 mValues = newValues; 464 } 465 mValues[newPropertyIndex] = IntProperty.UNKNOWN_AFTER; 466 } else if (property instanceof FloatProperty) { 467 int size = mFloatValues.length; 468 if (size == newPropertyIndex) { 469 float[] newValues = new float[size * 2]; 470 for (int i = 0; i < size; i++) { 471 newValues[i] = mFloatValues[i]; 472 } 473 mFloatValues = newValues; 474 } 475 mFloatValues[newPropertyIndex] = FloatProperty.UNKNOWN_AFTER; 476 } else { 477 throw new IllegalArgumentException("Invalid Property type"); 478 } 479 mProperties.add(property); 480 return property; 481 } 482 483 /** 484 * Verify sanity of property values, throws RuntimeException if fails. The property values 485 * must be in ascending order. UNKNOW_BEFORE and UNKNOWN_AFTER are not allowed to be next to 486 * each other. 487 */ verifyIntProperties()488 void verifyIntProperties() throws IllegalStateException { 489 if (mProperties.size() < 2) { 490 return; 491 } 492 int last = getIntPropertyValue(0); 493 for (int i = 1; i < mProperties.size(); i++) { 494 int v = getIntPropertyValue(i); 495 if (v < last) { 496 throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is" 497 + " smaller than Property[%d]\"%s\"", 498 i, mProperties.get(i).getName(), 499 i - 1, mProperties.get(i - 1).getName())); 500 } else if (last == IntProperty.UNKNOWN_BEFORE && v == IntProperty.UNKNOWN_AFTER) { 501 throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is" 502 + " UNKNOWN_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER", 503 i - 1, mProperties.get(i - 1).getName(), 504 i, mProperties.get(i).getName())); 505 } 506 last = v; 507 } 508 } 509 verifyFloatProperties()510 final void verifyFloatProperties() throws IllegalStateException { 511 if (mProperties.size() < 2) { 512 return; 513 } 514 float last = getFloatPropertyValue(0); 515 for (int i = 1; i < mProperties.size(); i++) { 516 float v = getFloatPropertyValue(i); 517 if (v < last) { 518 throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is" 519 + " smaller than Property[%d]\"%s\"", 520 i, mProperties.get(i).getName(), 521 i - 1, mProperties.get(i - 1).getName())); 522 } else if (last == FloatProperty.UNKNOWN_BEFORE && v 523 == FloatProperty.UNKNOWN_AFTER) { 524 throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is" 525 + " UNKNOWN_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER", 526 i - 1, mProperties.get(i - 1).getName(), 527 i, mProperties.get(i).getName())); 528 } 529 last = v; 530 } 531 } 532 533 /** 534 * Get index based property value. 535 * 536 * @param index Index of the property. 537 * @return Value of the property. 538 */ getFloatPropertyValue(int index)539 final float getFloatPropertyValue(int index) { 540 return mFloatValues[index]; 541 } 542 543 /** 544 * Set index based property value. 545 * 546 * @param index Index of the property. 547 * @param value Value of the property. 548 */ setFloatPropertyValue(int index, float value)549 final void setFloatPropertyValue(int index, float value) { 550 if (index >= mProperties.size()) { 551 throw new ArrayIndexOutOfBoundsException(); 552 } 553 mFloatValues[index] = value; 554 } 555 556 /** 557 * @return A unmodifiable list of properties. 558 */ getProperties()559 public final List<PropertyT> getProperties() { 560 return mPropertiesReadOnly; 561 } 562 563 /** 564 * Create a new Property object. App does not directly call this method. See 565 * {@link #addProperty(String)}. 566 * 567 * @param index Index of the property in this Parallax object. 568 * @return Newly created Property object. 569 */ createProperty(String name, int index)570 public abstract PropertyT createProperty(String name, int index); 571 572 /** 573 * Update property values and perform {@link ParallaxEffect}s. Subclass may override and call 574 * super.updateValues() after updated properties values. 575 */ 576 @CallSuper updateValues()577 public void updateValues() { 578 for (int i = 0; i < mEffects.size(); i++) { 579 mEffects.get(i).performMapping(this); 580 } 581 } 582 583 /** 584 * Returns a list of {@link ParallaxEffect} object which defines rules to perform mapping to 585 * multiple {@link ParallaxTarget}s. 586 * 587 * @return A list of {@link ParallaxEffect} object. 588 */ getEffects()589 public List<ParallaxEffect> getEffects() { 590 return mEffects; 591 } 592 593 /** 594 * Remove the {@link ParallaxEffect} object. 595 * 596 * @param effect The {@link ParallaxEffect} object to remove. 597 */ removeEffect(ParallaxEffect effect)598 public void removeEffect(ParallaxEffect effect) { 599 mEffects.remove(effect); 600 } 601 602 /** 603 * Remove all {@link ParallaxEffect} objects. 604 */ removeAllEffects()605 public void removeAllEffects() { 606 mEffects.clear(); 607 } 608 609 /** 610 * Create a {@link ParallaxEffect} object that will track source variable changes within a 611 * provided set of ranges. 612 * 613 * @param ranges A list of marker values that defines the ranges. 614 * @return Newly created ParallaxEffect object. 615 */ addEffect(PropertyMarkerValue... ranges)616 public ParallaxEffect addEffect(PropertyMarkerValue... ranges) { 617 ParallaxEffect effect; 618 if (ranges[0].getProperty() instanceof IntProperty) { 619 effect = new IntEffect(); 620 } else { 621 effect = new FloatEffect(); 622 } 623 effect.setPropertyRanges(ranges); 624 mEffects.add(effect); 625 return effect; 626 } 627 628 } 629