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