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