blob: 5c06e29b2cc91fd97f1ab5fd5982ea5b8808e7de [file] [log] [blame]
/*
* 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}.
* <p>
* 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)}.
* <p>
* App use {@link Parallax#addEffect(PropertyMarkerValue...)} to create a ParallaxEffect.
*/
public abstract class ParallaxEffect {
final List<Parallax.PropertyMarkerValue> mMarkerValues = new ArrayList(2);
final List<Float> mWeights = new ArrayList<Float>(2);
final List<Float> mTotalWeights = new ArrayList<Float>(2);
final List<ParallaxTarget> mTargets = new ArrayList<ParallaxTarget>(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<Parallax.PropertyMarkerValue> 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<Float> 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 <T> Type of target object.
* @param <V> Type of target property value, either Integer or Float.
* @see ParallaxTarget#isDirectMapping()
*/
public final <T, V extends Number> ParallaxEffect target(T targetObject,
Property<T, V> 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<ParallaxTarget> 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;
}
}
}