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 import android.view.animation.AnimationUtils; 28 import android.view.animation.Interpolator; 29 import com.android.systemui.ExpandHelper; 30 import com.android.systemui.Gefingerpoken; 31 import com.android.systemui.R; 32 33 /** 34 * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand 35 * the notification where the drag started. 36 */ 37 public class DragDownHelper implements Gefingerpoken { 38 39 private static final float RUBBERBAND_FACTOR_EXPANDABLE = 0.5f; 40 private static final float RUBBERBAND_FACTOR_STATIC = 0.15f; 41 42 private static final int SPRING_BACK_ANIMATION_LENGTH_MS = 375; 43 44 private int mMinDragDistance; 45 private ExpandHelper.Callback mCallback; 46 private float mInitialTouchX; 47 private float mInitialTouchY; 48 private boolean mDraggingDown; 49 private float mTouchSlop; 50 private DragDownCallback mDragDownCallback; 51 private View mHost; 52 private final int[] mTemp2 = new int[2]; 53 private boolean mDraggedFarEnough; 54 private ExpandableView mStartingChild; 55 private Interpolator mInterpolator; 56 private float mLastHeight; 57 DragDownHelper(Context context, View host, ExpandHelper.Callback callback, DragDownCallback dragDownCallback)58 public DragDownHelper(Context context, View host, ExpandHelper.Callback callback, 59 DragDownCallback dragDownCallback) { 60 mMinDragDistance = context.getResources().getDimensionPixelSize( 61 R.dimen.keyguard_drag_down_min_distance); 62 mInterpolator = 63 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 64 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 65 mCallback = callback; 66 mDragDownCallback = dragDownCallback; 67 mHost = host; 68 } 69 70 @Override onInterceptTouchEvent(MotionEvent event)71 public boolean onInterceptTouchEvent(MotionEvent event) { 72 final float x = event.getX(); 73 final float y = event.getY(); 74 75 switch (event.getActionMasked()) { 76 case MotionEvent.ACTION_DOWN: 77 mDraggedFarEnough = false; 78 mDraggingDown = false; 79 mStartingChild = null; 80 mInitialTouchY = y; 81 mInitialTouchX = x; 82 break; 83 84 case MotionEvent.ACTION_MOVE: 85 final float h = y - mInitialTouchY; 86 if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) { 87 mDraggingDown = true; 88 captureStartingChild(mInitialTouchX, mInitialTouchY); 89 mInitialTouchY = y; 90 mInitialTouchX = x; 91 mDragDownCallback.onTouchSlopExceeded(); 92 return true; 93 } 94 break; 95 } 96 return false; 97 } 98 99 @Override onTouchEvent(MotionEvent event)100 public boolean onTouchEvent(MotionEvent event) { 101 if (!mDraggingDown) { 102 return false; 103 } 104 final float x = event.getX(); 105 final float y = event.getY(); 106 107 switch (event.getActionMasked()) { 108 case MotionEvent.ACTION_MOVE: 109 mLastHeight = y - mInitialTouchY; 110 captureStartingChild(mInitialTouchX, mInitialTouchY); 111 if (mStartingChild != null) { 112 handleExpansion(mLastHeight, mStartingChild); 113 } else { 114 mDragDownCallback.setEmptyDragAmount(mLastHeight); 115 } 116 if (mLastHeight > mMinDragDistance) { 117 if (!mDraggedFarEnough) { 118 mDraggedFarEnough = true; 119 mDragDownCallback.onThresholdReached(); 120 } 121 } else { 122 if (mDraggedFarEnough) { 123 mDraggedFarEnough = false; 124 mDragDownCallback.onDragDownReset(); 125 } 126 } 127 return true; 128 case MotionEvent.ACTION_UP: 129 if (mDraggedFarEnough && mDragDownCallback.onDraggedDown(mStartingChild, 130 (int) (y - mInitialTouchY))) { 131 if (mStartingChild == null) { 132 mDragDownCallback.setEmptyDragAmount(0f); 133 } 134 mDraggingDown = false; 135 } else { 136 stopDragging(); 137 return false; 138 } 139 break; 140 case MotionEvent.ACTION_CANCEL: 141 stopDragging(); 142 return false; 143 } 144 return false; 145 } 146 captureStartingChild(float x, float y)147 private void captureStartingChild(float x, float y) { 148 if (mStartingChild == null) { 149 mStartingChild = findView(x, y); 150 if (mStartingChild != null) { 151 mCallback.setUserLockedChild(mStartingChild, true); 152 } 153 } 154 } 155 handleExpansion(float heightDelta, ExpandableView child)156 private void handleExpansion(float heightDelta, ExpandableView child) { 157 if (heightDelta < 0) { 158 heightDelta = 0; 159 } 160 boolean expandable = child.isContentExpandable(); 161 float rubberbandFactor = expandable 162 ? RUBBERBAND_FACTOR_EXPANDABLE 163 : RUBBERBAND_FACTOR_STATIC; 164 float rubberband = heightDelta * rubberbandFactor; 165 if (expandable && (rubberband + child.getMinHeight()) > child.getMaxContentHeight()) { 166 float overshoot = (rubberband + child.getMinHeight()) - child.getMaxContentHeight(); 167 overshoot *= (1 - RUBBERBAND_FACTOR_STATIC); 168 rubberband -= overshoot; 169 } 170 child.setContentHeight((int) (child.getMinHeight() + rubberband)); 171 } 172 cancelExpansion(final ExpandableView child)173 private void cancelExpansion(final ExpandableView child) { 174 if (child.getContentHeight() == child.getMinHeight()) { 175 return; 176 } 177 ObjectAnimator anim = ObjectAnimator.ofInt(child, "contentHeight", 178 child.getContentHeight(), child.getMinHeight()); 179 anim.setInterpolator(mInterpolator); 180 anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS); 181 anim.addListener(new AnimatorListenerAdapter() { 182 @Override 183 public void onAnimationEnd(Animator animation) { 184 mCallback.setUserLockedChild(child, false); 185 } 186 }); 187 anim.start(); 188 } 189 cancelExpansion()190 private void cancelExpansion() { 191 ValueAnimator anim = ValueAnimator.ofFloat(mLastHeight, 0); 192 anim.setInterpolator(mInterpolator); 193 anim.setDuration(SPRING_BACK_ANIMATION_LENGTH_MS); 194 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 195 @Override 196 public void onAnimationUpdate(ValueAnimator animation) { 197 mDragDownCallback.setEmptyDragAmount((Float) animation.getAnimatedValue()); 198 } 199 }); 200 anim.start(); 201 } 202 stopDragging()203 private void stopDragging() { 204 if (mStartingChild != null) { 205 cancelExpansion(mStartingChild); 206 } else { 207 cancelExpansion(); 208 } 209 mDraggingDown = false; 210 mDragDownCallback.onDragDownReset(); 211 } 212 findView(float x, float y)213 private ExpandableView findView(float x, float y) { 214 mHost.getLocationOnScreen(mTemp2); 215 x += mTemp2[0]; 216 y += mTemp2[1]; 217 return mCallback.getChildAtRawPosition(x, y); 218 } 219 220 public interface DragDownCallback { 221 222 /** 223 * @return true if the interaction is accepted, false if it should be cancelled 224 */ onDraggedDown(View startingChild, int dragLengthY)225 boolean onDraggedDown(View startingChild, int dragLengthY); onDragDownReset()226 void onDragDownReset(); onThresholdReached()227 void onThresholdReached(); onTouchSlopExceeded()228 void onTouchSlopExceeded(); setEmptyDragAmount(float amount)229 void setEmptyDragAmount(float amount); 230 } 231 } 232