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