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; 18 19 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ObjectAnimator; 24 import android.animation.ValueAnimator; 25 import android.content.Context; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewConfiguration; 29 30 import com.android.systemui.ExpandHelper; 31 import com.android.systemui.Gefingerpoken; 32 import com.android.systemui.Interpolators; 33 import com.android.systemui.R; 34 import com.android.systemui.plugins.FalsingManager; 35 import com.android.systemui.statusbar.notification.row.ExpandableView; 36 37 /** 38 * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand 39 * the notification where the drag started. 40 */ 41 public class DragDownHelper implements Gefingerpoken { 42 43 private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f; 44 private static final float RUBBERBAND_FACTOR_STATIC = 0.15f; 45 46 private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375; 47 48 private int mMinDragDistance; 49 private ExpandHelper.Callback mCallback; 50 private float mInitialTouchX; 51 private float mInitialTouchY; 52 private boolean mDraggingDown; 53 private final float mTouchSlop; 54 private final float mSlopMultiplier; 55 private DragDownCallback mDragDownCallback; 56 private View mHost; 57 private final int[] mTemp2 = new int[2]; 58 private boolean mDraggedFarEnough; 59 private ExpandableView mStartingChild; 60 private float mLastHeight; 61 private FalsingManager mFalsingManager; 62 DragDownHelper(Context context, View host, ExpandHelper.Callback callback, DragDownCallback dragDownCallback, FalsingManager falsingManager)63 public DragDownHelper(Context context, View host, ExpandHelper.Callback callback, 64 DragDownCallback dragDownCallback, 65 FalsingManager falsingManager) { 66 mMinDragDistance = context.getResources().getDimensionPixelSize( 67 R.dimen.keyguard_drag_down_min_distance); 68 final ViewConfiguration configuration = ViewConfiguration.get(context); 69 mTouchSlop = configuration.getScaledTouchSlop(); 70 mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); 71 mCallback = callback; 72 mDragDownCallback = dragDownCallback; 73 mHost = host; 74 mFalsingManager = falsingManager; 75 } 76 77 @Override onInterceptTouchEvent(MotionEvent event)78 public boolean onInterceptTouchEvent(MotionEvent event) { 79 final float x = event.getX(); 80 final float y = event.getY(); 81 82 switch (event.getActionMasked()) { 83 case MotionEvent.ACTION_DOWN: 84 mDraggedFarEnough = false; 85 mDraggingDown = false; 86 mStartingChild = null; 87 mInitialTouchY = y; 88 mInitialTouchX = x; 89 break; 90 91 case MotionEvent.ACTION_MOVE: 92 final float h = y - mInitialTouchY; 93 // Adjust the touch slop if another gesture may be being performed. 94 final float touchSlop = 95 event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE 96 ? mTouchSlop * mSlopMultiplier 97 : mTouchSlop; 98 if (h > touchSlop && h > Math.abs(x - mInitialTouchX)) { 99 mFalsingManager.onNotificatonStartDraggingDown(); 100 mDraggingDown = true; 101 captureStartingChild(mInitialTouchX, mInitialTouchY); 102 mInitialTouchY = y; 103 mInitialTouchX = x; 104 mDragDownCallback.onTouchSlopExceeded(); 105 return mStartingChild != null || mDragDownCallback.isDragDownAnywhereEnabled(); 106 } 107 break; 108 } 109 return false; 110 } 111 112 @Override onTouchEvent(MotionEvent event)113 public boolean onTouchEvent(MotionEvent event) { 114 if (!mDraggingDown) { 115 return false; 116 } 117 final float x = event.getX(); 118 final float y = event.getY(); 119 120 switch (event.getActionMasked()) { 121 case MotionEvent.ACTION_MOVE: 122 mLastHeight = y - mInitialTouchY; 123 captureStartingChild(mInitialTouchX, mInitialTouchY); 124 if (mStartingChild != null) { 125 handleExpansion(mLastHeight, mStartingChild); 126 } else { 127 mDragDownCallback.setEmptyDragAmount(mLastHeight); 128 } 129 if (mLastHeight > mMinDragDistance) { 130 if (!mDraggedFarEnough) { 131 mDraggedFarEnough = true; 132 mDragDownCallback.onCrossedThreshold(true); 133 } 134 } else { 135 if (mDraggedFarEnough) { 136 mDraggedFarEnough = false; 137 mDragDownCallback.onCrossedThreshold(false); 138 } 139 } 140 return true; 141 case MotionEvent.ACTION_UP: 142 if (!mFalsingManager.isUnlockingDisabled() && !isFalseTouch() 143 && mDragDownCallback.onDraggedDown(mStartingChild, 144 (int) (y - mInitialTouchY))) { 145 if (mStartingChild == null) { 146 cancelExpansion(); 147 } else { 148 mCallback.setUserLockedChild(mStartingChild, false); 149 mStartingChild = null; 150 } 151 mDraggingDown = false; 152 } else { 153 stopDragging(); 154 return false; 155 } 156 break; 157 case MotionEvent.ACTION_CANCEL: 158 stopDragging(); 159 return false; 160 } 161 return false; 162 } 163 isFalseTouch()164 private boolean isFalseTouch() { 165 if (!mDragDownCallback.isFalsingCheckNeeded()) { 166 return false; 167 } 168 return mFalsingManager.isFalseTouch(NOTIFICATION_DRAG_DOWN) || !mDraggedFarEnough; 169 } 170 captureStartingChild(float x, float y)171 private void captureStartingChild(float x, float y) { 172 if (mStartingChild == null) { 173 mStartingChild = findView(x, y); 174 if (mStartingChild != null) { 175 if (mDragDownCallback.isDragDownEnabledForView(mStartingChild)) { 176 mCallback.setUserLockedChild(mStartingChild, true); 177 } else { 178 mStartingChild = null; 179 } 180 } 181 } 182 } 183 handleExpansion(float heightDelta, ExpandableView child)184 private void handleExpansion(float heightDelta, ExpandableView child) { 185 if (heightDelta < 0) { 186 heightDelta = 0; 187 } 188 boolean expandable = child.isContentExpandable(); 189 float rubberbandFactor = expandable 190 ? RUBBERBAND_FACTOR_EXPANDABLE 191 : RUBBERBAND_FACTOR_STATIC; 192 float rubberband = heightDelta * rubberbandFactor; 193 if (expandable 194 && (rubberband + child.getCollapsedHeight()) > child.getMaxContentHeight()) { 195 float overshoot = 196 (rubberband + child.getCollapsedHeight()) - child.getMaxContentHeight(); 197 overshoot *= (1 - RUBBERBAND_FACTOR_STATIC); 198 rubberband -= overshoot; 199 } 200 child.setActualHeight((int) (child.getCollapsedHeight() + rubberband)); 201 } 202 cancelExpansion(final ExpandableView child)203 private void cancelExpansion(final ExpandableView child) { 204 if (child.getActualHeight() == child.getCollapsedHeight()) { 205 mCallback.setUserLockedChild(child, false); 206 return; 207 } 208 ObjectAnimator anim = ObjectAnimator.ofInt(child, "actualHeight", 209 child.getActualHeight(), child.getCollapsedHeight()); 210 anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 211 anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS); 212 anim.addListener(new AnimatorListenerAdapter() { 213 @Override 214 public void onAnimationEnd(Animator animation) { 215 mCallback.setUserLockedChild(child, false); 216 } 217 }); 218 anim.start(); 219 } 220 cancelExpansion()221 private void cancelExpansion() { 222 ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0); 223 anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 224 anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS); 225 anim.addUpdateListener(animation -> { 226 mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue()); 227 }); 228 anim.start(); 229 } 230 stopDragging()231 private void stopDragging() { 232 mFalsingManager.onNotificatonStopDraggingDown(); 233 if (mStartingChild != null) { 234 cancelExpansion(mStartingChild); 235 mStartingChild = null; 236 } else { 237 cancelExpansion(); 238 } 239 mDraggingDown = false; 240 mDragDownCallback.onDragDownReset(); 241 } 242 findView(float x, float y)243 private ExpandableView findView(float x, float y) { 244 mHost.getLocationOnScreen(mTemp2); 245 x += mTemp2[0]; 246 y += mTemp2[1]; 247 return mCallback.getChildAtRawPosition(x, y); 248 } 249 isDraggingDown()250 public boolean isDraggingDown() { 251 return mDraggingDown; 252 } 253 isDragDownEnabled()254 public boolean isDragDownEnabled() { 255 return mDragDownCallback.isDragDownEnabledForView(null); 256 } 257 258 public interface DragDownCallback { 259 260 /** 261 * @return true if the interaction is accepted, false if it should be cancelled 262 */ onDraggedDown(View startingChild, int dragLengthY)263 boolean onDraggedDown(View startingChild, int dragLengthY); onDragDownReset()264 void onDragDownReset(); 265 266 /** 267 * The user has dragged either above or below the threshold 268 * @param above whether he dragged above it 269 */ onCrossedThreshold(boolean above)270 void onCrossedThreshold(boolean above); onTouchSlopExceeded()271 void onTouchSlopExceeded(); setEmptyDragAmount(float amount)272 void setEmptyDragAmount(float amount); isFalsingCheckNeeded()273 boolean isFalsingCheckNeeded(); 274 275 /** 276 * Is dragging down enabled on a given view 277 * @param view The view to check or {@code null} to check if it's enabled at all 278 */ isDragDownEnabledForView(ExpandableView view)279 boolean isDragDownEnabledForView(ExpandableView view); 280 281 /** 282 * @return if drag down is enabled anywhere, not just on selected views. 283 */ isDragDownAnywhereEnabled()284 boolean isDragDownAnywhereEnabled(); 285 } 286 } 287