blob: 88aecb41d1c6f44ada6aa39908be600a25246220 [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 androidx.leanback.widget;
import android.util.Property;
import androidx.annotation.CallSuper;
import androidx.leanback.widget.ParallaxEffect.FloatEffect;
import androidx.leanback.widget.ParallaxEffect.IntEffect;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Parallax tracks a list of dynamic {@link Property}s typically representing foreground UI
* element positions on screen. Parallax keeps a list of {@link ParallaxEffect} objects which define
* rules to mapping property values to {@link ParallaxTarget}.
*
* <p>
* Example:
* <code>
* // when Property "var1" changes from 15 to max value, perform parallax effect to
* // change myView's translationY from 0 to 100.
* Parallax<IntProperty> parallax = new Parallax<IntProperty>() {...};
* p1 = parallax.addProperty("var1");
* parallax.addEffect(p1.at(15), p1.atMax())
* .target(myView, PropertyValuesHolder.ofFloat("translationY", 0, 100));
* </code>
* </p>
*
* <p>
* To create a {@link ParallaxEffect}, user calls {@link #addEffect(PropertyMarkerValue[])} with a
* list of {@link PropertyMarkerValue} which defines the range of {@link Parallax.IntProperty} or
* {@link Parallax.FloatProperty}. Then user adds {@link ParallaxTarget} into
* {@link ParallaxEffect}.
* </p>
* <p>
* App may subclass {@link Parallax.IntProperty} or {@link Parallax.FloatProperty} to supply
* additional information about how to retrieve Property value. {@link RecyclerViewParallax} is
* a great example of Parallax implementation tracking child view positions on screen.
* </p>
* <p>
* <ul>Restrictions of properties
* <li>FloatProperty and IntProperty cannot be mixed in one Parallax</li>
* <li>Values must be in ascending order.</li>
* <li>If the UI element is unknown above screen, use UNKNOWN_BEFORE.</li>
* <li>if the UI element is unknown below screen, use UNKNOWN_AFTER.</li>
* <li>UNKNOWN_BEFORE and UNKNOWN_AFTER are not allowed to be next to each other.</li>
* </ul>
* These rules will be verified at runtime.
* </p>
* <p>
* Subclass must override {@link #updateValues()} to update property values and perform
* {@link ParallaxEffect}s. Subclass may call {@link #updateValues()} automatically e.g.
* {@link RecyclerViewParallax} calls {@link #updateValues()} in RecyclerView scrolling. App might
* call {@link #updateValues()} manually when Parallax is unaware of the value change. For example,
* when a slide transition is running, {@link RecyclerViewParallax} is unaware of translation value
* changes; it's the app's responsibility to call {@link #updateValues()} in every frame of
* animation.
* </p>
* @param <PropertyT> Subclass of {@link Parallax.IntProperty} or {@link Parallax.FloatProperty}
*/
public abstract class Parallax<PropertyT extends android.util.Property> {
/**
* Class holding a fixed value for a Property in {@link Parallax}.
* @param <PropertyT> Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}.
*/
public static class PropertyMarkerValue<PropertyT> {
private final PropertyT mProperty;
public PropertyMarkerValue(PropertyT property) {
mProperty = property;
}
/**
* @return Associated property.
*/
public PropertyT getProperty() {
return mProperty;
}
}
/**
* IntProperty provide access to an index based integer type property inside
* {@link Parallax}. The IntProperty typically represents UI element position inside
* {@link Parallax}.
*/
public static class IntProperty extends Property<Parallax, Integer> {
/**
* Property value is unknown and it's smaller than minimal value of Parallax. For
* example if a child is not created and before the first visible child of RecyclerView.
*/
public static final int UNKNOWN_BEFORE = Integer.MIN_VALUE;
/**
* Property value is unknown and it's larger than {@link Parallax#getMaxValue()}. For
* example if a child is not created and after the last visible child of RecyclerView.
*/
public static final int UNKNOWN_AFTER = Integer.MAX_VALUE;
private final int mIndex;
/**
* Constructor.
*
* @param name Name of this Property.
* @param index Index of this Property inside {@link Parallax}.
*/
public IntProperty(String name, int index) {
super(Integer.class, name);
mIndex = index;
}
@Override
public final Integer get(Parallax object) {
return object.getIntPropertyValue(mIndex);
}
@Override
public final void set(Parallax object, Integer value) {
object.setIntPropertyValue(mIndex, value);
}
/**
* @return Index of this Property in {@link Parallax}.
*/
public final int getIndex() {
return mIndex;
}
/**
* Fast version of get() method that returns a primitive int value of the Property.
* @param object The Parallax object that owns this Property.
* @return Int value of the Property.
*/
public final int getValue(Parallax object) {
return object.getIntPropertyValue(mIndex);
}
/**
* Fast version of set() method that takes a primitive int value into the Property.
*
* @param object The Parallax object that owns this Property.
* @param value Int value of the Property.
*/
public final void setValue(Parallax object, int value) {
object.setIntPropertyValue(mIndex, value);
}
/**
* Creates an {@link PropertyMarkerValue} object for the absolute marker value.
*
* @param absoluteValue The integer marker value.
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atAbsolute(int absoluteValue) {
return new IntPropertyMarkerValue(this, absoluteValue, 0f);
}
/**
* Creates an {@link PropertyMarkerValue} object for the marker value representing
* {@link Parallax#getMaxValue()}.
*
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atMax() {
return new IntPropertyMarkerValue(this, 0, 1f);
}
/**
* Creates an {@link PropertyMarkerValue} object for the marker value representing 0.
*
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atMin() {
return new IntPropertyMarkerValue(this, 0);
}
/**
* Creates an {@link PropertyMarkerValue} object for a fraction of
* {@link Parallax#getMaxValue()}.
*
* @param fractionOfMaxValue 0 to 1 fraction to multiply with
* {@link Parallax#getMaxValue()} for
* the marker value.
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atFraction(float fractionOfMaxValue) {
return new IntPropertyMarkerValue(this, 0, fractionOfMaxValue);
}
/**
* Create an {@link PropertyMarkerValue} object by multiplying the fraction with
* {@link Parallax#getMaxValue()} and adding offsetValue to it.
*
* @param offsetValue An offset integer value to be added to marker
* value.
* @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
* {@link Parallax#getMaxValue()} for
* the marker value.
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue at(int offsetValue,
float fractionOfMaxParentVisibleSize) {
return new IntPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize);
}
}
/**
* Implementation of {@link PropertyMarkerValue} for {@link IntProperty}.
*/
static class IntPropertyMarkerValue extends PropertyMarkerValue<IntProperty> {
private final int mValue;
private final float mFactionOfMax;
IntPropertyMarkerValue(IntProperty property, int value) {
this(property, value, 0f);
}
IntPropertyMarkerValue(IntProperty property, int value, float fractionOfMax) {
super(property);
mValue = value;
mFactionOfMax = fractionOfMax;
}
/**
* @return The marker value of integer type.
*/
final int getMarkerValue(Parallax source) {
return mFactionOfMax == 0 ? mValue : mValue + Math.round(source
.getMaxValue() * mFactionOfMax);
}
}
/**
* FloatProperty provide access to an index based integer type property inside
* {@link Parallax}. The FloatProperty typically represents UI element position inside
* {@link Parallax}.
*/
public static class FloatProperty extends Property<Parallax, Float> {
/**
* Property value is unknown and it's smaller than minimal value of Parallax. For
* example if a child is not created and before the first visible child of RecyclerView.
*/
public static final float UNKNOWN_BEFORE = -Float.MAX_VALUE;
/**
* Property value is unknown and it's larger than {@link Parallax#getMaxValue()}. For
* example if a child is not created and after the last visible child of RecyclerView.
*/
public static final float UNKNOWN_AFTER = Float.MAX_VALUE;
private final int mIndex;
/**
* Constructor.
*
* @param name Name of this Property.
* @param index Index of this Property inside {@link Parallax}.
*/
public FloatProperty(String name, int index) {
super(Float.class, name);
mIndex = index;
}
@Override
public final Float get(Parallax object) {
return object.getFloatPropertyValue(mIndex);
}
@Override
public final void set(Parallax object, Float value) {
object.setFloatPropertyValue(mIndex, value);
}
/**
* @return Index of this Property in {@link Parallax}.
*/
public final int getIndex() {
return mIndex;
}
/**
* Fast version of get() method that returns a primitive int value of the Property.
* @param object The Parallax object that owns this Property.
* @return Float value of the Property.
*/
public final float getValue(Parallax object) {
return object.getFloatPropertyValue(mIndex);
}
/**
* Fast version of set() method that takes a primitive float value into the Property.
*
* @param object The Parallax object that owns this Property.
* @param value Float value of the Property.
*/
public final void setValue(Parallax object, float value) {
object.setFloatPropertyValue(mIndex, value);
}
/**
* Creates an {@link PropertyMarkerValue} object for the absolute marker value.
*
* @param markerValue The float marker value.
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atAbsolute(float markerValue) {
return new FloatPropertyMarkerValue(this, markerValue, 0f);
}
/**
* Creates an {@link PropertyMarkerValue} object for the marker value representing
* {@link Parallax#getMaxValue()}.
*
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atMax() {
return new FloatPropertyMarkerValue(this, 0, 1f);
}
/**
* Creates an {@link PropertyMarkerValue} object for the marker value representing 0.
*
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atMin() {
return new FloatPropertyMarkerValue(this, 0);
}
/**
* Creates an {@link PropertyMarkerValue} object for a fraction of
* {@link Parallax#getMaxValue()}.
*
* @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
* {@link Parallax#getMaxValue()} for
* the marker value.
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue atFraction(float fractionOfMaxParentVisibleSize) {
return new FloatPropertyMarkerValue(this, 0, fractionOfMaxParentVisibleSize);
}
/**
* Create an {@link PropertyMarkerValue} object by multiplying the fraction with
* {@link Parallax#getMaxValue()} and adding offsetValue to it.
*
* @param offsetValue An offset float value to be added to marker value.
* @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with
* {@link Parallax#getMaxValue()} for
* the marker value.
* @return A new {@link PropertyMarkerValue} object.
*/
public final PropertyMarkerValue at(float offsetValue,
float fractionOfMaxParentVisibleSize) {
return new FloatPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize);
}
}
/**
* Implementation of {@link PropertyMarkerValue} for {@link FloatProperty}.
*/
static class FloatPropertyMarkerValue extends PropertyMarkerValue<FloatProperty> {
private final float mValue;
private final float mFactionOfMax;
FloatPropertyMarkerValue(FloatProperty property, float value) {
this(property, value, 0f);
}
FloatPropertyMarkerValue(FloatProperty property, float value, float fractionOfMax) {
super(property);
mValue = value;
mFactionOfMax = fractionOfMax;
}
/**
* @return The marker value.
*/
final float getMarkerValue(Parallax source) {
return mFactionOfMax == 0 ? mValue : mValue + source.getMaxValue()
* mFactionOfMax;
}
}
final List<PropertyT> mProperties = new ArrayList<PropertyT>();
final List<PropertyT> mPropertiesReadOnly = Collections.unmodifiableList(mProperties);
private int[] mValues = new int[4];
private float[] mFloatValues = new float[4];
private final List<ParallaxEffect> mEffects = new ArrayList<ParallaxEffect>(4);
/**
* Return the max value which is typically size of parent visible area, e.g. RecyclerView's
* height if we are tracking Y position of a child. The size can be used to calculate marker
* value using the provided fraction of FloatPropertyMarkerValue.
*
* @return Size of parent visible area.
* @see IntPropertyMarkerValue#IntPropertyMarkerValue(IntProperty, int, float)
* @see FloatPropertyMarkerValue#FloatPropertyMarkerValue(FloatProperty, float, float)
*/
public abstract float getMaxValue();
/**
* Get index based property value.
*
* @param index Index of the property.
* @return Value of the property.
*/
final int getIntPropertyValue(int index) {
return mValues[index];
}
/**
* Set index based property value.
*
* @param index Index of the property.
* @param value Value of the property.
*/
final void setIntPropertyValue(int index, int value) {
if (index >= mProperties.size()) {
throw new ArrayIndexOutOfBoundsException();
}
mValues[index] = value;
}
/**
* Add a new IntProperty in the Parallax object. App may override
* {@link #createProperty(String, int)}.
*
* @param name Name of the property.
* @return Newly created Property object.
* @see #createProperty(String, int)
*/
public final PropertyT addProperty(String name) {
int newPropertyIndex = mProperties.size();
PropertyT property = createProperty(name, newPropertyIndex);
if (property instanceof IntProperty) {
int size = mValues.length;
if (size == newPropertyIndex) {
int[] newValues = new int[size * 2];
for (int i = 0; i < size; i++) {
newValues[i] = mValues[i];
}
mValues = newValues;
}
mValues[newPropertyIndex] = IntProperty.UNKNOWN_AFTER;
} else if (property instanceof FloatProperty) {
int size = mFloatValues.length;
if (size == newPropertyIndex) {
float[] newValues = new float[size * 2];
for (int i = 0; i < size; i++) {
newValues[i] = mFloatValues[i];
}
mFloatValues = newValues;
}
mFloatValues[newPropertyIndex] = FloatProperty.UNKNOWN_AFTER;
} else {
throw new IllegalArgumentException("Invalid Property type");
}
mProperties.add(property);
return property;
}
/**
* Verify sanity of property values, throws RuntimeException if fails. The property values
* must be in ascending order. UNKNOW_BEFORE and UNKNOWN_AFTER are not allowed to be next to
* each other.
*/
void verifyIntProperties() throws IllegalStateException {
if (mProperties.size() < 2) {
return;
}
int last = getIntPropertyValue(0);
for (int i = 1; i < mProperties.size(); i++) {
int v = getIntPropertyValue(i);
if (v < last) {
throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+ " smaller than Property[%d]\"%s\"",
i, mProperties.get(i).getName(),
i - 1, mProperties.get(i - 1).getName()));
} else if (last == IntProperty.UNKNOWN_BEFORE && v == IntProperty.UNKNOWN_AFTER) {
throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+ " UNKNOWN_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER",
i - 1, mProperties.get(i - 1).getName(),
i, mProperties.get(i).getName()));
}
last = v;
}
}
final void verifyFloatProperties() throws IllegalStateException {
if (mProperties.size() < 2) {
return;
}
float last = getFloatPropertyValue(0);
for (int i = 1; i < mProperties.size(); i++) {
float v = getFloatPropertyValue(i);
if (v < last) {
throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+ " smaller than Property[%d]\"%s\"",
i, mProperties.get(i).getName(),
i - 1, mProperties.get(i - 1).getName()));
} else if (last == FloatProperty.UNKNOWN_BEFORE && v
== FloatProperty.UNKNOWN_AFTER) {
throw new IllegalStateException(String.format("Parallax Property[%d]\"%s\" is"
+ " UNKNOWN_BEFORE and Property[%d]\"%s\" is UNKNOWN_AFTER",
i - 1, mProperties.get(i - 1).getName(),
i, mProperties.get(i).getName()));
}
last = v;
}
}
/**
* Get index based property value.
*
* @param index Index of the property.
* @return Value of the property.
*/
final float getFloatPropertyValue(int index) {
return mFloatValues[index];
}
/**
* Set index based property value.
*
* @param index Index of the property.
* @param value Value of the property.
*/
final void setFloatPropertyValue(int index, float value) {
if (index >= mProperties.size()) {
throw new ArrayIndexOutOfBoundsException();
}
mFloatValues[index] = value;
}
/**
* @return A unmodifiable list of properties.
*/
public final List<PropertyT> getProperties() {
return mPropertiesReadOnly;
}
/**
* Create a new Property object. App does not directly call this method. See
* {@link #addProperty(String)}.
*
* @param index Index of the property in this Parallax object.
* @return Newly created Property object.
*/
public abstract PropertyT createProperty(String name, int index);
/**
* Update property values and perform {@link ParallaxEffect}s. Subclass may override and call
* super.updateValues() after updated properties values.
*/
@CallSuper
public void updateValues() {
for (int i = 0; i < mEffects.size(); i++) {
mEffects.get(i).performMapping(this);
}
}
/**
* Returns a list of {@link ParallaxEffect} object which defines rules to perform mapping to
* multiple {@link ParallaxTarget}s.
*
* @return A list of {@link ParallaxEffect} object.
*/
public List<ParallaxEffect> getEffects() {
return mEffects;
}
/**
* Remove the {@link ParallaxEffect} object.
*
* @param effect The {@link ParallaxEffect} object to remove.
*/
public void removeEffect(ParallaxEffect effect) {
mEffects.remove(effect);
}
/**
* Remove all {@link ParallaxEffect} objects.
*/
public void removeAllEffects() {
mEffects.clear();
}
/**
* Create a {@link ParallaxEffect} object that will track source variable changes within a
* provided set of ranges.
*
* @param ranges A list of marker values that defines the ranges.
* @return Newly created ParallaxEffect object.
*/
public ParallaxEffect addEffect(PropertyMarkerValue... ranges) {
ParallaxEffect effect;
if (ranges[0].getProperty() instanceof IntProperty) {
effect = new IntEffect();
} else {
effect = new FloatEffect();
}
effect.setPropertyRanges(ranges);
mEffects.add(effect);
return effect;
}
}