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