Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2018 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 static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
     20 import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
     21 import static com.android.systemui.Interpolators.ALPHA_IN;
     22 import static com.android.systemui.Interpolators.ALPHA_OUT;
     23 import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
     24 import static com.android.systemui.OverviewProxyService.TAG_OPS;
     25 
     26 import android.animation.Animator;
     27 import android.animation.AnimatorListenerAdapter;
     28 import android.animation.AnimatorSet;
     29 import android.animation.ArgbEvaluator;
     30 import android.animation.ObjectAnimator;
     31 import android.animation.PropertyValuesHolder;
     32 import android.content.Context;
     33 import android.content.res.Resources;
     34 import android.graphics.Canvas;
     35 import android.graphics.Color;
     36 import android.graphics.Matrix;
     37 import android.graphics.Rect;
     38 import android.graphics.drawable.Drawable;
     39 import android.os.Handler;
     40 import android.os.RemoteException;
     41 import android.util.FloatProperty;
     42 import android.util.Log;
     43 import android.util.Slog;
     44 import android.view.MotionEvent;
     45 import android.view.View;
     46 import android.view.WindowManagerGlobal;
     47 import android.view.animation.DecelerateInterpolator;
     48 import android.view.animation.Interpolator;
     49 import android.support.annotation.DimenRes;
     50 import com.android.systemui.Dependency;
     51 import com.android.systemui.OverviewProxyService;
     52 import com.android.systemui.R;
     53 import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
     54 import com.android.systemui.shared.recents.IOverviewProxy;
     55 import com.android.systemui.shared.recents.utilities.Utilities;
     56 import com.android.systemui.shared.system.NavigationBarCompat;
     57 import com.android.internal.graphics.ColorUtils;
     58 
     59 /**
     60  * Class to detect gestures on the navigation bar and implement quick scrub.
     61  */
     62 public class QuickStepController implements GestureHelper {
     63 
     64     private static final String TAG = "QuickStepController";
     65     private static final int ANIM_IN_DURATION_MS = 150;
     66     private static final int ANIM_OUT_DURATION_MS = 134;
     67     private static final float TRACK_SCALE = 0.95f;
     68 
     69     private NavigationBarView mNavigationBarView;
     70 
     71     private boolean mQuickScrubActive;
     72     private boolean mAllowGestureDetection;
     73     private boolean mQuickStepStarted;
     74     private int mTouchDownX;
     75     private int mTouchDownY;
     76     private boolean mDragPositive;
     77     private boolean mIsVertical;
     78     private boolean mIsRTL;
     79     private float mTrackAlpha;
     80     private float mTrackScale = TRACK_SCALE;
     81     private int mLightTrackColor;
     82     private int mDarkTrackColor;
     83     private float mDarkIntensity;
     84     private AnimatorSet mTrackAnimator;
     85     private ButtonDispatcher mHitTarget;
     86     private View mCurrentNavigationBarView;
     87 
     88     private final Handler mHandler = new Handler();
     89     private final Rect mTrackRect = new Rect();
     90     private final Drawable mTrackDrawable;
     91     private final OverviewProxyService mOverviewEventSender;
     92     private final int mTrackThickness;
     93     private final int mTrackEndPadding;
     94     private final Context mContext;
     95     private final Matrix mTransformGlobalMatrix = new Matrix();
     96     private final Matrix mTransformLocalMatrix = new Matrix();
     97     private final ArgbEvaluator mTrackColorEvaluator = new ArgbEvaluator();
     98 
     99     private final FloatProperty<QuickStepController> mTrackAlphaProperty =
    100             new FloatProperty<QuickStepController>("TrackAlpha") {
    101         @Override
    102         public void setValue(QuickStepController controller, float alpha) {
    103             mTrackAlpha = alpha;
    104             mNavigationBarView.invalidate();
    105         }
    106 
    107         @Override
    108         public Float get(QuickStepController controller) {
    109             return mTrackAlpha;
    110         }
    111     };
    112 
    113     private final FloatProperty<QuickStepController> mTrackScaleProperty =
    114             new FloatProperty<QuickStepController>("TrackScale") {
    115         @Override
    116         public void setValue(QuickStepController controller, float scale) {
    117             mTrackScale = scale;
    118             mNavigationBarView.invalidate();
    119         }
    120 
    121         @Override
    122         public Float get(QuickStepController controller) {
    123             return mTrackScale;
    124         }
    125     };
    126 
    127     private final FloatProperty<QuickStepController> mNavBarAlphaProperty =
    128             new FloatProperty<QuickStepController>("NavBarAlpha") {
    129         @Override
    130         public void setValue(QuickStepController controller, float alpha) {
    131             if (mCurrentNavigationBarView != null) {
    132                 mCurrentNavigationBarView.setAlpha(alpha);
    133             }
    134         }
    135 
    136         @Override
    137         public Float get(QuickStepController controller) {
    138             if (mCurrentNavigationBarView != null) {
    139                 return mCurrentNavigationBarView.getAlpha();
    140             }
    141             return 1f;
    142         }
    143     };
    144 
    145     private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
    146         @Override
    147         public void onAnimationEnd(Animator animation) {
    148             resetQuickScrub();
    149         }
    150     };
    151 
    152     public QuickStepController(Context context) {
    153         final Resources res = context.getResources();
    154         mContext = context;
    155         mOverviewEventSender = Dependency.get(OverviewProxyService.class);
    156         mTrackThickness = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_thickness);
    157         mTrackEndPadding = res.getDimensionPixelSize(R.dimen.nav_quick_scrub_track_edge_padding);
    158         mTrackDrawable = context.getDrawable(R.drawable.qs_scrubber_track).mutate();
    159     }
    160 
    161     public void setComponents(NavigationBarView navigationBarView) {
    162         mNavigationBarView = navigationBarView;
    163     }
    164 
    165     /**
    166      * @return true if we want to intercept touch events for quick scrub and prevent proxying the
    167      *         event to the overview service.
    168      */
    169     @Override
    170     public boolean onInterceptTouchEvent(MotionEvent event) {
    171         return handleTouchEvent(event);
    172     }
    173 
    174     /**
    175      * @return true if we want to handle touch events for quick scrub or if down event (that will
    176      *         get consumed and ignored). No events will be proxied to the overview service.
    177      */
    178     @Override
    179     public boolean onTouchEvent(MotionEvent event) {
    180         // The same down event was just sent on intercept and therefore can be ignored here
    181         final boolean ignoreProxyDownEvent = event.getAction() == MotionEvent.ACTION_DOWN
    182                 && mOverviewEventSender.getProxy() != null;
    183         return ignoreProxyDownEvent || handleTouchEvent(event);
    184     }
    185 
    186     private boolean handleTouchEvent(MotionEvent event) {
    187         if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled()
    188                 && !mNavigationBarView.isQuickStepSwipeUpEnabled())) {
    189             return false;
    190         }
    191         mNavigationBarView.requestUnbufferedDispatch(event);
    192 
    193         int action = event.getActionMasked();
    194         switch (action) {
    195             case MotionEvent.ACTION_DOWN: {
    196                 int x = (int) event.getX();
    197                 int y = (int) event.getY();
    198 
    199                 // End any existing quickscrub animations before starting the new transition
    200                 if (mTrackAnimator != null) {
    201                     mTrackAnimator.end();
    202                     mTrackAnimator = null;
    203                 }
    204 
    205                 mCurrentNavigationBarView = mNavigationBarView.getCurrentView();
    206                 mHitTarget = mNavigationBarView.getButtonAtPosition(x, y);
    207                 if (mHitTarget != null) {
    208                     // Pre-emptively delay the touch feedback for the button that we just touched
    209                     mHitTarget.setDelayTouchFeedback(true);
    210                 }
    211                 mTouchDownX = x;
    212                 mTouchDownY = y;
    213                 mTransformGlobalMatrix.set(Matrix.IDENTITY_MATRIX);
    214                 mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX);
    215                 mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix);
    216                 mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix);
    217                 mQuickStepStarted = false;
    218                 mAllowGestureDetection = true;
    219                 break;
    220             }
    221             case MotionEvent.ACTION_MOVE: {
    222                 if (mQuickStepStarted || !mAllowGestureDetection){
    223                     break;
    224                 }
    225                 int x = (int) event.getX();
    226                 int y = (int) event.getY();
    227                 int xDiff = Math.abs(x - mTouchDownX);
    228                 int yDiff = Math.abs(y - mTouchDownY);
    229 
    230                 boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop;
    231                 int pos, touchDown, offset, trackSize;
    232 
    233                 if (mIsVertical) {
    234                     exceededScrubTouchSlop =
    235                             yDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && yDiff > xDiff;
    236                     exceededSwipeUpTouchSlop =
    237                             xDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && xDiff > yDiff;
    238                     pos = y;
    239                     touchDown = mTouchDownY;
    240                     offset = pos - mTrackRect.top;
    241                     trackSize = mTrackRect.height();
    242                 } else {
    243                     exceededScrubTouchSlop =
    244                             xDiff > NavigationBarCompat.getQuickScrubTouchSlopPx() && xDiff > yDiff;
    245                     exceededSwipeUpTouchSlop =
    246                             yDiff > NavigationBarCompat.getQuickStepTouchSlopPx() && yDiff > xDiff;
    247                     pos = x;
    248                     touchDown = mTouchDownX;
    249                     offset = pos - mTrackRect.left;
    250                     trackSize = mTrackRect.width();
    251                 }
    252                 // Decide to start quickstep if dragging away from the navigation bar, otherwise in
    253                 // the parallel direction, decide to start quickscrub. Only one may run.
    254                 if (!mQuickScrubActive && exceededSwipeUpTouchSlop) {
    255                     if (mNavigationBarView.isQuickStepSwipeUpEnabled()) {
    256                         startQuickStep(event);
    257                     }
    258                     break;
    259                 }
    260 
    261                 // Do not handle quick scrub if disabled
    262                 if (!mNavigationBarView.isQuickScrubEnabled()) {
    263                     break;
    264                 }
    265 
    266                 if (!mDragPositive) {
    267                     offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
    268                 }
    269 
    270                 final boolean allowDrag = !mDragPositive
    271                         ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
    272                 float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
    273                 if (allowDrag) {
    274                     // Passing the drag slop then touch slop will start quick step
    275                     if (!mQuickScrubActive && exceededScrubTouchSlop) {
    276                         startQuickScrub();
    277                     }
    278                 }
    279 
    280                 if (mQuickScrubActive && (mDragPositive && offset >= 0
    281                         || !mDragPositive && offset <= 0)) {
    282                     try {
    283                         mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
    284                         if (DEBUG_OVERVIEW_PROXY) {
    285                             Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
    286                         }
    287                     } catch (RemoteException e) {
    288                         Log.e(TAG, "Failed to send progress of quick scrub.", e);
    289                     }
    290                 }
    291                 break;
    292             }
    293             case MotionEvent.ACTION_CANCEL:
    294             case MotionEvent.ACTION_UP:
    295                 endQuickScrub(true /* animate */);
    296                 break;
    297         }
    298 
    299         // Proxy motion events to launcher if not handled by quick scrub
    300         // Proxy motion events up/cancel that would be sent after long press on any nav button
    301         if (!mQuickScrubActive && (mAllowGestureDetection || action == MotionEvent.ACTION_CANCEL
    302                 || action == MotionEvent.ACTION_UP)) {
    303             proxyMotionEvents(event);
    304         }
    305         return mQuickScrubActive || mQuickStepStarted;
    306     }
    307 
    308     @Override
    309     public void onDraw(Canvas canvas) {
    310         if (!mNavigationBarView.isQuickScrubEnabled()) {
    311             return;
    312         }
    313         int color = (int) mTrackColorEvaluator.evaluate(mDarkIntensity, mLightTrackColor,
    314                 mDarkTrackColor);
    315         int colorAlpha = ColorUtils.setAlphaComponent(color,
    316                 (int) (Color.alpha(color) * mTrackAlpha));
    317         mTrackDrawable.setTint(colorAlpha);
    318 
    319         // Scale the track, but apply the inverse scale from the nav bar
    320         canvas.save();
    321         canvas.scale(mTrackScale / mNavigationBarView.getScaleX(),
    322                 1f / mNavigationBarView.getScaleY(),
    323                 mTrackRect.centerX(), mTrackRect.centerY());
    324         mTrackDrawable.draw(canvas);
    325         canvas.restore();
    326     }
    327 
    328     @Override
    329     public void onLayout(boolean changed, int left, int top, int right, int bottom) {
    330         final int paddingLeft = mNavigationBarView.getPaddingLeft();
    331         final int paddingTop = mNavigationBarView.getPaddingTop();
    332         final int paddingRight = mNavigationBarView.getPaddingRight();
    333         final int paddingBottom = mNavigationBarView.getPaddingBottom();
    334         final int width = (right - left) - paddingRight - paddingLeft;
    335         final int height = (bottom - top) - paddingBottom - paddingTop;
    336         final int x1, x2, y1, y2;
    337         if (mIsVertical) {
    338             x1 = (width - mTrackThickness) / 2 + paddingLeft;
    339             x2 = x1 + mTrackThickness;
    340             y1 = paddingTop + mTrackEndPadding;
    341             y2 = y1 + height - 2 * mTrackEndPadding;
    342         } else {
    343             y1 = (height - mTrackThickness) / 2 + paddingTop;
    344             y2 = y1 + mTrackThickness;
    345             x1 = mNavigationBarView.getPaddingStart() + mTrackEndPadding;
    346             x2 = x1 + width - 2 * mTrackEndPadding;
    347         }
    348         mTrackRect.set(x1, y1, x2, y2);
    349         mTrackDrawable.setBounds(mTrackRect);
    350     }
    351 
    352     @Override
    353     public void onDarkIntensityChange(float intensity) {
    354         mDarkIntensity = intensity;
    355         mNavigationBarView.invalidate();
    356     }
    357 
    358     @Override
    359     public void setBarState(boolean isVertical, boolean isRTL) {
    360         final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL);
    361         if (changed) {
    362             // End quickscrub if the state changes mid-transition
    363             endQuickScrub(false /* animate */);
    364         }
    365         mIsVertical = isVertical;
    366         mIsRTL = isRTL;
    367         try {
    368             int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
    369             mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
    370             if (isRTL) {
    371                 mDragPositive = !mDragPositive;
    372             }
    373         } catch (RemoteException e) {
    374             Slog.e(TAG, "Failed to get nav bar position.", e);
    375         }
    376     }
    377 
    378     @Override
    379     public void onNavigationButtonLongPress(View v) {
    380         mAllowGestureDetection = false;
    381         mHandler.removeCallbacksAndMessages(null);
    382     }
    383 
    384     private void startQuickStep(MotionEvent event) {
    385         mQuickStepStarted = true;
    386         event.transform(mTransformGlobalMatrix);
    387         try {
    388             mOverviewEventSender.getProxy().onQuickStep(event);
    389             if (DEBUG_OVERVIEW_PROXY) {
    390                 Log.d(TAG_OPS, "Quick Step Start");
    391             }
    392         } catch (RemoteException e) {
    393             Log.e(TAG, "Failed to send quick step started.", e);
    394         } finally {
    395             event.transform(mTransformLocalMatrix);
    396         }
    397         mOverviewEventSender.notifyQuickStepStarted();
    398         mHandler.removeCallbacksAndMessages(null);
    399 
    400         if (mHitTarget != null) {
    401             mHitTarget.abortCurrentGesture();
    402         }
    403 
    404         if (mQuickScrubActive) {
    405             animateEnd();
    406         }
    407     }
    408 
    409     private void startQuickScrub() {
    410         if (!mQuickScrubActive) {
    411             mQuickScrubActive = true;
    412             mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
    413             mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
    414 
    415             ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
    416                     PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 1f),
    417                     PropertyValuesHolder.ofFloat(mTrackScaleProperty, 1f));
    418             trackAnimator.setInterpolator(ALPHA_IN);
    419             trackAnimator.setDuration(ANIM_IN_DURATION_MS);
    420             ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 0f);
    421             navBarAnimator.setInterpolator(ALPHA_OUT);
    422             navBarAnimator.setDuration(ANIM_OUT_DURATION_MS);
    423             mTrackAnimator = new AnimatorSet();
    424             mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
    425             mTrackAnimator.start();
    426 
    427             try {
    428                 mOverviewEventSender.getProxy().onQuickScrubStart();
    429                 if (DEBUG_OVERVIEW_PROXY) {
    430                     Log.d(TAG_OPS, "Quick Scrub Start");
    431                 }
    432             } catch (RemoteException e) {
    433                 Log.e(TAG, "Failed to send start of quick scrub.", e);
    434             }
    435             mOverviewEventSender.notifyQuickScrubStarted();
    436 
    437             if (mHitTarget != null) {
    438                 mHitTarget.abortCurrentGesture();
    439             }
    440         }
    441     }
    442 
    443     private void endQuickScrub(boolean animate) {
    444         if (mQuickScrubActive) {
    445             animateEnd();
    446             try {
    447                 mOverviewEventSender.getProxy().onQuickScrubEnd();
    448                 if (DEBUG_OVERVIEW_PROXY) {
    449                     Log.d(TAG_OPS, "Quick Scrub End");
    450                 }
    451             } catch (RemoteException e) {
    452                 Log.e(TAG, "Failed to send end of quick scrub.", e);
    453             }
    454         }
    455         if (!animate) {
    456             if (mTrackAnimator != null) {
    457                 mTrackAnimator.end();
    458                 mTrackAnimator = null;
    459             }
    460         }
    461     }
    462 
    463     private void animateEnd() {
    464         if (mTrackAnimator != null) {
    465             mTrackAnimator.cancel();
    466         }
    467 
    468         ObjectAnimator trackAnimator = ObjectAnimator.ofPropertyValuesHolder(this,
    469                 PropertyValuesHolder.ofFloat(mTrackAlphaProperty, 0f),
    470                 PropertyValuesHolder.ofFloat(mTrackScaleProperty, TRACK_SCALE));
    471         trackAnimator.setInterpolator(ALPHA_OUT);
    472         trackAnimator.setDuration(ANIM_OUT_DURATION_MS);
    473         ObjectAnimator navBarAnimator = ObjectAnimator.ofFloat(this, mNavBarAlphaProperty, 1f);
    474         navBarAnimator.setInterpolator(ALPHA_IN);
    475         navBarAnimator.setDuration(ANIM_IN_DURATION_MS);
    476         mTrackAnimator = new AnimatorSet();
    477         mTrackAnimator.playTogether(trackAnimator, navBarAnimator);
    478         mTrackAnimator.addListener(mQuickScrubEndListener);
    479         mTrackAnimator.start();
    480     }
    481 
    482     private void resetQuickScrub() {
    483         mQuickScrubActive = false;
    484         mAllowGestureDetection = false;
    485         mCurrentNavigationBarView = null;
    486     }
    487 
    488     private boolean proxyMotionEvents(MotionEvent event) {
    489         final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
    490         event.transform(mTransformGlobalMatrix);
    491         try {
    492             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
    493                 overviewProxy.onPreMotionEvent(mNavigationBarView.getDownHitTarget());
    494             }
    495             overviewProxy.onMotionEvent(event);
    496             if (DEBUG_OVERVIEW_PROXY) {
    497                 Log.d(TAG_OPS, "Send MotionEvent: " + event.toString());
    498             }
    499             return true;
    500         } catch (RemoteException e) {
    501             Log.e(TAG, "Callback failed", e);
    502         } finally {
    503             event.transform(mTransformLocalMatrix);
    504         }
    505         return false;
    506     }
    507 }
    508