1 /* 2 * Copyright (C) 2014 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 com.android.systemui.statusbar.phone; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.view.MotionEvent; 24 import android.view.VelocityTracker; 25 import android.view.View; 26 import android.view.ViewConfiguration; 27 28 import com.android.systemui.R; 29 import com.android.systemui.animation.Interpolators; 30 import com.android.systemui.classifier.Classifier; 31 import com.android.systemui.plugins.FalsingManager; 32 import com.android.systemui.statusbar.KeyguardAffordanceView; 33 import com.android.wm.shell.animation.FlingAnimationUtils; 34 35 /** 36 * A touch handler of the keyguard which is responsible for launching phone and camera affordances. 37 */ 38 public class KeyguardAffordanceHelper { 39 40 public static final long HINT_PHASE1_DURATION = 200; 41 private static final long HINT_PHASE2_DURATION = 350; 42 private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f; 43 private static final int HINT_CIRCLE_OPEN_DURATION = 500; 44 45 private final Context mContext; 46 private final Callback mCallback; 47 48 private FlingAnimationUtils mFlingAnimationUtils; 49 private VelocityTracker mVelocityTracker; 50 private boolean mSwipingInProgress; 51 private float mInitialTouchX; 52 private float mInitialTouchY; 53 private float mTranslation; 54 private float mTranslationOnDown; 55 private int mTouchSlop; 56 private int mMinTranslationAmount; 57 private int mMinFlingVelocity; 58 private int mHintGrowAmount; 59 private KeyguardAffordanceView mLeftIcon; 60 private KeyguardAffordanceView mRightIcon; 61 private Animator mSwipeAnimator; 62 private final FalsingManager mFalsingManager; 63 private int mMinBackgroundRadius; 64 private boolean mMotionCancelled; 65 private int mTouchTargetSize; 66 private View mTargetedView; 67 private boolean mTouchSlopExeeded; 68 private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() { 69 @Override 70 public void onAnimationEnd(Animator animation) { 71 mSwipeAnimator = null; 72 mSwipingInProgress = false; 73 mTargetedView = null; 74 } 75 }; 76 private Runnable mAnimationEndRunnable = new Runnable() { 77 @Override 78 public void run() { 79 mCallback.onAnimationToSideEnded(); 80 } 81 }; 82 KeyguardAffordanceHelper(Callback callback, Context context, FalsingManager falsingManager)83 KeyguardAffordanceHelper(Callback callback, Context context, FalsingManager falsingManager) { 84 mContext = context; 85 mCallback = callback; 86 initIcons(); 87 updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false); 88 updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false); 89 mFalsingManager = falsingManager; 90 initDimens(); 91 } 92 initDimens()93 private void initDimens() { 94 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 95 mTouchSlop = configuration.getScaledPagingTouchSlop(); 96 mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 97 mMinTranslationAmount = mContext.getResources().getDimensionPixelSize( 98 R.dimen.keyguard_min_swipe_amount); 99 mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( 100 R.dimen.keyguard_affordance_min_background_radius); 101 mTouchTargetSize = mContext.getResources().getDimensionPixelSize( 102 R.dimen.keyguard_affordance_touch_target_size); 103 mHintGrowAmount = 104 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); 105 mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(), 106 0.4f); 107 } 108 initIcons()109 private void initIcons() { 110 mLeftIcon = mCallback.getLeftIcon(); 111 mRightIcon = mCallback.getRightIcon(); 112 updatePreviews(); 113 } 114 updatePreviews()115 public void updatePreviews() { 116 mLeftIcon.setPreviewView(mCallback.getLeftPreview()); 117 mRightIcon.setPreviewView(mCallback.getRightPreview()); 118 } 119 onTouchEvent(MotionEvent event)120 public boolean onTouchEvent(MotionEvent event) { 121 int action = event.getActionMasked(); 122 if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) { 123 return false; 124 } 125 final float y = event.getY(); 126 final float x = event.getX(); 127 128 boolean isUp = false; 129 switch (action) { 130 case MotionEvent.ACTION_DOWN: 131 View targetView = getIconAtPosition(x, y); 132 if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) { 133 mMotionCancelled = true; 134 return false; 135 } 136 if (mTargetedView != null) { 137 cancelAnimation(); 138 } else { 139 mTouchSlopExeeded = false; 140 } 141 startSwiping(targetView); 142 mInitialTouchX = x; 143 mInitialTouchY = y; 144 mTranslationOnDown = mTranslation; 145 initVelocityTracker(); 146 trackMovement(event); 147 mMotionCancelled = false; 148 break; 149 case MotionEvent.ACTION_POINTER_DOWN: 150 mMotionCancelled = true; 151 endMotion(true /* forceSnapBack */, x, y); 152 break; 153 case MotionEvent.ACTION_MOVE: 154 trackMovement(event); 155 float xDist = x - mInitialTouchX; 156 float yDist = y - mInitialTouchY; 157 float distance = (float) Math.hypot(xDist, yDist); 158 if (!mTouchSlopExeeded && distance > mTouchSlop) { 159 mTouchSlopExeeded = true; 160 } 161 if (mSwipingInProgress) { 162 if (mTargetedView == mRightIcon) { 163 distance = mTranslationOnDown - distance; 164 distance = Math.min(0, distance); 165 } else { 166 distance = mTranslationOnDown + distance; 167 distance = Math.max(0, distance); 168 } 169 setTranslation(distance, false /* isReset */, false /* animateReset */); 170 } 171 break; 172 173 case MotionEvent.ACTION_UP: 174 isUp = true; 175 case MotionEvent.ACTION_CANCEL: 176 boolean hintOnTheRight = mTargetedView == mRightIcon; 177 trackMovement(event); 178 endMotion(!isUp, x, y); 179 if (!mTouchSlopExeeded && isUp) { 180 mCallback.onIconClicked(hintOnTheRight); 181 } 182 break; 183 } 184 return true; 185 } 186 startSwiping(View targetView)187 private void startSwiping(View targetView) { 188 mCallback.onSwipingStarted(targetView == mRightIcon); 189 mSwipingInProgress = true; 190 mTargetedView = targetView; 191 } 192 getIconAtPosition(float x, float y)193 private View getIconAtPosition(float x, float y) { 194 if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) { 195 return mLeftIcon; 196 } 197 if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) { 198 return mRightIcon; 199 } 200 return null; 201 } 202 isOnAffordanceIcon(float x, float y)203 public boolean isOnAffordanceIcon(float x, float y) { 204 return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y); 205 } 206 isOnIcon(View icon, float x, float y)207 private boolean isOnIcon(View icon, float x, float y) { 208 float iconX = icon.getX() + icon.getWidth() / 2.0f; 209 float iconY = icon.getY() + icon.getHeight() / 2.0f; 210 double distance = Math.hypot(x - iconX, y - iconY); 211 return distance <= mTouchTargetSize / 2; 212 } 213 endMotion(boolean forceSnapBack, float lastX, float lastY)214 private void endMotion(boolean forceSnapBack, float lastX, float lastY) { 215 if (mSwipingInProgress) { 216 flingWithCurrentVelocity(forceSnapBack, lastX, lastY); 217 } else { 218 mTargetedView = null; 219 } 220 if (mVelocityTracker != null) { 221 mVelocityTracker.recycle(); 222 mVelocityTracker = null; 223 } 224 } 225 rightSwipePossible()226 private boolean rightSwipePossible() { 227 return mRightIcon.getVisibility() == View.VISIBLE; 228 } 229 leftSwipePossible()230 private boolean leftSwipePossible() { 231 return mLeftIcon.getVisibility() == View.VISIBLE; 232 } 233 onInterceptTouchEvent(MotionEvent ev)234 public boolean onInterceptTouchEvent(MotionEvent ev) { 235 return false; 236 } 237 startHintAnimation(boolean right, Runnable onFinishedListener)238 public void startHintAnimation(boolean right, 239 Runnable onFinishedListener) { 240 cancelAnimation(); 241 startHintAnimationPhase1(right, onFinishedListener); 242 } 243 startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener)244 private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) { 245 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 246 ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount); 247 animator.addListener(new AnimatorListenerAdapter() { 248 private boolean mCancelled; 249 250 @Override 251 public void onAnimationCancel(Animator animation) { 252 mCancelled = true; 253 } 254 255 @Override 256 public void onAnimationEnd(Animator animation) { 257 if (mCancelled) { 258 mSwipeAnimator = null; 259 mTargetedView = null; 260 onFinishedListener.run(); 261 } else { 262 startUnlockHintAnimationPhase2(right, onFinishedListener); 263 } 264 } 265 }); 266 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 267 animator.setDuration(HINT_PHASE1_DURATION); 268 animator.start(); 269 mSwipeAnimator = animator; 270 mTargetedView = targetView; 271 } 272 273 /** 274 * Phase 2: Move back. 275 */ startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener)276 private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) { 277 ValueAnimator animator = getAnimatorToRadius(right, 0); 278 animator.addListener(new AnimatorListenerAdapter() { 279 @Override 280 public void onAnimationEnd(Animator animation) { 281 mSwipeAnimator = null; 282 mTargetedView = null; 283 onFinishedListener.run(); 284 } 285 }); 286 animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 287 animator.setDuration(HINT_PHASE2_DURATION); 288 animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); 289 animator.start(); 290 mSwipeAnimator = animator; 291 } 292 getAnimatorToRadius(final boolean right, int radius)293 private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { 294 final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 295 ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); 296 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 297 @Override 298 public void onAnimationUpdate(ValueAnimator animation) { 299 float newRadius = (float) animation.getAnimatedValue(); 300 targetView.setCircleRadiusWithoutAnimation(newRadius); 301 float translation = getTranslationFromRadius(newRadius); 302 mTranslation = right ? -translation : translation; 303 updateIconsFromTranslation(targetView); 304 } 305 }); 306 return animator; 307 } 308 cancelAnimation()309 private void cancelAnimation() { 310 if (mSwipeAnimator != null) { 311 mSwipeAnimator.cancel(); 312 } 313 } 314 flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY)315 private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) { 316 float vel = getCurrentVelocity(lastX, lastY); 317 318 // We snap back if the current translation is not far enough 319 boolean snapBack = false; 320 if (mCallback.needsAntiFalsing()) { 321 snapBack = snapBack || mFalsingManager.isFalseTouch( 322 mTargetedView == mRightIcon 323 ? Classifier.RIGHT_AFFORDANCE : Classifier.LEFT_AFFORDANCE); 324 } 325 snapBack = snapBack || isBelowFalsingThreshold(); 326 327 // or if the velocity is in the opposite direction. 328 boolean velIsInWrongDirection = vel * mTranslation < 0; 329 snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; 330 vel = snapBack ^ velIsInWrongDirection ? 0 : vel; 331 fling(vel, snapBack || forceSnapBack, mTranslation < 0); 332 } 333 isBelowFalsingThreshold()334 private boolean isBelowFalsingThreshold() { 335 return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount(); 336 } 337 getMinTranslationAmount()338 private int getMinTranslationAmount() { 339 float factor = mCallback.getAffordanceFalsingFactor(); 340 return (int) (mMinTranslationAmount * factor); 341 } 342 fling(float vel, final boolean snapBack, boolean right)343 private void fling(float vel, final boolean snapBack, boolean right) { 344 float target = right ? -mCallback.getMaxTranslationDistance() 345 : mCallback.getMaxTranslationDistance(); 346 target = snapBack ? 0 : target; 347 348 ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); 349 mFlingAnimationUtils.apply(animator, mTranslation, target, vel); 350 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 351 @Override 352 public void onAnimationUpdate(ValueAnimator animation) { 353 mTranslation = (float) animation.getAnimatedValue(); 354 } 355 }); 356 animator.addListener(mFlingEndListener); 357 if (!snapBack) { 358 startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right); 359 mCallback.onAnimationToSideStarted(right, mTranslation, vel); 360 } else { 361 reset(true); 362 } 363 animator.start(); 364 mSwipeAnimator = animator; 365 if (snapBack) { 366 mCallback.onSwipingAborted(); 367 } 368 } 369 startFinishingCircleAnimation(float velocity, Runnable animationEndRunnable, boolean right)370 private void startFinishingCircleAnimation(float velocity, Runnable animationEndRunnable, 371 boolean right) { 372 KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon; 373 targetView.finishAnimation(velocity, animationEndRunnable); 374 } 375 setTranslation(float translation, boolean isReset, boolean animateReset)376 private void setTranslation(float translation, boolean isReset, boolean animateReset) { 377 translation = rightSwipePossible() ? translation : Math.max(0, translation); 378 translation = leftSwipePossible() ? translation : Math.min(0, translation); 379 float absTranslation = Math.abs(translation); 380 if (translation != mTranslation || isReset) { 381 KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon; 382 KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon; 383 float alpha = absTranslation / getMinTranslationAmount(); 384 385 // We interpolate the alpha of the other icons to 0 386 float fadeOutAlpha = 1.0f - alpha; 387 fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f); 388 389 boolean animateIcons = isReset && animateReset; 390 boolean forceNoCircleAnimation = isReset && !animateReset; 391 float radius = getRadiusFromTranslation(absTranslation); 392 boolean slowAnimation = isReset && isBelowFalsingThreshold(); 393 if (!isReset) { 394 updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(), 395 false, false, false, false); 396 } else { 397 updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(), 398 animateIcons, slowAnimation, true /* isReset */, forceNoCircleAnimation); 399 } 400 updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(), 401 animateIcons, slowAnimation, isReset, forceNoCircleAnimation); 402 403 mTranslation = translation; 404 } 405 } 406 updateIconsFromTranslation(KeyguardAffordanceView targetView)407 private void updateIconsFromTranslation(KeyguardAffordanceView targetView) { 408 float absTranslation = Math.abs(mTranslation); 409 float alpha = absTranslation / getMinTranslationAmount(); 410 411 // We interpolate the alpha of the other icons to 0 412 float fadeOutAlpha = 1.0f - alpha; 413 fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); 414 415 // We interpolate the alpha of the targetView to 1 416 KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon; 417 updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false); 418 updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false); 419 } 420 getTranslationFromRadius(float circleSize)421 private float getTranslationFromRadius(float circleSize) { 422 float translation = (circleSize - mMinBackgroundRadius) 423 / BACKGROUND_RADIUS_SCALE_FACTOR; 424 return translation > 0.0f ? translation + mTouchSlop : 0.0f; 425 } 426 getRadiusFromTranslation(float translation)427 private float getRadiusFromTranslation(float translation) { 428 if (translation <= mTouchSlop) { 429 return 0.0f; 430 } 431 return (translation - mTouchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius; 432 } 433 animateHideLeftRightIcon()434 public void animateHideLeftRightIcon() { 435 cancelAnimation(); 436 updateIcon(mRightIcon, 0f, 0f, true, false, false, false); 437 updateIcon(mLeftIcon, 0f, 0f, true, false, false, false); 438 } 439 updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, boolean animate, boolean slowRadiusAnimation, boolean force, boolean forceNoCircleAnimation)440 private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha, 441 boolean animate, boolean slowRadiusAnimation, boolean force, 442 boolean forceNoCircleAnimation) { 443 if (view.getVisibility() != View.VISIBLE && !force) { 444 return; 445 } 446 if (forceNoCircleAnimation) { 447 view.setCircleRadiusWithoutAnimation(circleRadius); 448 } else { 449 view.setCircleRadius(circleRadius, slowRadiusAnimation); 450 } 451 updateIconAlpha(view, alpha, animate); 452 } 453 updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate)454 private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) { 455 float scale = getScale(alpha, view); 456 alpha = Math.min(1.0f, alpha); 457 view.setImageAlpha(alpha, animate); 458 view.setImageScale(scale, animate); 459 } 460 getScale(float alpha, KeyguardAffordanceView icon)461 private float getScale(float alpha, KeyguardAffordanceView icon) { 462 float scale = alpha / icon.getRestingAlpha() * 0.2f + 463 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT; 464 return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT); 465 } 466 trackMovement(MotionEvent event)467 private void trackMovement(MotionEvent event) { 468 if (mVelocityTracker != null) { 469 mVelocityTracker.addMovement(event); 470 } 471 } 472 initVelocityTracker()473 private void initVelocityTracker() { 474 if (mVelocityTracker != null) { 475 mVelocityTracker.recycle(); 476 } 477 mVelocityTracker = VelocityTracker.obtain(); 478 } 479 getCurrentVelocity(float lastX, float lastY)480 private float getCurrentVelocity(float lastX, float lastY) { 481 if (mVelocityTracker == null) { 482 return 0; 483 } 484 mVelocityTracker.computeCurrentVelocity(1000); 485 float aX = mVelocityTracker.getXVelocity(); 486 float aY = mVelocityTracker.getYVelocity(); 487 float bX = lastX - mInitialTouchX; 488 float bY = lastY - mInitialTouchY; 489 float bLen = (float) Math.hypot(bX, bY); 490 // Project the velocity onto the distance vector: a * b / |b| 491 float projectedVelocity = (aX * bX + aY * bY) / bLen; 492 if (mTargetedView == mRightIcon) { 493 projectedVelocity = -projectedVelocity; 494 } 495 return projectedVelocity; 496 } 497 onConfigurationChanged()498 public void onConfigurationChanged() { 499 initDimens(); 500 initIcons(); 501 } 502 onRtlPropertiesChanged()503 public void onRtlPropertiesChanged() { 504 initIcons(); 505 } 506 reset(boolean animate)507 public void reset(boolean animate) { 508 cancelAnimation(); 509 setTranslation(0.0f, true /* isReset */, animate); 510 mMotionCancelled = true; 511 if (mSwipingInProgress) { 512 mCallback.onSwipingAborted(); 513 mSwipingInProgress = false; 514 } 515 } 516 isSwipingInProgress()517 public boolean isSwipingInProgress() { 518 return mSwipingInProgress; 519 } 520 launchAffordance(boolean animate, boolean left)521 public void launchAffordance(boolean animate, boolean left) { 522 if (mSwipingInProgress) { 523 // We don't want to mess with the state if the user is actually swiping already. 524 return; 525 } 526 KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon; 527 KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon; 528 startSwiping(targetView); 529 530 // Do not animate the circle expanding if the affordance isn't visible, 531 // otherwise the circle will be meaningless. 532 if (targetView.getVisibility() != View.VISIBLE) { 533 animate = false; 534 } 535 536 if (animate) { 537 fling(0, false, !left); 538 updateIcon(otherView, 0.0f, 0, true, false, true, false); 539 } else { 540 mCallback.onAnimationToSideStarted(!left, mTranslation, 0); 541 mTranslation = left ? mCallback.getMaxTranslationDistance() 542 : mCallback.getMaxTranslationDistance(); 543 updateIcon(otherView, 0.0f, 0.0f, false, false, true, false); 544 targetView.instantFinishAnimation(); 545 mFlingEndListener.onAnimationEnd(null); 546 mAnimationEndRunnable.run(); 547 } 548 } 549 550 public interface Callback { 551 552 /** 553 * Notifies the callback when an animation to a side page was started. 554 * 555 * @param rightPage Is the page animated to the right page? 556 */ onAnimationToSideStarted(boolean rightPage, float translation, float vel)557 void onAnimationToSideStarted(boolean rightPage, float translation, float vel); 558 559 /** 560 * Notifies the callback the animation to a side page has ended. 561 */ onAnimationToSideEnded()562 void onAnimationToSideEnded(); 563 getMaxTranslationDistance()564 float getMaxTranslationDistance(); 565 onSwipingStarted(boolean rightIcon)566 void onSwipingStarted(boolean rightIcon); 567 onSwipingAborted()568 void onSwipingAborted(); 569 onIconClicked(boolean rightIcon)570 void onIconClicked(boolean rightIcon); 571 getLeftIcon()572 KeyguardAffordanceView getLeftIcon(); 573 getRightIcon()574 KeyguardAffordanceView getRightIcon(); 575 getLeftPreview()576 View getLeftPreview(); 577 getRightPreview()578 View getRightPreview(); 579 580 /** 581 * @return The factor the minimum swipe amount should be multiplied with. 582 */ getAffordanceFalsingFactor()583 float getAffordanceFalsingFactor(); 584 needsAntiFalsing()585 boolean needsAntiFalsing(); 586 } 587 } 588