1 /* 2 * Copyright 2018 Google Inc. 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.car.notification; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.view.MotionEvent; 22 import android.view.VelocityTracker; 23 import android.view.View; 24 25 /** 26 * OnTouchListener that enables swipe-to-dismiss gesture on heads-up notifications. 27 */ 28 class HeadsUpNotificationOnTouchListener implements View.OnTouchListener { 29 /** 30 * Minimum velocity to initiate a fling, as measured in pixels per second. 31 */ 32 private static final int MINIMUM_FLING_VELOCITY = 2000; 33 34 /** 35 * Distance a touch can wander before we think the user is scrolling in pixels. 36 */ 37 private static final int TOUCH_SLOP = 20; 38 39 /** 40 * The proportion which view has to be swiped before it dismisses. 41 */ 42 private static final float THRESHOLD = 0.3f; 43 44 /** 45 * The unit of velocity in milliseconds. A value of 1 means "pixels per millisecond", 46 * 1000 means "pixels per 1000 milliseconds (1 second)". 47 */ 48 private static final int VELOCITY_UNITS = 1000; 49 50 private final View mView; 51 private final DismissCallbacks mCallbacks; 52 53 private VelocityTracker mVelocityTracker; 54 private float mDownX; 55 private boolean mSwiping; 56 private int mSwipingSlop; 57 private float mTranslationX; 58 private boolean mDismissOnSwipe = true; 59 60 /** 61 * The callback indicating the supplied view has been dismissed. 62 */ 63 interface DismissCallbacks { onDismiss()64 void onDismiss(); 65 } 66 HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe, DismissCallbacks callbacks)67 HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe, 68 DismissCallbacks callbacks) { 69 mView = view; 70 mCallbacks = callbacks; 71 mDismissOnSwipe = dismissOnSwipe; 72 } 73 74 @Override onTouch(View view, MotionEvent motionEvent)75 public boolean onTouch(View view, MotionEvent motionEvent) { 76 motionEvent.offsetLocation(mTranslationX, /* deltaY= */ 0); 77 int viewWidth = mView.getWidth(); 78 79 switch (motionEvent.getActionMasked()) { 80 case MotionEvent.ACTION_DOWN: { 81 mDownX = motionEvent.getRawX(); 82 mVelocityTracker = VelocityTracker.obtain(); 83 mVelocityTracker.addMovement(motionEvent); 84 return false; 85 } 86 87 case MotionEvent.ACTION_UP: { 88 if (mVelocityTracker == null) { 89 return false; 90 } 91 92 float deltaX = motionEvent.getRawX() - mDownX; 93 mVelocityTracker.addMovement(motionEvent); 94 mVelocityTracker.computeCurrentVelocity(VELOCITY_UNITS); 95 float velocityX = mVelocityTracker.getXVelocity(); 96 float absVelocityX = Math.abs(velocityX); 97 float absVelocityY = Math.abs(mVelocityTracker.getYVelocity()); 98 boolean dismiss = false; 99 boolean dismissRight = false; 100 if (Math.abs(deltaX) > viewWidth * THRESHOLD) { 101 // dismiss when the movement is more than the defined threshold. 102 dismiss = true; 103 dismissRight = deltaX > 0; 104 } else if (MINIMUM_FLING_VELOCITY <= absVelocityX 105 && absVelocityY < absVelocityX 106 && mSwiping) { 107 // dismiss when the velocity is more than the defined threshold. 108 // dismiss only if flinging in the same direction as dragging. 109 dismiss = (velocityX < 0) == (deltaX < 0); 110 dismissRight = mVelocityTracker.getXVelocity() > 0; 111 } 112 if (dismiss && mDismissOnSwipe) { 113 mCallbacks.onDismiss(); 114 mView.animate() 115 .translationX(dismissRight ? viewWidth : -viewWidth) 116 .alpha(0) 117 .setListener(new AnimatorListenerAdapter() { 118 @Override 119 public void onAnimationEnd(Animator animation) { 120 mView.setAlpha(1f); 121 mView.setTranslationX(0); 122 } 123 }); 124 } else if (mSwiping) { 125 animateToCenter(); 126 } 127 reset(); 128 break; 129 } 130 131 case MotionEvent.ACTION_CANCEL: { 132 if (mVelocityTracker == null) { 133 return false; 134 } 135 animateToCenter(); 136 reset(); 137 return false; 138 } 139 140 case MotionEvent.ACTION_MOVE: { 141 if (mVelocityTracker == null) { 142 return false; 143 } 144 145 mVelocityTracker.addMovement(motionEvent); 146 float deltaX = motionEvent.getRawX() - mDownX; 147 if (Math.abs(deltaX) > TOUCH_SLOP) { 148 mSwiping = true; 149 mSwipingSlop = (deltaX > 0 ? TOUCH_SLOP : -TOUCH_SLOP); 150 mView.getParent().requestDisallowInterceptTouchEvent(true); 151 152 // prevent onClickListener being triggered when moving. 153 MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); 154 cancelEvent.setAction(MotionEvent.ACTION_CANCEL | 155 (motionEvent.getActionIndex() << 156 MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 157 mView.onTouchEvent(cancelEvent); 158 cancelEvent.recycle(); 159 } 160 161 if (mSwiping) { 162 mTranslationX = deltaX; 163 mView.setTranslationX(deltaX - mSwipingSlop); 164 if (!mDismissOnSwipe) { 165 return true; 166 } 167 mView.setAlpha(Math.max(0f, Math.min(1f, 168 1f - 2f * Math.abs(deltaX) / viewWidth))); 169 return true; 170 } 171 } 172 173 default: { 174 return false; 175 } 176 } 177 return false; 178 } 179 animateToCenter()180 private void animateToCenter() { 181 mView.animate() 182 .translationX(0) 183 .alpha(1) 184 .setListener(null); 185 } 186 reset()187 private void reset() { 188 if (mVelocityTracker != null) { 189 mVelocityTracker.recycle(); 190 } 191 mVelocityTracker = null; 192 mTranslationX = 0; 193 mDownX = 0; 194 mSwiping = false; 195 } 196 } 197