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
17package android.support.v17.leanback.widget;
18
19import android.support.annotation.CallSuper;
20import android.support.v17.leanback.widget.ParallaxEffect.FloatEffect;
21import android.support.v17.leanback.widget.ParallaxEffect.IntEffect;
22import android.util.Property;
23
24import java.util.ArrayList;
25import java.util.Collections;
26import 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  Subclass of {@link Parallax.IntProperty} or {@link Parallax.FloatProperty}
76 */
77public abstract class Parallax<PropertyT extends android.util.Property> {
78
79    /**
80     * Class holding a fixed value for a Property in {@link Parallax}.
81     * @param  Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}.
82     */
83    public static class PropertyMarkerValue<PropertyT> {
84        private final PropertyT mProperty;
85
86        public PropertyMarkerValue(PropertyT property) {
87            mProperty = property;
88        }
89
90        /**
91         * @return Associated property.
92         */
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         */
125        public IntProperty(String name, int index) {
126            super(Integer.class, name);
127            mIndex = index;
128        }
129
130        @Override
131        public final Integer get(Parallax object) {
132            return object.getIntPropertyValue(mIndex);
133        }
134
135        @Override
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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
232        IntPropertyMarkerValue(IntProperty property, int value) {
233            this(property, value, 0f);
234        }
235
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         */
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         */
278        public FloatProperty(String name, int index) {
279            super(Float.class, name);
280            mIndex = index;
281        }
282
283        @Override
284        public final Float get(Parallax object) {
285            return object.getFloatPropertyValue(mIndex);
286        }
287
288        @Override
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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
384        FloatPropertyMarkerValue(FloatProperty property, float value) {
385            this(property, value, 0f);
386        }
387
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         */
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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
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     */
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     */
598    public void removeEffect(ParallaxEffect effect) {
599        mEffects.remove(effect);
600    }
601
602    /**
603     * Remove all {@link ParallaxEffect} objects.
604     */
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     */
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