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