/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v17.leanback.widget; import android.animation.PropertyValuesHolder; import android.support.v17.leanback.widget.Parallax.FloatProperty; import android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue; import android.support.v17.leanback.widget.Parallax.IntProperty; import android.support.v17.leanback.widget.Parallax.PropertyMarkerValue; import android.util.Property; import java.util.ArrayList; import java.util.List; /** * ParallaxEffect class drives changes in {@link ParallaxTarget} in response to changes in * variables defined in {@link Parallax}. *

* ParallaxEffect has a list of {@link Parallax.PropertyMarkerValue}s which represents the range of * values that source variables can take. The main function is * {@link ParallaxEffect#performMapping(Parallax)} which computes a fraction between 0 and 1 * based on the current values of variables in {@link Parallax}. As the parallax effect goes * on, the fraction increases from 0 at beginning to 1 at the end. Then the fraction is passed on * to {@link ParallaxTarget#update(float)}. *

* App use {@link Parallax#addEffect(PropertyMarkerValue...)} to create a ParallaxEffect. */ public abstract class ParallaxEffect { final List mMarkerValues = new ArrayList(2); final List mWeights = new ArrayList(2); final List mTotalWeights = new ArrayList(2); final List mTargets = new ArrayList(4); /** * Only accessible from package */ ParallaxEffect() { } /** * Returns the list of {@link PropertyMarkerValue}s, which represents the range of values that * source variables can take. * * @return A list of {@link Parallax.PropertyMarkerValue}s. * @see #performMapping(Parallax) */ public final List getPropertyRanges() { return mMarkerValues; } /** * Returns a list of Float objects that represents weight associated with each variable range. * Weights are used when there are three or more marker values. * * @return A list of Float objects that represents weight associated with each variable range. * @hide */ public final List getWeights() { return mWeights; } /** * Sets the list of {@link PropertyMarkerValue}s, which represents the range of values that * source variables can take. * * @param markerValues A list of {@link PropertyMarkerValue}s. * @see #performMapping(Parallax) */ public final void setPropertyRanges(Parallax.PropertyMarkerValue... markerValues) { mMarkerValues.clear(); for (Parallax.PropertyMarkerValue markerValue : markerValues) { mMarkerValues.add(markerValue); } } /** * Sets a list of Float objects that represents weight associated with each variable range. * Weights are used when there are three or more marker values. * * @param weights A list of Float objects that represents weight associated with each variable * range. * @hide */ public final void setWeights(float... weights) { for (float weight : weights) { if (weight <= 0) { throw new IllegalArgumentException(); } } mWeights.clear(); mTotalWeights.clear(); float totalWeight = 0f; for (float weight : weights) { mWeights.add(weight); totalWeight += weight; mTotalWeights.add(totalWeight); } } /** * Sets a list of Float objects that represents weight associated with each variable range. * Weights are used when there are three or more marker values. * * @param weights A list of Float objects that represents weight associated with each variable * range. * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. * @hide */ public final ParallaxEffect weights(float... weights) { setWeights(weights); return this; } /** * Add a ParallaxTarget to run parallax effect. * * @param target ParallaxTarget to add. */ public final void addTarget(ParallaxTarget target) { mTargets.add(target); } /** * Add a ParallaxTarget to run parallax effect. * * @param target ParallaxTarget to add. * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. */ public final ParallaxEffect target(ParallaxTarget target) { mTargets.add(target); return this; } /** * Creates a {@link ParallaxTarget} from {@link PropertyValuesHolder} and adds it to the list * of targets. * * @param targetObject Target object for PropertyValuesHolderTarget. * @param values PropertyValuesHolder for PropertyValuesHolderTarget. * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. */ public final ParallaxEffect target(Object targetObject, PropertyValuesHolder values) { mTargets.add(new ParallaxTarget.PropertyValuesHolderTarget(targetObject, values)); return this; } /** * Creates a {@link ParallaxTarget} using direct mapping from source property into target * property, the new {@link ParallaxTarget} will be added to its list of targets. * * @param targetObject Target object for property. * @param targetProperty The target property that will receive values. * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. * @param Type of target object. * @param Type of target property value, either Integer or Float. * @see ParallaxTarget#isDirectMapping() */ public final ParallaxEffect target(T targetObject, Property targetProperty) { mTargets.add(new ParallaxTarget.DirectPropertyTarget(targetObject, targetProperty)); return this; } /** * Returns the list of {@link ParallaxTarget} objects. * * @return The list of {@link ParallaxTarget} objects. */ public final List getTargets() { return mTargets; } /** * Remove a {@link ParallaxTarget} object from the list. * @param target The {@link ParallaxTarget} object to be removed. */ public final void removeTarget(ParallaxTarget target) { mTargets.remove(target); } /** * Perform mapping from {@link Parallax} to list of {@link ParallaxTarget}. */ public final void performMapping(Parallax source) { if (mMarkerValues.size() < 2) { return; } if (this instanceof IntEffect) { source.verifyIntProperties(); } else { source.verifyFloatProperties(); } boolean fractionCalculated = false; float fraction = 0; Number directValue = null; for (int i = 0; i < mTargets.size(); i++) { ParallaxTarget target = mTargets.get(i); if (target.isDirectMapping()) { if (directValue == null) { directValue = calculateDirectValue(source); } target.directUpdate(directValue); } else { if (!fractionCalculated) { fractionCalculated = true; fraction = calculateFraction(source); } target.update(fraction); } } } /** * This method is expected to compute a fraction between 0 and 1 based on the current values of * variables in {@link Parallax}. As the parallax effect goes on, the fraction increases * from 0 at beginning to 1 at the end. * * @return Float value between 0 and 1. */ abstract float calculateFraction(Parallax source); /** * This method is expected to get the current value of the single {@link IntProperty} or * {@link FloatProperty}. * * @return Current value of the single {@link IntProperty} or {@link FloatProperty}. */ abstract Number calculateDirectValue(Parallax source); /** * When there are multiple ranges (aka three or more markerValues), this method adjust the * fraction inside a range to fraction of whole range. * e.g. four marker values, three weight values: 6, 2, 2. totalWeights are 6, 8, 10 * When markerValueIndex is 3, the fraction is inside last range. * adjusted_fraction = 8 / 10 + 2 / 10 * fraction. */ final float getFractionWithWeightAdjusted(float fraction, int markerValueIndex) { // when there are three or more markerValues, take weight into consideration. if (mMarkerValues.size() >= 3) { final boolean hasWeightsDefined = mWeights.size() == mMarkerValues.size() - 1; if (hasWeightsDefined) { // use weights user defined final float allWeights = mTotalWeights.get(mTotalWeights.size() - 1); fraction = fraction * mWeights.get(markerValueIndex - 1) / allWeights; if (markerValueIndex >= 2) { fraction += mTotalWeights.get(markerValueIndex - 2) / allWeights; } } else { // assume each range has same weight. final float allWeights = mMarkerValues.size() - 1; fraction = fraction / allWeights; if (markerValueIndex >= 2) { fraction += (float) (markerValueIndex - 1) / allWeights; } } } return fraction; } /** * Implementation of {@link ParallaxEffect} for integer type. */ static final class IntEffect extends ParallaxEffect { @Override Number calculateDirectValue(Parallax source) { if (mMarkerValues.size() != 2) { throw new RuntimeException("Must use two marker values for direct mapping"); } if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) { throw new RuntimeException( "Marker value must use same Property for direct mapping"); } int value1 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(0)) .getMarkerValue(source); int value2 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(1)) .getMarkerValue(source); if (value1 > value2) { int swapValue = value2; value2 = value1; value1 = swapValue; } Number currentValue = ((IntProperty) mMarkerValues.get(0).getProperty()).get(source); if (currentValue.intValue() < value1) { currentValue = value1; } else if (currentValue.intValue() > value2) { currentValue = value2; } return currentValue; } @Override float calculateFraction(Parallax source) { int lastIndex = 0; int lastValue = 0; int lastMarkerValue = 0; // go through all markerValues, find first markerValue that current value is less than. for (int i = 0; i < mMarkerValues.size(); i++) { Parallax.IntPropertyMarkerValue k = (Parallax.IntPropertyMarkerValue) mMarkerValues.get(i); int index = k.getProperty().getIndex(); int markerValue = k.getMarkerValue(source); int currentValue = source.getIntPropertyValue(index); float fraction; if (i == 0) { if (currentValue >= markerValue) { return 0f; } } else { if (lastIndex == index && lastMarkerValue < markerValue) { throw new IllegalStateException("marker value of same variable must be " + "descendant order"); } if (currentValue == IntProperty.UNKNOWN_AFTER) { // Implies lastValue is less than lastMarkerValue and lastValue is not // UNKNWON_AFTER. Estimates based on distance of two variables is screen // size. fraction = (float) (lastMarkerValue - lastValue) / source.getMaxValue(); return getFractionWithWeightAdjusted(fraction, i); } else if (currentValue >= markerValue) { if (lastIndex == index) { // same variable index, same UI element at two different MarkerValues, // e.g. UI element moves from lastMarkerValue=500 to markerValue=0, // fraction moves from 0 to 1. fraction = (float) (lastMarkerValue - currentValue) / (lastMarkerValue - markerValue); } else if (lastValue != IntProperty.UNKNOWN_BEFORE) { // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when // UIElement_1 is at markerValue=300, markerValue of UIElement_2 by // adding delta of values to markerValue of UIElement_2. lastMarkerValue = lastMarkerValue + (currentValue - lastValue); fraction = (float) (lastMarkerValue - currentValue) / (lastMarkerValue - markerValue); } else { // Last variable is UNKNOWN_BEFORE. Estimates based on assumption total // travel distance from last variable to this variable is screen visible // size. fraction = 1f - (float) (currentValue - markerValue) / source.getMaxValue(); } return getFractionWithWeightAdjusted(fraction, i); } } lastValue = currentValue; lastIndex = index; lastMarkerValue = markerValue; } return 1f; } } /** * Implementation of {@link ParallaxEffect} for float type. */ static final class FloatEffect extends ParallaxEffect { @Override Number calculateDirectValue(Parallax source) { if (mMarkerValues.size() != 2) { throw new RuntimeException("Must use two marker values for direct mapping"); } if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) { throw new RuntimeException( "Marker value must use same Property for direct mapping"); } float value1 = ((FloatPropertyMarkerValue) mMarkerValues.get(0)) .getMarkerValue(source); float value2 = ((FloatPropertyMarkerValue) mMarkerValues.get(1)) .getMarkerValue(source); if (value1 > value2) { float swapValue = value2; value2 = value1; value1 = swapValue; } Number currentValue = ((FloatProperty) mMarkerValues.get(0).getProperty()).get(source); if (currentValue.floatValue() < value1) { currentValue = value1; } else if (currentValue.floatValue() > value2) { currentValue = value2; } return currentValue; } @Override float calculateFraction(Parallax source) { int lastIndex = 0; float lastValue = 0; float lastMarkerValue = 0; // go through all markerValues, find first markerValue that current value is less than. for (int i = 0; i < mMarkerValues.size(); i++) { FloatPropertyMarkerValue k = (FloatPropertyMarkerValue) mMarkerValues.get(i); int index = k.getProperty().getIndex(); float markerValue = k.getMarkerValue(source); float currentValue = source.getFloatPropertyValue(index); float fraction; if (i == 0) { if (currentValue >= markerValue) { return 0f; } } else { if (lastIndex == index && lastMarkerValue < markerValue) { throw new IllegalStateException("marker value of same variable must be " + "descendant order"); } if (currentValue == FloatProperty.UNKNOWN_AFTER) { // Implies lastValue is less than lastMarkerValue and lastValue is not // UNKNOWN_AFTER. Estimates based on distance of two variables is screen // size. fraction = (float) (lastMarkerValue - lastValue) / source.getMaxValue(); return getFractionWithWeightAdjusted(fraction, i); } else if (currentValue >= markerValue) { if (lastIndex == index) { // same variable index, same UI element at two different MarkerValues, // e.g. UI element moves from lastMarkerValue=500 to markerValue=0, // fraction moves from 0 to 1. fraction = (float) (lastMarkerValue - currentValue) / (lastMarkerValue - markerValue); } else if (lastValue != FloatProperty.UNKNOWN_BEFORE) { // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when // UIElement_1 is at markerValue=300, markerValue of UIElement_2 by // adding delta of values to markerValue of UIElement_2. lastMarkerValue = lastMarkerValue + (currentValue - lastValue); fraction = (float) (lastMarkerValue - currentValue) / (lastMarkerValue - markerValue); } else { // Last variable is UNKNOWN_BEFORE. Estimates based on assumption total // travel distance from last variable to this variable is screen visible // size. fraction = 1f - (float) (currentValue - markerValue) / source.getMaxValue(); } return getFractionWithWeightAdjusted(fraction, i); } } lastValue = currentValue; lastIndex = index; lastMarkerValue = markerValue; } return 1f; } } }