2 * Copyright (C) 2012 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package com.android.systemui;
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ObjectAnimator;
23 import android.content.Context;
24 import android.util.Log;
25 import android.view.Gravity;
26 import android.view.HapticFeedbackConstants;
27 import android.view.MotionEvent;
28 import android.view.ScaleGestureDetector;
29 import android.view.ScaleGestureDetector.OnScaleGestureListener;
30 import android.view.VelocityTracker;
31 import android.view.View;
32 import android.view.ViewConfiguration;
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
36 import com.android.systemui.statusbar.notification.row.ExpandableView;
37 import com.android.systemui.statusbar.FlingAnimationUtils;
38 import com.android.systemui.statusbar.policy.ScrollAdapter;
40 public class ExpandHelper implements Gefingerpoken {
41 public interface Callback {
42 ExpandableView getChildAtRawPosition(float x, float y);
43 ExpandableView getChildAtPosition(float x, float y);
44 boolean canChildBeExpanded(View v);
45 void setUserExpandedChild(View v, boolean userExpanded);
46 void setUserLockedChild(View v, boolean userLocked);
47 void expansionStateChanged(boolean isExpanding);
48 int getMaxExpandHeight(ExpandableView view);
49 void setExpansionCancelled(View view);
52 private static final String TAG = "ExpandHelper";
53 protected static final boolean DEBUG = false;
54 protected static final boolean DEBUG_SCALE = false;
55 private static final float EXPAND_DURATION = 0.3f;
57 // Set to false to disable focus-based gestures (spread-finger vertical pull).
58 private static final boolean USE_DRAG = true;
59 // Set to false to disable scale-based gestures (both horizontal and vertical).
60 private static final boolean USE_SPAN = true;
61 // Both gestures types may be active at the same time.
62 // At least one gesture type should be active.
63 // A variant of the screwdriver gesture will emerge from either gesture type.
65 // amount of overstretch for maximum brightness expressed in U
66 // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
67 private static final float STRETCH_INTERVAL = 2f;
69 @SuppressWarnings("unused")
70 private Context mContext;
72 private boolean mExpanding;
73 private static final int NONE = 0;
74 private static final int BLINDS = 1<<0;
75 private static final int PULL = 1<<1;
76 private static final int STRETCH = 1<<2;
77 private int mExpansionStyle = NONE;
78 private boolean mWatchingForPull;
79 private boolean mHasPopped;
80 private View mEventSource;
81 private float mOldHeight;
82 private float mNaturalHeight;
83 private float mInitialTouchFocusY;
84 private float mInitialTouchX;
85 private float mInitialTouchY;
86 private float mInitialTouchSpan;
87 private float mLastFocusY;
88 private float mLastSpanY;
89 private int mTouchSlop;
90 private float mLastMotionY;
91 private float mPullGestureMinXSpan;
92 private Callback mCallback;
93 private ScaleGestureDetector mSGD;
94 private ViewScaler mScaler;
95 private ObjectAnimator mScaleAnimation;
96 private boolean mEnabled = true;
97 private ExpandableView mResizedView;
98 private float mCurrentHeight;
100 private int mSmallSize;
101 private int mLargeSize;
102 private float mMaximumStretch;
103 private boolean mOnlyMovements;
105 private int mGravity;
107 private ScrollAdapter mScrollAdapter;
108 private FlingAnimationUtils mFlingAnimationUtils;
109 private VelocityTracker mVelocityTracker;
111 private OnScaleGestureListener mScaleGestureListener
112 = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
114 public boolean onScaleBegin(ScaleGestureDetector detector) {
115 if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()");
117 if (!mOnlyMovements) {
118 startExpanding(mResizedView, STRETCH);
124 public boolean onScale(ScaleGestureDetector detector) {
125 if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView);
130 public void onScaleEnd(ScaleGestureDetector detector) {
135 ObjectAnimator getScaleAnimation() {
136 return mScaleAnimation;
139 private class ViewScaler {
140 ExpandableView mView;
142 public ViewScaler() {}
143 public void setView(ExpandableView v) {
146 public void setHeight(float h) {
147 if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
148 mView.setActualHeight((int) h);
151 public float getHeight() {
152 return mView.getActualHeight();
154 public int getNaturalHeight() {
155 return mCallback.getMaxExpandHeight(mView);
160 * Handle expansion gestures to expand and contract children of the callback.
162 * @param context application context
163 * @param callback the container that holds the items to be manipulated
164 * @param small the smallest allowable size for the manuipulated items.
165 * @param large the largest allowable size for the manuipulated items.
167 public ExpandHelper(Context context, Callback callback, int small, int large) {
169 mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
172 mCallback = callback;
173 mScaler = new ViewScaler();
174 mGravity = Gravity.TOP;
175 mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
176 mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
178 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
179 mTouchSlop = configuration.getScaledTouchSlop();
181 mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
182 mFlingAnimationUtils = new FlingAnimationUtils(context, EXPAND_DURATION);
186 void updateExpansion() {
187 if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()");
188 // are we scaling or dragging?
189 float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
190 span *= USE_SPAN ? 1f : 0f;
191 float drag = mSGD.getFocusY() - mInitialTouchFocusY;
192 drag *= USE_DRAG ? 1f : 0f;
193 drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
194 float pull = Math.abs(drag) + Math.abs(span) + 1f;
195 float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
196 float target = hand + mOldHeight;
197 float newHeight = clamp(target);
198 mScaler.setHeight(newHeight);
199 mLastFocusY = mSGD.getFocusY();
200 mLastSpanY = mSGD.getCurrentSpan();
203 private float clamp(float target) {
205 out = out < mSmallSize ? mSmallSize : out;
206 out = out > mNaturalHeight ? mNaturalHeight : out;
210 private ExpandableView findView(float x, float y) {
212 if (mEventSource != null) {
213 int[] location = new int[2];
214 mEventSource.getLocationOnScreen(location);
217 v = mCallback.getChildAtRawPosition(x, y);
219 v = mCallback.getChildAtPosition(x, y);
224 private boolean isInside(View v, float x, float y) {
225 if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")");
228 if (DEBUG) Log.d(TAG, "isinside null subject");
231 if (mEventSource != null) {
232 int[] location = new int[2];
233 mEventSource.getLocationOnScreen(location);
236 if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")");
238 int[] location = new int[2];
239 v.getLocationOnScreen(location);
242 if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")");
243 if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")");
244 boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
248 public void setEventSource(View eventSource) {
249 mEventSource = eventSource;
252 public void setGravity(int gravity) {
256 public void setScrollAdapter(ScrollAdapter adapter) {
257 mScrollAdapter = adapter;
261 public boolean onInterceptTouchEvent(MotionEvent ev) {
266 final int action = ev.getAction();
267 if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
268 " expanding=" + mExpanding +
269 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
270 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
271 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
272 // check for a spread-finger vertical pull gesture
273 mSGD.onTouchEvent(ev);
274 final int x = (int) mSGD.getFocusX();
275 final int y = (int) mSGD.getFocusY();
277 mInitialTouchFocusY = y;
278 mInitialTouchSpan = mSGD.getCurrentSpan();
279 mLastFocusY = mInitialTouchFocusY;
280 mLastSpanY = mInitialTouchSpan;
281 if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan);
284 mLastMotionY = ev.getRawY();
285 maybeRecycleVelocityTracker(ev);
288 if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
289 // we've begun Venetian blinds style expansion
292 switch (action & MotionEvent.ACTION_MASK) {
293 case MotionEvent.ACTION_MOVE: {
294 final float xspan = mSGD.getCurrentSpanX();
295 if (xspan > mPullGestureMinXSpan &&
296 xspan > mSGD.getCurrentSpanY() && !mExpanding) {
297 // detect a vertical pulling gesture with fingers somewhat separated
298 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
299 startExpanding(mResizedView, PULL);
300 mWatchingForPull = false;
302 if (mWatchingForPull) {
303 final float yDiff = ev.getRawY() - mInitialTouchY;
304 final float xDiff = ev.getRawX() - mInitialTouchX;
305 if (yDiff > mTouchSlop && yDiff > Math.abs(xDiff)) {
306 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
307 mWatchingForPull = false;
308 if (mResizedView != null && !isFullyExpanded(mResizedView)) {
309 if (startExpanding(mResizedView, BLINDS)) {
310 mLastMotionY = ev.getRawY();
311 mInitialTouchY = ev.getRawY();
320 case MotionEvent.ACTION_DOWN:
321 mWatchingForPull = mScrollAdapter != null &&
322 isInside(mScrollAdapter.getHostView(), x, y)
323 && mScrollAdapter.isScrolledToTop();
324 mResizedView = findView(x, y);
325 if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) {
327 mWatchingForPull = false;
329 mInitialTouchY = ev.getRawY();
330 mInitialTouchX = ev.getRawX();
333 case MotionEvent.ACTION_CANCEL:
334 case MotionEvent.ACTION_UP:
335 if (DEBUG) Log.d(TAG, "up/cancel");
336 finishExpanding(ev.getActionMasked() == MotionEvent.ACTION_CANCEL /* forceAbort */,
337 getCurrentVelocity());
341 mLastMotionY = ev.getRawY();
342 maybeRecycleVelocityTracker(ev);
347 private void trackVelocity(MotionEvent event) {
348 int action = event.getActionMasked();
350 case MotionEvent.ACTION_DOWN:
351 if (mVelocityTracker == null) {
352 mVelocityTracker = VelocityTracker.obtain();
354 mVelocityTracker.clear();
356 mVelocityTracker.addMovement(event);
358 case MotionEvent.ACTION_MOVE:
359 if (mVelocityTracker == null) {
360 mVelocityTracker = VelocityTracker.obtain();
362 mVelocityTracker.addMovement(event);
369 private void maybeRecycleVelocityTracker(MotionEvent event) {
370 if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL
371 || event.getActionMasked() == MotionEvent.ACTION_UP)) {
372 mVelocityTracker.recycle();
373 mVelocityTracker = null;
377 private float getCurrentVelocity() {
378 if (mVelocityTracker != null) {
379 mVelocityTracker.computeCurrentVelocity(1000);
380 return mVelocityTracker.getYVelocity();
386 public void setEnabled(boolean enable) {
390 private boolean isEnabled() {
394 private boolean isFullyExpanded(ExpandableView underFocus) {
395 return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight()
396 && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded());
400 public boolean onTouchEvent(MotionEvent ev) {
401 if (!isEnabled() && !mExpanding) {
402 // In case we're expanding we still want to finish the current motion.
406 final int action = ev.getActionMasked();
407 if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
408 " expanding=" + mExpanding +
409 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
410 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
411 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
413 mSGD.onTouchEvent(ev);
414 final int x = (int) mSGD.getFocusX();
415 final int y = (int) mSGD.getFocusY();
417 if (mOnlyMovements) {
418 mLastMotionY = ev.getRawY();
422 case MotionEvent.ACTION_DOWN:
423 mWatchingForPull = mScrollAdapter != null &&
424 isInside(mScrollAdapter.getHostView(), x, y);
425 mResizedView = findView(x, y);
426 mInitialTouchX = ev.getRawX();
427 mInitialTouchY = ev.getRawY();
429 case MotionEvent.ACTION_MOVE: {
430 if (mWatchingForPull) {
431 final float yDiff = ev.getRawY() - mInitialTouchY;
432 final float xDiff = ev.getRawX() - mInitialTouchX;
433 if (yDiff > mTouchSlop && yDiff > Math.abs(xDiff)) {
434 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
435 mWatchingForPull = false;
436 if (mResizedView != null && !isFullyExpanded(mResizedView)) {
437 if (startExpanding(mResizedView, BLINDS)) {
438 mInitialTouchY = ev.getRawY();
439 mLastMotionY = ev.getRawY();
445 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) {
446 final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight;
447 final float newHeight = clamp(rawHeight);
448 boolean isFinished = false;
449 boolean expanded = false;
450 if (rawHeight > mNaturalHeight) {
454 if (rawHeight < mSmallSize) {
460 if (mEventSource != null) {
461 mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
466 mScaler.setHeight(newHeight);
467 mLastMotionY = ev.getRawY();
469 mCallback.expansionStateChanged(false);
471 mCallback.expansionStateChanged(true);
478 // Gestural expansion is running
480 mLastMotionY = ev.getRawY();
487 case MotionEvent.ACTION_POINTER_UP:
488 case MotionEvent.ACTION_POINTER_DOWN:
489 if (DEBUG) Log.d(TAG, "pointer change");
490 mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
491 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
494 case MotionEvent.ACTION_UP:
495 case MotionEvent.ACTION_CANCEL:
496 if (DEBUG) Log.d(TAG, "up/cancel");
497 finishExpanding(!isEnabled() || ev.getActionMasked() == MotionEvent.ACTION_CANCEL,
498 getCurrentVelocity());
502 mLastMotionY = ev.getRawY();
503 maybeRecycleVelocityTracker(ev);
504 return mResizedView != null;
508 * @return True if the view is expandable, false otherwise.
511 boolean startExpanding(ExpandableView v, int expandType) {
512 if (!(v instanceof ExpandableNotificationRow)) {
515 mExpansionStyle = expandType;
516 if (mExpanding && v == mResizedView) {
520 mCallback.expansionStateChanged(true);
521 if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
522 mCallback.setUserLockedChild(v, true);
524 mOldHeight = mScaler.getHeight();
525 mCurrentHeight = mOldHeight;
526 boolean canBeExpanded = mCallback.canChildBeExpanded(v);
528 if (DEBUG) Log.d(TAG, "working on an expandable child");
529 mNaturalHeight = mScaler.getNaturalHeight();
530 mSmallSize = v.getCollapsedHeight();
532 if (DEBUG) Log.d(TAG, "working on a non-expandable child");
533 mNaturalHeight = mOldHeight;
535 if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
536 " mNaturalHeight: " + mNaturalHeight);
541 * Finish the current expand motion
542 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
544 * @param velocity the velocity this was expanded/ collapsed with
547 void finishExpanding(boolean forceAbort, float velocity) {
548 finishExpanding(forceAbort, velocity, true /* allowAnimation */);
552 * Finish the current expand motion
553 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old
555 * @param velocity the velocity this was expanded/ collapsed with
557 private void finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation) {
558 if (!mExpanding) return;
560 if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView);
562 float currentHeight = mScaler.getHeight();
563 final boolean wasClosed = (mOldHeight == mSmallSize);
567 nowExpanded = currentHeight > mOldHeight && velocity >= 0;
569 nowExpanded = currentHeight >= mOldHeight || velocity > 0;
571 nowExpanded |= mNaturalHeight == mSmallSize;
573 nowExpanded = !wasClosed;
575 if (mScaleAnimation.isRunning()) {
576 mScaleAnimation.cancel();
578 mCallback.expansionStateChanged(false);
579 int naturalHeight = mScaler.getNaturalHeight();
580 float targetHeight = nowExpanded ? naturalHeight : mSmallSize;
581 if (targetHeight != currentHeight && mEnabled && allowAnimation) {
582 mScaleAnimation.setFloatValues(targetHeight);
583 mScaleAnimation.setupStartValues();
584 final View scaledView = mResizedView;
585 final boolean expand = nowExpanded;
586 mScaleAnimation.addListener(new AnimatorListenerAdapter() {
587 public boolean mCancelled;
590 public void onAnimationEnd(Animator animation) {
592 mCallback.setUserExpandedChild(scaledView, expand);
594 mScaler.setView(null);
597 mCallback.setExpansionCancelled(scaledView);
599 mCallback.setUserLockedChild(scaledView, false);
600 mScaleAnimation.removeListener(this);
604 public void onAnimationCancel(Animator animation) {
608 velocity = nowExpanded == velocity >= 0 ? velocity : 0;
609 mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity);
610 mScaleAnimation.start();
612 if (targetHeight != currentHeight) {
613 mScaler.setHeight(targetHeight);
615 mCallback.setUserExpandedChild(mResizedView, nowExpanded);
616 mCallback.setUserLockedChild(mResizedView, false);
617 mScaler.setView(null);
621 mExpansionStyle = NONE;
623 if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
624 if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
625 if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
626 if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
627 if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView);
630 private void clearView() {
635 * Use this to abort any pending expansions in progress and force that there will be no
638 public void cancelImmediately() {
639 cancel(false /* allowAnimation */);
643 * Use this to abort any pending expansions in progress.
645 public void cancel() {
646 cancel(true /* allowAnimation */);
649 private void cancel(boolean allowAnimation) {
650 finishExpanding(true /* forceAbort */, 0f /* velocity */, allowAnimation);
653 // reset the gesture detector
654 mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
658 * Change the expansion mode to only observe movements and don't perform any resizing.
659 * This is needed when the expanding is finished and the scroller kicks in,
660 * performing an overscroll motion. We only want to shrink it again when we are not
663 * @param onlyMovements Should only movements be observed?
665 public void onlyObserveMovements(boolean onlyMovements) {
666 mOnlyMovements = onlyMovements;