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