• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ParallaxEffect()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      */
getPropertyRanges()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)
getWeights()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      */
setPropertyRanges(Parallax.PropertyMarkerValue... markerValues)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)
setWeights(float... weights)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)
weights(float... weights)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      */
addTarget(ParallaxTarget target)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      */
target(ParallaxTarget target)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      */
target(Object targetObject, PropertyValuesHolder values)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      */
target(T targetObject, Property<T, V> targetProperty)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      */
getTargets()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      */
removeTarget(ParallaxTarget target)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      */
performMapping(Parallax source)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      */
calculateFraction(Parallax source)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      */
calculateDirectValue(Parallax source)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      */
getFractionWithWeightAdjusted(float fraction, int markerValueIndex)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
calculateDirectValue(Parallax source)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
calculateFraction(Parallax source)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
calculateDirectValue(Parallax source)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
calculateFraction(Parallax source)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