/* * 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}. * * <p> * There are two types of Parallax, int or float. App should subclass either * {@link Parallax.IntParallax} or {@link Parallax.FloatParallax}. 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>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 can be verified by {@link #verifyProperties()}. * </p> * Subclass should 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> Class of the property, e.g. {@link IntProperty} or {@link FloatProperty}. */ public abstract class Parallax<PropertyT extends Property> { private final List<ParallaxEffect> mEffects = new ArrayList<ParallaxEffect>(4); /** * Class holding a fixed value for a Property in {@link Parallax}. * Base class for {@link IntPropertyMarkerValue} and {@link FloatPropertyMarkerValue}. * @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 IntParallax}. The IntProperty typically represents UI element position inside * {@link IntParallax}. */ public static class IntProperty extends Property<IntParallax, 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 IntParallax#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 IntParallax}. */ public IntProperty(String name, int index) { super(Integer.class, name); mIndex = index; } @Override public final Integer get(IntParallax object) { return getIntValue(object); } @Override public final void set(IntParallax object, Integer value) { setIntValue(object, value); } final int getIntValue(IntParallax source) { return source.getPropertyValue(mIndex); } final void setIntValue(IntParallax source, int value) { source.setPropertyValue(mIndex, value); } /** * @return Index of this Property in {@link IntParallax}. */ public final int getIndex() { return mIndex; } /** * Creates an {@link IntPropertyMarkerValue} object for the absolute marker value. * * @param absoluteValue The integer marker value. * @return A new {@link IntPropertyMarkerValue} object. */ public final IntPropertyMarkerValue atAbsolute(int absoluteValue) { return new IntPropertyMarkerValue(this, absoluteValue, 0f); } /** * Creates an {@link IntPropertyMarkerValue} object for a fraction of * {@link IntParallax#getMaxValue()}. * * @param fractionOfMaxValue 0 to 1 fraction to multiply with * {@link IntParallax#getMaxValue()} for * the marker value. * @return A new {@link IntPropertyMarkerValue} object. */ public final IntPropertyMarkerValue atFraction(float fractionOfMaxValue) { return new IntPropertyMarkerValue(this, 0, fractionOfMaxValue); } /** * Create an {@link IntPropertyMarkerValue} object by multiplying the fraction with * {@link IntParallax#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 IntParallax#getMaxValue()} for * the marker value. * @return A new {@link IntPropertyMarkerValue} object. */ public final IntPropertyMarkerValue at(int offsetValue, float fractionOfMaxParentVisibleSize) { return new IntPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize); } } /** * Parallax that manages a list of {@link IntProperty}. App may override this class with a * specific {@link IntProperty} subclass. * * @param <IntPropertyT> Type of {@link IntProperty} or subclass. */ public abstract static class IntParallax<IntPropertyT extends IntProperty> extends Parallax<IntPropertyT> { private int[] mValues = new int[4]; /** * Get index based property value. * * @param index Index of the property. * @return Value of the property. */ public final int getPropertyValue(int index) { return mValues[index]; } /** * Set index based property value. * * @param index Index of the property. * @param value Value of the property. */ public final void setPropertyValue(int index, int value) { if (index >= mProperties.size()) { throw new ArrayIndexOutOfBoundsException(); } mValues[index] = value; } /** * Return the max value, which is typically 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 IntPropertyMarkerValue. * * @return Max value of parallax. * @see IntPropertyMarkerValue#IntPropertyMarkerValue(IntProperty, int, float) */ public abstract int getMaxValue(); @Override public final IntPropertyT addProperty(String name) { int newPropertyIndex = mProperties.size(); IntPropertyT property = createProperty(name, newPropertyIndex); mProperties.add(property); 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; return property; } @Override public final void verifyProperties() throws IllegalStateException { if (mProperties.size() < 2) { return; } int last = mProperties.get(0).getIntValue(this); for (int i = 1; i < mProperties.size(); i++) { int v = mProperties.get(i).getIntValue(this); 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; } } } /** * Implementation of {@link PropertyMarkerValue} for {@link IntProperty}. */ public static class IntPropertyMarkerValue extends PropertyMarkerValue<IntProperty> { private final int mValue; private final float mFactionOfMax; public IntPropertyMarkerValue(IntProperty property, int value) { this(property, value, 0f); } public IntPropertyMarkerValue(IntProperty property, int value, float fractionOfMax) { super(property); mValue = value; mFactionOfMax = fractionOfMax; } /** * @return The marker value of integer type. */ public final int getMarkerValue(IntParallax source) { return mFactionOfMax == 0 ? mValue : mValue + Math.round(source .getMaxValue() * mFactionOfMax); } } /** * FloatProperty provide access to an index based integer type property inside * {@link FloatParallax}. The FloatProperty typically represents UI element position inside * {@link FloatParallax}. */ public static class FloatProperty extends Property<FloatParallax, 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 FloatParallax#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 FloatParallax}. */ public FloatProperty(String name, int index) { super(Float.class, name); mIndex = index; } @Override public final Float get(FloatParallax object) { return getFloatValue(object); } @Override public final void set(FloatParallax object, Float value) { setFloatValue(object, value); } final float getFloatValue(FloatParallax source) { return source.getPropertyValue(mIndex); } final void setFloatValue(FloatParallax source, float value) { source.setPropertyValue(mIndex, value); } /** * @return Index of this Property in {@link FloatParallax}. */ public final int getIndex() { return mIndex; } /** * Creates an {@link FloatPropertyMarkerValue} object for the absolute marker value. * * @param markerValue The float marker value. * @return A new {@link FloatPropertyMarkerValue} object. */ public final FloatPropertyMarkerValue atAbsolute(float markerValue) { return new FloatPropertyMarkerValue(this, markerValue, 0f); } /** * Creates an {@link FloatPropertyMarkerValue} object for a fraction of * {@link FloatParallax#getMaxValue()}. * * @param fractionOfMaxParentVisibleSize 0 to 1 fraction to multiply with * {@link FloatParallax#getMaxValue()} for * the marker value. * @return A new {@link FloatPropertyMarkerValue} object. */ public final FloatPropertyMarkerValue atFraction(float fractionOfMaxParentVisibleSize) { return new FloatPropertyMarkerValue(this, 0, fractionOfMaxParentVisibleSize); } /** * Create an {@link FloatPropertyMarkerValue} object by multiplying the fraction with * {@link FloatParallax#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 FloatParallax#getMaxValue()} for * the marker value. * @return A new {@link FloatPropertyMarkerValue} object. */ public final FloatPropertyMarkerValue at(float offsetValue, float fractionOfMaxParentVisibleSize) { return new FloatPropertyMarkerValue(this, offsetValue, fractionOfMaxParentVisibleSize); } } /** * Parallax that manages a list of {@link FloatProperty}. App may override this class with a * specific {@link FloatProperty} subclass. * * @param <FloatPropertyT> Type of {@link FloatProperty} or subclass. */ public abstract static class FloatParallax<FloatPropertyT extends FloatProperty> extends Parallax<FloatPropertyT> { private float[] mValues = new float[4]; /** * Get index based property value. * * @param index Index of the property. * @return Value of the property. */ public final float getPropertyValue(int index) { return mValues[index]; } /** * Set index based property value. * * @param index Index of the property. * @param value Value of the property. */ public final void setPropertyValue(int index, float value) { if (index >= mProperties.size()) { throw new ArrayIndexOutOfBoundsException(); } mValues[index] = value; } /** * 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 FloatPropertyMarkerValue#FloatPropertyMarkerValue(FloatProperty, float, float) */ public abstract float getMaxValue(); @Override public final FloatPropertyT addProperty(String name) { int newPropertyIndex = mProperties.size(); FloatPropertyT property = createProperty(name, newPropertyIndex); mProperties.add(property); int size = mValues.length; if (size == newPropertyIndex) { float[] newValues = new float[size * 2]; for (int i = 0; i < size; i++) { newValues[i] = mValues[i]; } mValues = newValues; } mValues[newPropertyIndex] = FloatProperty.UNKNOWN_AFTER; return property; } @Override public final void verifyProperties() throws IllegalStateException { if (mProperties.size() < 2) { return; } float last = mProperties.get(0).getFloatValue(this); for (int i = 1; i < mProperties.size(); i++) { float v = mProperties.get(i).getFloatValue(this); 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; } } } /** * Implementation of {@link PropertyMarkerValue} for {@link FloatProperty}. */ public static class FloatPropertyMarkerValue extends PropertyMarkerValue<FloatProperty> { private final float mValue; private final float mFactionOfMax; public FloatPropertyMarkerValue(FloatProperty property, float value) { this(property, value, 0f); } public FloatPropertyMarkerValue(FloatProperty property, float value, float fractionOfMax) { super(property); mValue = value; mFactionOfMax = fractionOfMax; } /** * @return The marker value. */ public final float getMarkerValue(FloatParallax source) { return mFactionOfMax == 0 ? mValue : mValue + source.getMaxValue() * mFactionOfMax; } } final List<PropertyT> mProperties = new ArrayList<PropertyT>(); final List<PropertyT> mPropertiesReadOnly = Collections.unmodifiableList(mProperties); /** * @return A unmodifiable list of properties. */ public final List<PropertyT> getProperties() { return mPropertiesReadOnly; } /** * Add a new Property in the Parallax object. * * @param name Name of the property. * @return Newly created Property. */ public abstract PropertyT addProperty(String name); /** * 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); /** * 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. */ public abstract void verifyProperties() throws IllegalStateException; /** * 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); } } /** * Adds a {@link ParallaxEffect} object which defines rules to perform mapping to multiple * {@link ParallaxTarget}s. * * @param effect A {@link ParallaxEffect} object. */ public void addEffect(ParallaxEffect effect) { mEffects.add(effect); } /** * 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(IntPropertyMarkerValue... ranges) { IntEffect effect = new IntEffect(); effect.setPropertyRanges(ranges); addEffect(effect); return effect; } /** * 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(FloatPropertyMarkerValue... ranges) { FloatEffect effect = new FloatEffect(); effect.setPropertyRanges(ranges); addEffect(effect); return effect; } }