1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.v17.leanback.widget; 18 19 import android.animation.PropertyValuesHolder; 20 import android.support.v17.leanback.widget.Parallax.FloatProperty; 21 import android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue; 22 import android.support.v17.leanback.widget.Parallax.IntProperty; 23 import android.support.v17.leanback.widget.Parallax.PropertyMarkerValue; 24 import android.util.Property; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 29 /** 30 * ParallaxEffect class drives changes in {@link ParallaxTarget} in response to changes in 31 * variables defined in {@link Parallax}. 32 * <p> 33 * ParallaxEffect has a list of {@link Parallax.PropertyMarkerValue}s which represents the range of 34 * values that source variables can take. The main function is 35 * {@link ParallaxEffect#performMapping(Parallax)} which computes a fraction between 0 and 1 36 * based on the current values of variables in {@link Parallax}. As the parallax effect goes 37 * on, the fraction increases from 0 at beginning to 1 at the end. Then the fraction is passed on 38 * to {@link ParallaxTarget#update(float)}. 39 * <p> 40 * App use {@link Parallax#addEffect(PropertyMarkerValue...)} to create a ParallaxEffect. 41 */ 42 public abstract class ParallaxEffect { 43 44 final List<Parallax.PropertyMarkerValue> mMarkerValues = new ArrayList(2); 45 final List<Float> mWeights = new ArrayList<Float>(2); 46 final List<Float> mTotalWeights = new ArrayList<Float>(2); 47 final List<ParallaxTarget> mTargets = new ArrayList<ParallaxTarget>(4); 48 49 /** 50 * Only accessible from package 51 */ ParallaxEffect()52 ParallaxEffect() { 53 } 54 55 /** 56 * Returns the list of {@link PropertyMarkerValue}s, which represents the range of values that 57 * source variables can take. 58 * 59 * @return A list of {@link Parallax.PropertyMarkerValue}s. 60 * @see #performMapping(Parallax) 61 */ getPropertyRanges()62 public final List<Parallax.PropertyMarkerValue> getPropertyRanges() { 63 return mMarkerValues; 64 } 65 66 /** 67 * Returns a list of Float objects that represents weight associated with each variable range. 68 * Weights are used when there are three or more marker values. 69 * 70 * @return A list of Float objects that represents weight associated with each variable range. 71 * @hide 72 */ getWeights()73 public final List<Float> getWeights() { 74 return mWeights; 75 } 76 77 /** 78 * Sets the list of {@link PropertyMarkerValue}s, which represents the range of values that 79 * source variables can take. 80 * 81 * @param markerValues A list of {@link PropertyMarkerValue}s. 82 * @see #performMapping(Parallax) 83 */ setPropertyRanges(Parallax.PropertyMarkerValue... markerValues)84 public final void setPropertyRanges(Parallax.PropertyMarkerValue... markerValues) { 85 mMarkerValues.clear(); 86 for (Parallax.PropertyMarkerValue markerValue : markerValues) { 87 mMarkerValues.add(markerValue); 88 } 89 } 90 91 /** 92 * Sets a list of Float objects that represents weight associated with each variable range. 93 * Weights are used when there are three or more marker values. 94 * 95 * @param weights A list of Float objects that represents weight associated with each variable 96 * range. 97 * @hide 98 */ setWeights(float... weights)99 public final void setWeights(float... weights) { 100 for (float weight : weights) { 101 if (weight <= 0) { 102 throw new IllegalArgumentException(); 103 } 104 } 105 mWeights.clear(); 106 mTotalWeights.clear(); 107 float totalWeight = 0f; 108 for (float weight : weights) { 109 mWeights.add(weight); 110 totalWeight += weight; 111 mTotalWeights.add(totalWeight); 112 } 113 } 114 115 /** 116 * Sets a list of Float objects that represents weight associated with each variable range. 117 * Weights are used when there are three or more marker values. 118 * 119 * @param weights A list of Float objects that represents weight associated with each variable 120 * range. 121 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 122 * @hide 123 */ weights(float... weights)124 public final ParallaxEffect weights(float... weights) { 125 setWeights(weights); 126 return this; 127 } 128 129 /** 130 * Add a ParallaxTarget to run parallax effect. 131 * 132 * @param target ParallaxTarget to add. 133 */ addTarget(ParallaxTarget target)134 public final void addTarget(ParallaxTarget target) { 135 mTargets.add(target); 136 } 137 138 /** 139 * Add a ParallaxTarget to run parallax effect. 140 * 141 * @param target ParallaxTarget to add. 142 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 143 */ target(ParallaxTarget target)144 public final ParallaxEffect target(ParallaxTarget target) { 145 mTargets.add(target); 146 return this; 147 } 148 149 /** 150 * Creates a {@link ParallaxTarget} from {@link PropertyValuesHolder} and adds it to the list 151 * of targets. 152 * 153 * @param targetObject Target object for PropertyValuesHolderTarget. 154 * @param values PropertyValuesHolder for PropertyValuesHolderTarget. 155 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 156 */ target(Object targetObject, PropertyValuesHolder values)157 public final ParallaxEffect target(Object targetObject, PropertyValuesHolder values) { 158 mTargets.add(new ParallaxTarget.PropertyValuesHolderTarget(targetObject, values)); 159 return this; 160 } 161 162 /** 163 * Creates a {@link ParallaxTarget} using direct mapping from source property into target 164 * property, the new {@link ParallaxTarget} will be added to its list of targets. 165 * 166 * @param targetObject Target object for property. 167 * @param targetProperty The target property that will receive values. 168 * @return This ParallaxEffect object, allowing calls to methods in this class to be chained. 169 * @param <T> Type of target object. 170 * @param <V> Type of target property value, either Integer or Float. 171 * @see ParallaxTarget#isDirectMapping() 172 */ target(T targetObject, Property<T, V> targetProperty)173 public final <T, V extends Number> ParallaxEffect target(T targetObject, 174 Property<T, V> targetProperty) { 175 mTargets.add(new ParallaxTarget.DirectPropertyTarget(targetObject, targetProperty)); 176 return this; 177 } 178 179 /** 180 * Returns the list of {@link ParallaxTarget} objects. 181 * 182 * @return The list of {@link ParallaxTarget} objects. 183 */ getTargets()184 public final List<ParallaxTarget> getTargets() { 185 return mTargets; 186 } 187 188 /** 189 * Remove a {@link ParallaxTarget} object from the list. 190 * @param target The {@link ParallaxTarget} object to be removed. 191 */ removeTarget(ParallaxTarget target)192 public final void removeTarget(ParallaxTarget target) { 193 mTargets.remove(target); 194 } 195 196 /** 197 * Perform mapping from {@link Parallax} to list of {@link ParallaxTarget}. 198 */ performMapping(Parallax source)199 public final void performMapping(Parallax source) { 200 if (mMarkerValues.size() < 2) { 201 return; 202 } 203 if (this instanceof IntEffect) { 204 source.verifyIntProperties(); 205 } else { 206 source.verifyFloatProperties(); 207 } 208 boolean fractionCalculated = false; 209 float fraction = 0; 210 Number directValue = null; 211 for (int i = 0; i < mTargets.size(); i++) { 212 ParallaxTarget target = mTargets.get(i); 213 if (target.isDirectMapping()) { 214 if (directValue == null) { 215 directValue = calculateDirectValue(source); 216 } 217 target.directUpdate(directValue); 218 } else { 219 if (!fractionCalculated) { 220 fractionCalculated = true; 221 fraction = calculateFraction(source); 222 } 223 target.update(fraction); 224 } 225 } 226 } 227 228 /** 229 * This method is expected to compute a fraction between 0 and 1 based on the current values of 230 * variables in {@link Parallax}. As the parallax effect goes on, the fraction increases 231 * from 0 at beginning to 1 at the end. 232 * 233 * @return Float value between 0 and 1. 234 */ calculateFraction(Parallax source)235 abstract float calculateFraction(Parallax source); 236 237 /** 238 * This method is expected to get the current value of the single {@link IntProperty} or 239 * {@link FloatProperty}. 240 * 241 * @return Current value of the single {@link IntProperty} or {@link FloatProperty}. 242 */ calculateDirectValue(Parallax source)243 abstract Number calculateDirectValue(Parallax source); 244 245 /** 246 * When there are multiple ranges (aka three or more markerValues), this method adjust the 247 * fraction inside a range to fraction of whole range. 248 * e.g. four marker values, three weight values: 6, 2, 2. totalWeights are 6, 8, 10 249 * When markerValueIndex is 3, the fraction is inside last range. 250 * adjusted_fraction = 8 / 10 + 2 / 10 * fraction. 251 */ getFractionWithWeightAdjusted(float fraction, int markerValueIndex)252 final float getFractionWithWeightAdjusted(float fraction, int markerValueIndex) { 253 // when there are three or more markerValues, take weight into consideration. 254 if (mMarkerValues.size() >= 3) { 255 final boolean hasWeightsDefined = mWeights.size() == mMarkerValues.size() - 1; 256 if (hasWeightsDefined) { 257 // use weights user defined 258 final float allWeights = mTotalWeights.get(mTotalWeights.size() - 1); 259 fraction = fraction * mWeights.get(markerValueIndex - 1) / allWeights; 260 if (markerValueIndex >= 2) { 261 fraction += mTotalWeights.get(markerValueIndex - 2) / allWeights; 262 } 263 } else { 264 // assume each range has same weight. 265 final float allWeights = mMarkerValues.size() - 1; 266 fraction = fraction / allWeights; 267 if (markerValueIndex >= 2) { 268 fraction += (float) (markerValueIndex - 1) / allWeights; 269 } 270 } 271 } 272 return fraction; 273 } 274 275 /** 276 * Implementation of {@link ParallaxEffect} for integer type. 277 */ 278 static final class IntEffect extends ParallaxEffect { 279 280 @Override calculateDirectValue(Parallax source)281 Number calculateDirectValue(Parallax source) { 282 if (mMarkerValues.size() != 2) { 283 throw new RuntimeException("Must use two marker values for direct mapping"); 284 } 285 if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) { 286 throw new RuntimeException( 287 "Marker value must use same Property for direct mapping"); 288 } 289 int value1 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(0)) 290 .getMarkerValue(source); 291 int value2 = ((Parallax.IntPropertyMarkerValue) mMarkerValues.get(1)) 292 .getMarkerValue(source); 293 if (value1 > value2) { 294 int swapValue = value2; 295 value2 = value1; 296 value1 = swapValue; 297 } 298 299 Number currentValue = ((IntProperty) mMarkerValues.get(0).getProperty()).get(source); 300 if (currentValue.intValue() < value1) { 301 currentValue = value1; 302 } else if (currentValue.intValue() > value2) { 303 currentValue = value2; 304 } 305 return currentValue; 306 } 307 308 @Override calculateFraction(Parallax source)309 float calculateFraction(Parallax source) { 310 int lastIndex = 0; 311 int lastValue = 0; 312 int lastMarkerValue = 0; 313 // go through all markerValues, find first markerValue that current value is less than. 314 for (int i = 0; i < mMarkerValues.size(); i++) { 315 Parallax.IntPropertyMarkerValue k = (Parallax.IntPropertyMarkerValue) 316 mMarkerValues.get(i); 317 int index = k.getProperty().getIndex(); 318 int markerValue = k.getMarkerValue(source); 319 int currentValue = source.getIntPropertyValue(index); 320 321 float fraction; 322 if (i == 0) { 323 if (currentValue >= markerValue) { 324 return 0f; 325 } 326 } else { 327 if (lastIndex == index && lastMarkerValue < markerValue) { 328 throw new IllegalStateException("marker value of same variable must be " 329 + "descendant order"); 330 } 331 if (currentValue == IntProperty.UNKNOWN_AFTER) { 332 // Implies lastValue is less than lastMarkerValue and lastValue is not 333 // UNKNWON_AFTER. Estimates based on distance of two variables is screen 334 // size. 335 fraction = (float) (lastMarkerValue - lastValue) 336 / source.getMaxValue(); 337 return getFractionWithWeightAdjusted(fraction, i); 338 } else if (currentValue >= markerValue) { 339 if (lastIndex == index) { 340 // same variable index, same UI element at two different MarkerValues, 341 // e.g. UI element moves from lastMarkerValue=500 to markerValue=0, 342 // fraction moves from 0 to 1. 343 fraction = (float) (lastMarkerValue - currentValue) 344 / (lastMarkerValue - markerValue); 345 } else if (lastValue != IntProperty.UNKNOWN_BEFORE) { 346 // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when 347 // UIElement_1 is at markerValue=300, markerValue of UIElement_2 by 348 // adding delta of values to markerValue of UIElement_2. 349 lastMarkerValue = lastMarkerValue + (currentValue - lastValue); 350 fraction = (float) (lastMarkerValue - currentValue) 351 / (lastMarkerValue - markerValue); 352 } else { 353 // Last variable is UNKNOWN_BEFORE. Estimates based on assumption total 354 // travel distance from last variable to this variable is screen visible 355 // size. 356 fraction = 1f - (float) (currentValue - markerValue) 357 / source.getMaxValue(); 358 } 359 return getFractionWithWeightAdjusted(fraction, i); 360 } 361 } 362 lastValue = currentValue; 363 lastIndex = index; 364 lastMarkerValue = markerValue; 365 } 366 return 1f; 367 } 368 } 369 370 /** 371 * Implementation of {@link ParallaxEffect} for float type. 372 */ 373 static final class FloatEffect extends ParallaxEffect { 374 375 @Override calculateDirectValue(Parallax source)376 Number calculateDirectValue(Parallax source) { 377 if (mMarkerValues.size() != 2) { 378 throw new RuntimeException("Must use two marker values for direct mapping"); 379 } 380 if (mMarkerValues.get(0).getProperty() != mMarkerValues.get(1).getProperty()) { 381 throw new RuntimeException( 382 "Marker value must use same Property for direct mapping"); 383 } 384 float value1 = ((FloatPropertyMarkerValue) mMarkerValues.get(0)) 385 .getMarkerValue(source); 386 float value2 = ((FloatPropertyMarkerValue) mMarkerValues.get(1)) 387 .getMarkerValue(source); 388 if (value1 > value2) { 389 float swapValue = value2; 390 value2 = value1; 391 value1 = swapValue; 392 } 393 394 Number currentValue = ((FloatProperty) mMarkerValues.get(0).getProperty()).get(source); 395 if (currentValue.floatValue() < value1) { 396 currentValue = value1; 397 } else if (currentValue.floatValue() > value2) { 398 currentValue = value2; 399 } 400 return currentValue; 401 } 402 403 @Override calculateFraction(Parallax source)404 float calculateFraction(Parallax source) { 405 int lastIndex = 0; 406 float lastValue = 0; 407 float lastMarkerValue = 0; 408 // go through all markerValues, find first markerValue that current value is less than. 409 for (int i = 0; i < mMarkerValues.size(); i++) { 410 FloatPropertyMarkerValue k = (FloatPropertyMarkerValue) mMarkerValues.get(i); 411 int index = k.getProperty().getIndex(); 412 float markerValue = k.getMarkerValue(source); 413 float currentValue = source.getFloatPropertyValue(index); 414 415 float fraction; 416 if (i == 0) { 417 if (currentValue >= markerValue) { 418 return 0f; 419 } 420 } else { 421 if (lastIndex == index && lastMarkerValue < markerValue) { 422 throw new IllegalStateException("marker value of same variable must be " 423 + "descendant order"); 424 } 425 if (currentValue == FloatProperty.UNKNOWN_AFTER) { 426 // Implies lastValue is less than lastMarkerValue and lastValue is not 427 // UNKNOWN_AFTER. Estimates based on distance of two variables is screen 428 // size. 429 fraction = (float) (lastMarkerValue - lastValue) 430 / source.getMaxValue(); 431 return getFractionWithWeightAdjusted(fraction, i); 432 } else if (currentValue >= markerValue) { 433 if (lastIndex == index) { 434 // same variable index, same UI element at two different MarkerValues, 435 // e.g. UI element moves from lastMarkerValue=500 to markerValue=0, 436 // fraction moves from 0 to 1. 437 fraction = (float) (lastMarkerValue - currentValue) 438 / (lastMarkerValue - markerValue); 439 } else if (lastValue != FloatProperty.UNKNOWN_BEFORE) { 440 // e.g. UIElement_1 at 300 scroll to UIElement_2 at 400, figure out when 441 // UIElement_1 is at markerValue=300, markerValue of UIElement_2 by 442 // adding delta of values to markerValue of UIElement_2. 443 lastMarkerValue = lastMarkerValue + (currentValue - lastValue); 444 fraction = (float) (lastMarkerValue - currentValue) 445 / (lastMarkerValue - markerValue); 446 } else { 447 // Last variable is UNKNOWN_BEFORE. Estimates based on assumption total 448 // travel distance from last variable to this variable is screen visible 449 // size. 450 fraction = 1f - (float) (currentValue - markerValue) 451 / source.getMaxValue(); 452 } 453 return getFractionWithWeightAdjusted(fraction, i); 454 } 455 } 456 lastValue = currentValue; 457 lastIndex = index; 458 lastMarkerValue = markerValue; 459 } 460 return 1f; 461 } 462 } 463 464 } 465 466