Home | History | Annotate | Download | only in widget
      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  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  Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}.
     83      */
     84     public static class PropertyMarkerValue<PropertyT> {
     85         private final PropertyT mProperty;
     86 
     87         public PropertyMarkerValue(PropertyT property) {
     88             mProperty = property;
     89         }
     90 
     91         /**
     92          * @return Associated property.
     93          */
     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          */
    126         public IntProperty(String name, int index) {
    127             super(Integer.class, name);
    128             mIndex = index;
    129         }
    130 
    131         @Override
    132         public final Integer get(Parallax object) {
    133             return object.getIntPropertyValue(mIndex);
    134         }
    135 
    136         @Override
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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 
    233         IntPropertyMarkerValue(IntProperty property, int value) {
    234             this(property, value, 0f);
    235         }
    236 
    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          */
    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          */
    279         public FloatProperty(String name, int index) {
    280             super(Float.class, name);
    281             mIndex = index;
    282         }
    283 
    284         @Override
    285         public final Float get(Parallax object) {
    286             return object.getFloatPropertyValue(mIndex);
    287         }
    288 
    289         @Override
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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 
    385         FloatPropertyMarkerValue(FloatProperty property, float value) {
    386             this(property, value, 0f);
    387         }
    388 
    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          */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    599     public void removeEffect(ParallaxEffect effect) {
    600         mEffects.remove(effect);
    601     }
    602 
    603     /**
    604      * Remove all {@link ParallaxEffect} objects.
    605      */
    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      */
    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