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.animation.PropertyValuesHolder;
     20 import android.util.Property;
     21 
     22 import androidx.annotation.RestrictTo;
     23 import androidx.leanback.widget.Parallax.FloatProperty;
     24 import androidx.leanback.widget.Parallax.FloatPropertyMarkerValue;
     25 import androidx.leanback.widget.Parallax.IntProperty;
     26 import androidx.leanback.widget.Parallax.PropertyMarkerValue;
     27 
     28 import java.util.ArrayList;
     29 import java.util.List;
     30 
     31 /**
     32  * ParallaxEffect class drives changes in {@link ParallaxTarget} in response to changes in
     33  * variables defined in {@link Parallax}.
     34  * <p>
     35  * ParallaxEffect has a list of {@link Parallax.PropertyMarkerValue}s which represents the range of
     36  * values that source variables can take. The main function is
     37  * {@link ParallaxEffect#performMapping(Parallax)} which computes a fraction between 0 and 1
     38  * based on the current values of variables in {@link Parallax}. As the parallax effect goes
     39  * on, the fraction increases from 0 at beginning to 1 at the end. Then the fraction is passed on
     40  * to {@link ParallaxTarget#update(float)}.
     41  * <p>
     42  * App use {@link Parallax#addEffect(PropertyMarkerValue...)} to create a ParallaxEffect.
     43  */
     44 public abstract class ParallaxEffect {
     45 
     46     final List<Parallax.PropertyMarkerValue> mMarkerValues = new ArrayList(2);
     47     final List<Float> mWeights = new ArrayList<Float>(2);
     48     final List<Float> mTotalWeights = new ArrayList<Float>(2);
     49     final List<ParallaxTarget> mTargets = new ArrayList<ParallaxTarget>(4);
     50 
     51     /**
     52      * Only accessible from package
     53      */
     54     ParallaxEffect() {
     55     }
     56 
     57     /**
     58      * Returns the list of {@link PropertyMarkerValue}s, which represents the range of values that
     59      * source variables can take.
     60      *
     61      * @return A list of {@link Parallax.PropertyMarkerValue}s.
     62      * @see #performMapping(Parallax)
     63      */
     64     public final List<Parallax.PropertyMarkerValue> getPropertyRanges() {
     65         return mMarkerValues;
     66     }
     67 
     68     /**
     69      * Returns a list of Float objects that represents weight associated with each variable range.
     70      * Weights are used when there are three or more marker values.
     71      *
     72      * @return A list of Float objects that represents weight associated with each variable range.
     73      * @hide
     74      */
     75     @RestrictTo(RestrictTo.Scope.LIBRARY)
     76     public final List<Float> getWeights() {
     77         return mWeights;
     78     }
     79 
     80     /**
     81      * Sets the list of {@link PropertyMarkerValue}s, which represents the range of values that
     82      * source variables can take.
     83      *
     84      * @param markerValues A list of {@link PropertyMarkerValue}s.
     85      * @see #performMapping(Parallax)
     86      */
     87     public final void setPropertyRanges(Parallax.PropertyMarkerValue... markerValues) {
     88         mMarkerValues.clear();
     89         for (Parallax.PropertyMarkerValue markerValue : markerValues) {
     90             mMarkerValues.add(markerValue);
     91         }
     92     }
     93 
     94     /**
     95      * Sets a list of Float objects that represents weight associated with each variable range.
     96      * Weights are used when there are three or more marker values.
     97      *
     98      * @param weights A list of Float objects that represents weight associated with each variable
     99      *                range.
    100      * @hide
    101      */
    102     @RestrictTo(RestrictTo.Scope.LIBRARY)
    103     public final void setWeights(float... weights) {
    104         for (float weight : weights) {
    105             if (weight <= 0) {
    106                 throw new IllegalArgumentException();
    107             }
    108         }
    109         mWeights.clear();
    110         mTotalWeights.clear();
    111         float totalWeight = 0f;
    112         for (float weight : weights) {
    113             mWeights.add(weight);
    114             totalWeight += weight;
    115             mTotalWeights.add(totalWeight);
    116         }
    117     }
    118 
    119     /**
    120      * Sets a list of Float objects that represents weight associated with each variable range.
    121      * Weights are used when there are three or more marker values.
    122      *
    123      * @param weights A list of Float objects that represents weight associated with each variable
    124      *                range.
    125      * @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
    126      * @hide
    127      */
    128     @RestrictTo(RestrictTo.Scope.LIBRARY)
    129     public final ParallaxEffect weights(float... weights) {
    130         setWeights(weights);
    131         return this;
    132     }
    133 
    134     /**
    135      * Add a ParallaxTarget to run parallax effect.
    136      *
    137      * @param target ParallaxTarget to add.
    138      */
    139     public final void addTarget(ParallaxTarget target) {
    140         mTargets.add(target);
    141     }
    142 
    143     /**
    144      * Add a ParallaxTarget to run parallax effect.
    145      *
    146      * @param target ParallaxTarget to add.
    147      * @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
    148      */
    149     public final ParallaxEffect target(ParallaxTarget target) {
    150         mTargets.add(target);
    151         return this;
    152     }
    153 
    154     /**
    155      * Creates a {@link ParallaxTarget} from {@link PropertyValuesHolder} and adds it to the list
    156      * of targets.
    157      *
    158      * @param targetObject Target object for PropertyValuesHolderTarget.
    159      * @param values       PropertyValuesHolder for PropertyValuesHolderTarget.
    160      * @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
    161      */
    162     public final ParallaxEffect target(Object targetObject, PropertyValuesHolder values) {
    163         mTargets.add(new ParallaxTarget.PropertyValuesHolderTarget(targetObject, values));
    164         return this;
    165     }
    166 
    167     /**
    168      * Creates a {@link ParallaxTarget} using direct mapping from source property into target
    169      * property, the new {@link ParallaxTarget} will be added to its list of targets.
    170      *
    171      * @param targetObject Target object for property.
    172      * @param targetProperty The target property that will receive values.
    173      * @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
    174      * @param <T> Type of target object.
    175      * @param <V> Type of target property value, either Integer or Float.
    176      * @see ParallaxTarget#isDirectMapping()
    177      */
    178     public final <T, V extends Number> ParallaxEffect target(T targetObject,
    179             Property<T, V> targetProperty) {
    180         mTargets.add(new ParallaxTarget.DirectPropertyTarget(targetObject, targetProperty));
    181         return this;
    182     }
    183 
    184     /**
    185      * Returns the list of {@link ParallaxTarget} objects.
    186      *
    187      * @return The list of {@link ParallaxTarget} objects.
    188      */
    189     public final List<ParallaxTarget> getTargets() {
    190         return mTargets;
    191     }
    192 
    193     /**
    194      * Remove a {@link ParallaxTarget} object from the list.
    195      * @param target The {@link ParallaxTarget} object to be removed.
    196      */
    197     public final void removeTarget(ParallaxTarget target) {
    198         mTargets.remove(target);
    199     }
    200 
    201     /**
    202      * Perform mapping from {@link Parallax} to list of {@link ParallaxTarget}.
    203      */
    204     public final void performMapping(Parallax source) {
    205         if (mMarkerValues.size() < 2) {
    206             return;
    207         }
    208         if (this instanceof IntEffect) {
    209             source.verifyIntProperties();
    210         } else {
    211             source.verifyFloatProperties();
    212         }
    213         boolean fractionCalculated = false;
    214         float fraction = 0;
    215         Number directValue = null;
    216         for (int i = 0; i < mTargets.size(); i++) {
    217             ParallaxTarget target = mTargets.get(i);
    218             if (target.isDirectMapping()) {
    219                 if (directValue == null) {
    220                     directValue = calculateDirectValue(source);
    221                 }
    222                 target.directUpdate(directValue);
    223             } else {
    224                 if (!fractionCalculated) {
    225                     fractionCalculated = true;
    226                     fraction = calculateFraction(source);
    227                 }
    228                 target.update(fraction);
    229             }
    230         }
    231     }
    232 
    233     /**
    234      * This method is expected to compute a fraction between 0 and 1 based on the current values of
    235      * variables in {@link Parallax}. As the parallax effect goes on, the fraction increases
    236      * from 0 at beginning to 1 at the end.
    237      *
    238      * @return Float value between 0 and 1.
    239      */
    240     abstract float calculateFraction(Parallax source);
    241 
    242     /**
    243      * This method is expected to get the current value of the single {@link IntProperty} or
    244      * {@link FloatProperty}.
    245      *
    246      * @return Current value of the single {@link IntProperty} or {@link FloatProperty}.
    247      */
    248     abstract Number calculateDirectValue(Parallax source);
    249 
    250     /**
    251      * When there are multiple ranges (aka three or more markerValues),  this method adjust the
    252      * fraction inside a range to fraction of whole range.
    253      * e.g. four marker values, three weight values: 6, 2, 2.  totalWeights are 6, 8, 10
    254      * When markerValueIndex is 3, the fraction is inside last range.
    255      * adjusted_fraction = 8 / 10 + 2 / 10 * fraction.
    256      */
    257     final float getFractionWithWeightAdjusted(float fraction, int markerValueIndex) {
    258         // when there are three or more markerValues, take weight into consideration.
    259         if (mMarkerValues.size() >= 3) {
    260             final boolean hasWeightsDefined = mWeights.size() == mMarkerValues.size() - 1;
    261             if (hasWeightsDefined) {
    262                 // use weights user defined
    263                 final float allWeights = mTotalWeights.get(mTotalWeights.size() - 1);
    264                 fraction = fraction * mWeights.get(markerValueIndex - 1) / allWeights;
    265                 if (markerValueIndex >= 2) {
    266                     fraction += mTotalWeights.get(markerValueIndex - 2) / allWeights;
    267                 }
    268             } else {
    269                 // assume each range has same weight.
    270                 final float allWeights =  mMarkerValues.size() - 1;
    271                 fraction = fraction / allWeights;
    272                 if (markerValueIndex >= 2) {
    273                     fraction += (float) (markerValueIndex - 1) / allWeights;
    274                 }
    275             }
    276         }
    277         return fraction;
    278     }
    279 
    280     /**
    281      * Implementation of {@link ParallaxEffect} for integer type.
    282      */
    283     static final class IntEffect extends ParallaxEffect {
    284 
    285         @Override
    286         Number calculateDirectValue(Parallax source) {
    287             if (mMarkerValues.size() != 2) {
    288                 throw new RuntimeException("Must use two marker values for direct mapping");
    289             }
    290             if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) {
    291                 throw new RuntimeException(
    292                         "Marker value must use same Property for direct mapping");
    293             }
    294             int value1 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(0))
    295                     .getMarkerValue(source);
    296             int value2 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(1))
    297                     .getMarkerValue(source);
    298             if (value1 > value2) {
    299                 int swapValue = value2;
    300                 value2 = value1;
    301                 value1 = swapValue;
    302             }
    303 
    304             Number currentValue = ((IntProperty) mMarkerValues.get(0).getProperty()).get(source);
    305             if (currentValue.intValue() < value1) {
    306                 currentValue = value1;
    307             } else if (currentValue.intValue() > value2) {
    308                 currentValue = value2;
    309             }
    310             return currentValue;
    311         }
    312 
    313         @Override
    314         float calculateFraction(Parallax source) {
    315             int lastIndex = 0;
    316             int lastValue = 0;
    317             int lastMarkerValue = 0;
    318             // go through all markerValues, find first markerValue that current value is less than.
    319             for (int i = 0; i <  mMarkerValues.size(); i++) {
    320                 Parallax.IntPropertyMarkerValue k =  (Parallax.IntPropertyMarkerValue)
    321                         mMarkerValues.get(i);
    322                 int index = k.getProperty().getIndex();
    323                 int markerValue = k.getMarkerValue(source);
    324                 int currentValue = source.getIntPropertyValue(index);
    325 
    326                 float fraction;
    327                 if (i == 0) {
    328                     if (currentValue >= markerValue) {
    329                         return 0f;
    330                     }
    331                 } else {
    332                     if (lastIndex == index && lastMarkerValue < markerValue) {
    333                         throw new IllegalStateException("marker value of same variable must be "
    334                                 + "descendant order");
    335                     }
    336                     if (currentValue == IntProperty.UNKNOWN_AFTER) {
    337                         // Implies lastValue is less than lastMarkerValue and lastValue is not
    338                         // UNKNWON_AFTER.  Estimates based on distance of two variables is screen
    339                         // size.
    340                         fraction = (float) (lastMarkerValue - lastValue)
    341                                 / source.getMaxValue();
    342                         return getFractionWithWeightAdjusted(fraction, i);
    343                     } else if (currentValue >= markerValue) {
    344                         if (lastIndex == index) {
    345                             // same variable index,  same UI element at two different MarkerValues,
    346                             // e.g. UI element moves from lastMarkerValue=500 to markerValue=0,
    347                             // fraction moves from 0 to 1.
    348                             fraction = (float) (lastMarkerValue - currentValue)
    349                                     / (lastMarkerValue - markerValue);
    350                         } else if (lastValue != IntProperty.UNKNOWN_BEFORE) {
    351                             // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when
    352                             // UIElement_1 is at markerValue=300,  markerValue of UIElement_2 by
    353                             // adding delta of values to markerValue of UIElement_2.
    354                             lastMarkerValue = lastMarkerValue + (currentValue - lastValue);
    355                             fraction = (float) (lastMarkerValue - currentValue)
    356                                     / (lastMarkerValue - markerValue);
    357                         } else {
    358                             // Last variable is UNKNOWN_BEFORE.  Estimates based on assumption total
    359                             // travel distance from last variable to this variable is screen visible
    360                             // size.
    361                             fraction = 1f - (float) (currentValue - markerValue)
    362                                     / source.getMaxValue();
    363                         }
    364                         return getFractionWithWeightAdjusted(fraction, i);
    365                     }
    366                 }
    367                 lastValue = currentValue;
    368                 lastIndex = index;
    369                 lastMarkerValue = markerValue;
    370             }
    371             return 1f;
    372         }
    373     }
    374 
    375     /**
    376      * Implementation of {@link ParallaxEffect} for float type.
    377      */
    378     static final class FloatEffect extends ParallaxEffect {
    379 
    380         @Override
    381         Number calculateDirectValue(Parallax source) {
    382             if (mMarkerValues.size() != 2) {
    383                 throw new RuntimeException("Must use two marker values for direct mapping");
    384             }
    385             if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) {
    386                 throw new RuntimeException(
    387                         "Marker value must use same Property for direct mapping");
    388             }
    389             float value1 = ((FloatPropertyMarkerValue) mMarkerValues.get(0))
    390                     .getMarkerValue(source);
    391             float value2 = ((FloatPropertyMarkerValue) mMarkerValues.get(1))
    392                     .getMarkerValue(source);
    393             if (value1 > value2) {
    394                 float swapValue = value2;
    395                 value2 = value1;
    396                 value1 = swapValue;
    397             }
    398 
    399             Number currentValue = ((FloatProperty) mMarkerValues.get(0).getProperty()).get(source);
    400             if (currentValue.floatValue() < value1) {
    401                 currentValue = value1;
    402             } else if (currentValue.floatValue() > value2) {
    403                 currentValue = value2;
    404             }
    405             return currentValue;
    406         }
    407 
    408         @Override
    409         float calculateFraction(Parallax source) {
    410             int lastIndex = 0;
    411             float lastValue = 0;
    412             float lastMarkerValue = 0;
    413             // go through all markerValues, find first markerValue that current value is less than.
    414             for (int i = 0; i <  mMarkerValues.size(); i++) {
    415                 FloatPropertyMarkerValue k = (FloatPropertyMarkerValue) mMarkerValues.get(i);
    416                 int index = k.getProperty().getIndex();
    417                 float markerValue = k.getMarkerValue(source);
    418                 float currentValue = source.getFloatPropertyValue(index);
    419 
    420                 float fraction;
    421                 if (i == 0) {
    422                     if (currentValue >= markerValue) {
    423                         return 0f;
    424                     }
    425                 } else {
    426                     if (lastIndex == index && lastMarkerValue < markerValue) {
    427                         throw new IllegalStateException("marker value of same variable must be "
    428                                 + "descendant order");
    429                     }
    430                     if (currentValue == FloatProperty.UNKNOWN_AFTER) {
    431                         // Implies lastValue is less than lastMarkerValue and lastValue is not
    432                         // UNKNOWN_AFTER.  Estimates based on distance of two variables is screen
    433                         // size.
    434                         fraction = (float) (lastMarkerValue - lastValue)
    435                                 / source.getMaxValue();
    436                         return getFractionWithWeightAdjusted(fraction, i);
    437                     } else if (currentValue >= markerValue) {
    438                         if (lastIndex == index) {
    439                             // same variable index,  same UI element at two different MarkerValues,
    440                             // e.g. UI element moves from lastMarkerValue=500 to markerValue=0,
    441                             // fraction moves from 0 to 1.
    442                             fraction = (float) (lastMarkerValue - currentValue)
    443                                     / (lastMarkerValue - markerValue);
    444                         } else if (lastValue != FloatProperty.UNKNOWN_BEFORE) {
    445                             // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when
    446                             // UIElement_1 is at markerValue=300,  markerValue of UIElement_2 by
    447                             // adding delta of values to markerValue of UIElement_2.
    448                             lastMarkerValue = lastMarkerValue + (currentValue - lastValue);
    449                             fraction = (float) (lastMarkerValue - currentValue)
    450                                     / (lastMarkerValue - markerValue);
    451                         } else {
    452                             // Last variable is UNKNOWN_BEFORE.  Estimates based on assumption total
    453                             // travel distance from last variable to this variable is screen visible
    454                             // size.
    455                             fraction = 1f - (float) (currentValue - markerValue)
    456                                     / source.getMaxValue();
    457                         }
    458                         return getFractionWithWeightAdjusted(fraction, i);
    459                     }
    460                 }
    461                 lastValue = currentValue;
    462                 lastIndex = index;
    463                 lastMarkerValue = markerValue;
    464             }
    465             return 1f;
    466         }
    467     }
    468 
    469 }
    470 
    471