/* * 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.support.annotation.CallSuper; import android.support.v17.leanback.widget.ParallaxEffect.FloatEffect; import android.support.v17.leanback.widget.ParallaxEffect.IntEffect; import android.util.Property; 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}. * *

* Example: * * // when Property "var1" changes from 15 to max value, perform parallax effect to * // change myView's translationY from 0 to 100. * Parallax parallax = new Parallax() {...}; * p1 = parallax.addProperty("var1"); * parallax.addEffect(p1.at(15), p1.atMax()) * .target(myView, PropertyValuesHolder.ofFloat("translationY", 0, 100)); * *

* *

* 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}. *

*

* 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. *

*

*

* These rules will be verified at runtime. *

*

* 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. *

* @param Subclass of {@link Parallax.IntProperty} or {@link Parallax.FloatProperty} */ public abstract class Parallax { /** * Class holding a fixed value for a Property in {@link Parallax}. * @param Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}. */ public static class PropertyMarkerValue { 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 { /** * 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 { 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 { /** * 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 { 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 mProperties = new ArrayList(); final List mPropertiesReadOnly = Collections.unmodifiableList(mProperties); private int[] mValues = new int[4]; private float[] mFloatValues = new float[4]; private final List mEffects = new ArrayList(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 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 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; } }