• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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.phone;
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.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.view.InputDevice;
29 import android.view.MotionEvent;
30 import android.view.ViewConfiguration;
31 import android.view.ViewTreeObserver;
32 import android.view.animation.Interpolator;
33 import android.widget.FrameLayout;
34 
35 import com.android.systemui.EventLogConstants;
36 import com.android.systemui.EventLogTags;
37 import com.android.systemui.Interpolators;
38 import com.android.systemui.R;
39 import com.android.systemui.classifier.FalsingManager;
40 import com.android.systemui.doze.DozeLog;
41 import com.android.systemui.statusbar.FlingAnimationUtils;
42 import com.android.systemui.statusbar.StatusBarState;
43 import com.android.systemui.statusbar.policy.HeadsUpManager;
44 
45 import java.io.FileDescriptor;
46 import java.io.PrintWriter;
47 
48 public abstract class PanelView extends FrameLayout {
49     public static final boolean DEBUG = PanelBar.DEBUG;
50     public static final String TAG = PanelView.class.getSimpleName();
51 
logf(String fmt, Object... args)52     private final void logf(String fmt, Object... args) {
53         Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
54     }
55 
56     protected PhoneStatusBar mStatusBar;
57     protected HeadsUpManager mHeadsUpManager;
58 
59     private float mPeekHeight;
60     private float mHintDistance;
61     private float mInitialOffsetOnTouch;
62     private boolean mCollapsedAndHeadsUpOnDown;
63     private float mExpandedFraction = 0;
64     protected float mExpandedHeight = 0;
65     private boolean mPanelClosedOnDown;
66     private boolean mHasLayoutedSinceDown;
67     private float mUpdateFlingVelocity;
68     private boolean mUpdateFlingOnLayout;
69     private boolean mPeekTouching;
70     private boolean mJustPeeked;
71     private boolean mClosing;
72     protected boolean mTracking;
73     private boolean mTouchSlopExceeded;
74     private int mTrackingPointer;
75     protected int mTouchSlop;
76     protected boolean mHintAnimationRunning;
77     private boolean mOverExpandedBeforeFling;
78     private boolean mTouchAboveFalsingThreshold;
79     private int mUnlockFalsingThreshold;
80     private boolean mTouchStartedInEmptyArea;
81     private boolean mMotionAborted;
82     private boolean mUpwardsWhenTresholdReached;
83     private boolean mAnimatingOnDown;
84 
85     private ValueAnimator mHeightAnimator;
86     private ObjectAnimator mPeekAnimator;
87     private VelocityTrackerInterface mVelocityTracker;
88     private FlingAnimationUtils mFlingAnimationUtils;
89     private FalsingManager mFalsingManager;
90 
91     /**
92      * Whether an instant expand request is currently pending and we are just waiting for layout.
93      */
94     private boolean mInstantExpanding;
95 
96     PanelBar mBar;
97 
98     private String mViewName;
99     private float mInitialTouchY;
100     private float mInitialTouchX;
101     private boolean mTouchDisabled;
102 
103     private Interpolator mBounceInterpolator;
104     protected KeyguardBottomAreaView mKeyguardBottomArea;
105 
106     private boolean mPeekPending;
107     private boolean mCollapseAfterPeek;
108 
109     /**
110      * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
111      */
112     private float mNextCollapseSpeedUpFactor = 1.0f;
113 
114     protected boolean mExpanding;
115     private boolean mGestureWaitForTouchSlop;
116     private boolean mIgnoreXTouchSlop;
117     private Runnable mPeekRunnable = new Runnable() {
118         @Override
119         public void run() {
120             mPeekPending = false;
121             runPeekAnimation();
122         }
123     };
124 
onExpandingFinished()125     protected void onExpandingFinished() {
126         mBar.onExpandingFinished();
127     }
128 
onExpandingStarted()129     protected void onExpandingStarted() {
130     }
131 
notifyExpandingStarted()132     private void notifyExpandingStarted() {
133         if (!mExpanding) {
134             mExpanding = true;
135             onExpandingStarted();
136         }
137     }
138 
notifyExpandingFinished()139     protected final void notifyExpandingFinished() {
140         endClosing();
141         if (mExpanding) {
142             mExpanding = false;
143             onExpandingFinished();
144         }
145     }
146 
schedulePeek()147     private void schedulePeek() {
148         mPeekPending = true;
149         long timeout = ViewConfiguration.getTapTimeout();
150         postOnAnimationDelayed(mPeekRunnable, timeout);
151         notifyBarPanelExpansionChanged();
152     }
153 
runPeekAnimation()154     private void runPeekAnimation() {
155         mPeekHeight = getPeekHeight();
156         if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
157         if (mHeightAnimator != null) {
158             return;
159         }
160         mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight)
161                 .setDuration(250);
162         mPeekAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
163         mPeekAnimator.addListener(new AnimatorListenerAdapter() {
164             private boolean mCancelled;
165 
166             @Override
167             public void onAnimationCancel(Animator animation) {
168                 mCancelled = true;
169             }
170 
171             @Override
172             public void onAnimationEnd(Animator animation) {
173                 mPeekAnimator = null;
174                 if (mCollapseAfterPeek && !mCancelled) {
175                     postOnAnimation(mPostCollapseRunnable);
176                 }
177                 mCollapseAfterPeek = false;
178             }
179         });
180         notifyExpandingStarted();
181         mPeekAnimator.start();
182         mJustPeeked = true;
183     }
184 
PanelView(Context context, AttributeSet attrs)185     public PanelView(Context context, AttributeSet attrs) {
186         super(context, attrs);
187         mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f);
188         mBounceInterpolator = new BounceInterpolator();
189         mFalsingManager = FalsingManager.getInstance(context);
190     }
191 
loadDimens()192     protected void loadDimens() {
193         final Resources res = getContext().getResources();
194         final ViewConfiguration configuration = ViewConfiguration.get(getContext());
195         mTouchSlop = configuration.getScaledTouchSlop();
196         mHintDistance = res.getDimension(R.dimen.hint_move_distance);
197         mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold);
198     }
199 
trackMovement(MotionEvent event)200     private void trackMovement(MotionEvent event) {
201         // Add movement to velocity tracker using raw screen X and Y coordinates instead
202         // of window coordinates because the window frame may be moving at the same time.
203         float deltaX = event.getRawX() - event.getX();
204         float deltaY = event.getRawY() - event.getY();
205         event.offsetLocation(deltaX, deltaY);
206         if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
207         event.offsetLocation(-deltaX, -deltaY);
208     }
209 
setTouchDisabled(boolean disabled)210     public void setTouchDisabled(boolean disabled) {
211         mTouchDisabled = disabled;
212         if (mTouchDisabled && mTracking) {
213             onTrackingStopped(true /* expanded */);
214         }
215     }
216 
217     @Override
onTouchEvent(MotionEvent event)218     public boolean onTouchEvent(MotionEvent event) {
219         if (mInstantExpanding || mTouchDisabled
220                 || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
221             return false;
222         }
223 
224         // On expanding, single mouse click expands the panel instead of dragging.
225         if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
226             if (event.getAction() == MotionEvent.ACTION_UP) {
227                 expand(true);
228             }
229             return true;
230         }
231 
232         /*
233          * We capture touch events here and update the expand height here in case according to
234          * the users fingers. This also handles multi-touch.
235          *
236          * If the user just clicks shortly, we show a quick peek of the shade.
237          *
238          * Flinging is also enabled in order to open or close the shade.
239          */
240 
241         int pointerIndex = event.findPointerIndex(mTrackingPointer);
242         if (pointerIndex < 0) {
243             pointerIndex = 0;
244             mTrackingPointer = event.getPointerId(pointerIndex);
245         }
246         final float x = event.getX(pointerIndex);
247         final float y = event.getY(pointerIndex);
248 
249         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
250             mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
251             mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
252         }
253 
254         switch (event.getActionMasked()) {
255             case MotionEvent.ACTION_DOWN:
256                 startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
257                 mJustPeeked = false;
258                 mPanelClosedOnDown = isFullyCollapsed();
259                 mHasLayoutedSinceDown = false;
260                 mUpdateFlingOnLayout = false;
261                 mMotionAborted = false;
262                 mPeekTouching = mPanelClosedOnDown;
263                 mTouchAboveFalsingThreshold = false;
264                 mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
265                         && mHeadsUpManager.hasPinnedHeadsUp();
266                 if (mVelocityTracker == null) {
267                     initVelocityTracker();
268                 }
269                 trackMovement(event);
270                 if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) ||
271                         mPeekPending || mPeekAnimator != null) {
272                     cancelHeightAnimator();
273                     cancelPeek();
274                     mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
275                             || mPeekPending || mPeekAnimator != null;
276                     onTrackingStarted();
277                 }
278                 if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {
279                     schedulePeek();
280                 }
281                 break;
282 
283             case MotionEvent.ACTION_POINTER_UP:
284                 final int upPointer = event.getPointerId(event.getActionIndex());
285                 if (mTrackingPointer == upPointer) {
286                     // gesture is ongoing, find a new pointer to track
287                     final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
288                     final float newY = event.getY(newIndex);
289                     final float newX = event.getX(newIndex);
290                     mTrackingPointer = event.getPointerId(newIndex);
291                     startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
292                 }
293                 break;
294             case MotionEvent.ACTION_POINTER_DOWN:
295                 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
296                     mMotionAborted = true;
297                     endMotionEvent(event, x, y, true /* forceCancel */);
298                     return false;
299                 }
300                 break;
301             case MotionEvent.ACTION_MOVE:
302                 float h = y - mInitialTouchY;
303 
304                 // If the panel was collapsed when touching, we only need to check for the
305                 // y-component of the gesture, as we have no conflicting horizontal gesture.
306                 if (Math.abs(h) > mTouchSlop
307                         && (Math.abs(h) > Math.abs(x - mInitialTouchX)
308                                 || mIgnoreXTouchSlop)) {
309                     mTouchSlopExceeded = true;
310                     if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
311                         if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
312                             startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
313                             h = 0;
314                         }
315                         cancelHeightAnimator();
316                         removeCallbacks(mPeekRunnable);
317                         mPeekPending = false;
318                         onTrackingStarted();
319                     }
320                 }
321                 final float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
322                 if (newHeight > mPeekHeight) {
323                     if (mPeekAnimator != null) {
324                         mPeekAnimator.cancel();
325                     }
326                     mJustPeeked = false;
327                 }
328                 if (-h >= getFalsingThreshold()) {
329                     mTouchAboveFalsingThreshold = true;
330                     mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
331                 }
332                 if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
333                     setExpandedHeightInternal(newHeight);
334                 }
335 
336                 trackMovement(event);
337                 break;
338 
339             case MotionEvent.ACTION_UP:
340             case MotionEvent.ACTION_CANCEL:
341                 trackMovement(event);
342                 endMotionEvent(event, x, y, false /* forceCancel */);
343                 break;
344         }
345         return !mGestureWaitForTouchSlop || mTracking;
346     }
347 
348     /**
349      * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
350      * horizontal direction
351      */
isDirectionUpwards(float x, float y)352     private boolean isDirectionUpwards(float x, float y) {
353         float xDiff = x - mInitialTouchX;
354         float yDiff = y - mInitialTouchY;
355         if (yDiff >= 0) {
356             return false;
357         }
358         return Math.abs(yDiff) >= Math.abs(xDiff);
359     }
360 
startExpandMotion(float newX, float newY, boolean startTracking, float expandedHeight)361     protected void startExpandMotion(float newX, float newY, boolean startTracking,
362             float expandedHeight) {
363         mInitialOffsetOnTouch = expandedHeight;
364         mInitialTouchY = newY;
365         mInitialTouchX = newX;
366         if (startTracking) {
367             mTouchSlopExceeded = true;
368             setExpandedHeight(mInitialOffsetOnTouch);
369             onTrackingStarted();
370         }
371     }
372 
endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel)373     private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
374         mTrackingPointer = -1;
375         if ((mTracking && mTouchSlopExceeded)
376                 || Math.abs(x - mInitialTouchX) > mTouchSlop
377                 || Math.abs(y - mInitialTouchY) > mTouchSlop
378                 || event.getActionMasked() == MotionEvent.ACTION_CANCEL
379                 || forceCancel) {
380             float vel = 0f;
381             float vectorVel = 0f;
382             if (mVelocityTracker != null) {
383                 mVelocityTracker.computeCurrentVelocity(1000);
384                 vel = mVelocityTracker.getYVelocity();
385                 vectorVel = (float) Math.hypot(
386                         mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
387             }
388             boolean expand = flingExpands(vel, vectorVel, x, y)
389                     || event.getActionMasked() == MotionEvent.ACTION_CANCEL
390                     || forceCancel;
391             DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
392                     mStatusBar.isFalsingThresholdNeeded(),
393                     mStatusBar.isWakeUpComingFromTouch());
394                     // Log collapse gesture if on lock screen.
395                     if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
396                         float displayDensity = mStatusBar.getDisplayDensity();
397                         int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity);
398                         int velocityDp = (int) Math.abs(vel / displayDensity);
399                         EventLogTags.writeSysuiLockscreenGesture(
400                                 EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK,
401                                 heightDp, velocityDp);
402                     }
403             fling(vel, expand, isFalseTouch(x, y));
404             onTrackingStopped(expand);
405             mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
406             if (mUpdateFlingOnLayout) {
407                 mUpdateFlingVelocity = vel;
408             }
409         } else {
410             boolean expands = onEmptySpaceClick(mInitialTouchX);
411             onTrackingStopped(expands);
412         }
413 
414         if (mVelocityTracker != null) {
415             mVelocityTracker.recycle();
416             mVelocityTracker = null;
417         }
418         mPeekTouching = false;
419     }
420 
getFalsingThreshold()421     private int getFalsingThreshold() {
422         float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
423         return (int) (mUnlockFalsingThreshold * factor);
424     }
425 
hasConflictingGestures()426     protected abstract boolean hasConflictingGestures();
427 
shouldGestureIgnoreXTouchSlop(float x, float y)428     protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
429 
onTrackingStopped(boolean expand)430     protected void onTrackingStopped(boolean expand) {
431         mTracking = false;
432         mBar.onTrackingStopped(expand);
433         notifyBarPanelExpansionChanged();
434     }
435 
onTrackingStarted()436     protected void onTrackingStarted() {
437         endClosing();
438         mTracking = true;
439         mCollapseAfterPeek = false;
440         mBar.onTrackingStarted();
441         notifyExpandingStarted();
442         notifyBarPanelExpansionChanged();
443     }
444 
445     @Override
onInterceptTouchEvent(MotionEvent event)446     public boolean onInterceptTouchEvent(MotionEvent event) {
447         if (mInstantExpanding
448                 || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
449             return false;
450         }
451 
452         /*
453          * If the user drags anywhere inside the panel we intercept it if the movement is
454          * upwards. This allows closing the shade from anywhere inside the panel.
455          *
456          * We only do this if the current content is scrolled to the bottom,
457          * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
458          * possible.
459          */
460         int pointerIndex = event.findPointerIndex(mTrackingPointer);
461         if (pointerIndex < 0) {
462             pointerIndex = 0;
463             mTrackingPointer = event.getPointerId(pointerIndex);
464         }
465         final float x = event.getX(pointerIndex);
466         final float y = event.getY(pointerIndex);
467         boolean scrolledToBottom = isScrolledToBottom();
468 
469         switch (event.getActionMasked()) {
470             case MotionEvent.ACTION_DOWN:
471                 mStatusBar.userActivity();
472                 mAnimatingOnDown = mHeightAnimator != null;
473                 if (mAnimatingOnDown && mClosing && !mHintAnimationRunning || mPeekPending || mPeekAnimator != null) {
474                     cancelHeightAnimator();
475                     cancelPeek();
476                     mTouchSlopExceeded = true;
477                     return true;
478                 }
479                 mInitialTouchY = y;
480                 mInitialTouchX = x;
481                 mTouchStartedInEmptyArea = !isInContentBounds(x, y);
482                 mTouchSlopExceeded = false;
483                 mJustPeeked = false;
484                 mMotionAborted = false;
485                 mPanelClosedOnDown = isFullyCollapsed();
486                 mCollapsedAndHeadsUpOnDown = false;
487                 mHasLayoutedSinceDown = false;
488                 mUpdateFlingOnLayout = false;
489                 mTouchAboveFalsingThreshold = false;
490                 initVelocityTracker();
491                 trackMovement(event);
492                 break;
493             case MotionEvent.ACTION_POINTER_UP:
494                 final int upPointer = event.getPointerId(event.getActionIndex());
495                 if (mTrackingPointer == upPointer) {
496                     // gesture is ongoing, find a new pointer to track
497                     final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
498                     mTrackingPointer = event.getPointerId(newIndex);
499                     mInitialTouchX = event.getX(newIndex);
500                     mInitialTouchY = event.getY(newIndex);
501                 }
502                 break;
503             case MotionEvent.ACTION_POINTER_DOWN:
504                 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
505                     mMotionAborted = true;
506                     if (mVelocityTracker != null) {
507                         mVelocityTracker.recycle();
508                         mVelocityTracker = null;
509                     }
510                 }
511                 break;
512             case MotionEvent.ACTION_MOVE:
513                 final float h = y - mInitialTouchY;
514                 trackMovement(event);
515                 if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) {
516                     float hAbs = Math.abs(h);
517                     if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop))
518                             && hAbs > Math.abs(x - mInitialTouchX)) {
519                         cancelHeightAnimator();
520                         startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
521                         return true;
522                     }
523                 }
524                 break;
525             case MotionEvent.ACTION_CANCEL:
526             case MotionEvent.ACTION_UP:
527                 if (mVelocityTracker != null) {
528                     mVelocityTracker.recycle();
529                     mVelocityTracker = null;
530                 }
531                 break;
532         }
533         return false;
534     }
535 
536     /**
537      * @return Whether a pair of coordinates are inside the visible view content bounds.
538      */
isInContentBounds(float x, float y)539     protected abstract boolean isInContentBounds(float x, float y);
540 
cancelHeightAnimator()541     protected void cancelHeightAnimator() {
542         if (mHeightAnimator != null) {
543             mHeightAnimator.cancel();
544         }
545         endClosing();
546     }
547 
endClosing()548     private void endClosing() {
549         if (mClosing) {
550             mClosing = false;
551             onClosingFinished();
552         }
553     }
554 
initVelocityTracker()555     private void initVelocityTracker() {
556         if (mVelocityTracker != null) {
557             mVelocityTracker.recycle();
558         }
559         mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
560     }
561 
isScrolledToBottom()562     protected boolean isScrolledToBottom() {
563         return true;
564     }
565 
getContentHeight()566     protected float getContentHeight() {
567         return mExpandedHeight;
568     }
569 
570     @Override
onFinishInflate()571     protected void onFinishInflate() {
572         super.onFinishInflate();
573         loadDimens();
574     }
575 
576     @Override
onConfigurationChanged(Configuration newConfig)577     protected void onConfigurationChanged(Configuration newConfig) {
578         super.onConfigurationChanged(newConfig);
579         loadDimens();
580     }
581 
582     /**
583      * @param vel the current vertical velocity of the motion
584      * @param vectorVel the length of the vectorial velocity
585      * @return whether a fling should expands the panel; contracts otherwise
586      */
flingExpands(float vel, float vectorVel, float x, float y)587     protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
588         if (isFalseTouch(x, y)) {
589             return true;
590         }
591         if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
592             return getExpandedFraction() > 0.5f;
593         } else {
594             return vel > 0;
595         }
596     }
597 
598     /**
599      * @param x the final x-coordinate when the finger was lifted
600      * @param y the final y-coordinate when the finger was lifted
601      * @return whether this motion should be regarded as a false touch
602      */
isFalseTouch(float x, float y)603     private boolean isFalseTouch(float x, float y) {
604         if (!mStatusBar.isFalsingThresholdNeeded()) {
605             return false;
606         }
607         if (mFalsingManager.isClassiferEnabled()) {
608             return mFalsingManager.isFalseTouch();
609         }
610         if (!mTouchAboveFalsingThreshold) {
611             return true;
612         }
613         if (mUpwardsWhenTresholdReached) {
614             return false;
615         }
616         return !isDirectionUpwards(x, y);
617     }
618 
fling(float vel, boolean expand)619     protected void fling(float vel, boolean expand) {
620         fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
621     }
622 
fling(float vel, boolean expand, boolean expandBecauseOfFalsing)623     protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
624         fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
625     }
626 
fling(float vel, boolean expand, float collapseSpeedUpFactor, boolean expandBecauseOfFalsing)627     protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
628             boolean expandBecauseOfFalsing) {
629         cancelPeek();
630         float target = expand ? getMaxPanelHeight() : 0.0f;
631         if (!expand) {
632             mClosing = true;
633         }
634         flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
635     }
636 
flingToHeight(float vel, boolean expand, float target, float collapseSpeedUpFactor, boolean expandBecauseOfFalsing)637     protected void flingToHeight(float vel, boolean expand, float target,
638             float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
639         // Hack to make the expand transition look nice when clear all button is visible - we make
640         // the animation only to the last notification, and then jump to the maximum panel height so
641         // clear all just fades in and the decelerating motion is towards the last notification.
642         final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
643                 && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
644                 && !isClearAllVisible();
645         if (clearAllExpandHack) {
646             target = getMaxPanelHeight() - getClearAllHeight();
647         }
648         if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
649             notifyExpandingFinished();
650             return;
651         }
652         mOverExpandedBeforeFling = getOverExpansionAmount() > 0f;
653         ValueAnimator animator = createHeightAnimator(target);
654         if (expand) {
655             if (expandBecauseOfFalsing) {
656                 vel = 0;
657             }
658             mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
659             if (expandBecauseOfFalsing) {
660                 animator.setDuration(350);
661             }
662         } else {
663             mFlingAnimationUtils.applyDismissing(animator, mExpandedHeight, target, vel,
664                     getHeight());
665 
666             // Make it shorter if we run a canned animation
667             if (vel == 0) {
668                 animator.setDuration((long)
669                         (animator.getDuration() * getCannedFlingDurationFactor()
670                                 / collapseSpeedUpFactor));
671             }
672         }
673         animator.addListener(new AnimatorListenerAdapter() {
674             private boolean mCancelled;
675 
676             @Override
677             public void onAnimationCancel(Animator animation) {
678                 mCancelled = true;
679             }
680 
681             @Override
682             public void onAnimationEnd(Animator animation) {
683                 if (clearAllExpandHack && !mCancelled) {
684                     setExpandedHeightInternal(getMaxPanelHeight());
685                 }
686                 mHeightAnimator = null;
687                 if (!mCancelled) {
688                     notifyExpandingFinished();
689                 }
690                 notifyBarPanelExpansionChanged();
691             }
692         });
693         mHeightAnimator = animator;
694         animator.start();
695     }
696 
697     @Override
onAttachedToWindow()698     protected void onAttachedToWindow() {
699         super.onAttachedToWindow();
700         mViewName = getResources().getResourceName(getId());
701     }
702 
getName()703     public String getName() {
704         return mViewName;
705     }
706 
setExpandedHeight(float height)707     public void setExpandedHeight(float height) {
708         if (DEBUG) logf("setExpandedHeight(%.1f)", height);
709         setExpandedHeightInternal(height + getOverExpansionPixels());
710     }
711 
712     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)713     protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
714         super.onLayout(changed, left, top, right, bottom);
715         mStatusBar.onPanelLaidOut();
716         requestPanelHeightUpdate();
717         mHasLayoutedSinceDown = true;
718         if (mUpdateFlingOnLayout) {
719             abortAnimations();
720             fling(mUpdateFlingVelocity, true /* expands */);
721             mUpdateFlingOnLayout = false;
722         }
723     }
724 
requestPanelHeightUpdate()725     protected void requestPanelHeightUpdate() {
726         float currentMaxPanelHeight = getMaxPanelHeight();
727 
728         // If the user isn't actively poking us, let's update the height
729         if ((!mTracking || isTrackingBlocked())
730                 && mHeightAnimator == null
731                 && !isFullyCollapsed()
732                 && currentMaxPanelHeight != mExpandedHeight
733                 && !mPeekPending
734                 && mPeekAnimator == null
735                 && !mPeekTouching) {
736             setExpandedHeight(currentMaxPanelHeight);
737         }
738     }
739 
setExpandedHeightInternal(float h)740     public void setExpandedHeightInternal(float h) {
741         float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
742         if (mHeightAnimator == null) {
743             float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
744             if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
745                 setOverExpansion(overExpansionPixels, true /* isPixels */);
746             }
747             mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
748         } else {
749             mExpandedHeight = h;
750             if (mOverExpandedBeforeFling) {
751                 setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
752             }
753         }
754 
755         mExpandedHeight = Math.max(0, mExpandedHeight);
756         mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0
757                 ? 0
758                 : mExpandedHeight / fhWithoutOverExpansion);
759         onHeightUpdated(mExpandedHeight);
760         notifyBarPanelExpansionChanged();
761     }
762 
763     /**
764      * @return true if the panel tracking should be temporarily blocked; this is used when a
765      *         conflicting gesture (opening QS) is happening
766      */
isTrackingBlocked()767     protected abstract boolean isTrackingBlocked();
768 
setOverExpansion(float overExpansion, boolean isPixels)769     protected abstract void setOverExpansion(float overExpansion, boolean isPixels);
770 
onHeightUpdated(float expandedHeight)771     protected abstract void onHeightUpdated(float expandedHeight);
772 
getOverExpansionAmount()773     protected abstract float getOverExpansionAmount();
774 
getOverExpansionPixels()775     protected abstract float getOverExpansionPixels();
776 
777     /**
778      * This returns the maximum height of the panel. Children should override this if their
779      * desired height is not the full height.
780      *
781      * @return the default implementation simply returns the maximum height.
782      */
getMaxPanelHeight()783     protected abstract int getMaxPanelHeight();
784 
setExpandedFraction(float frac)785     public void setExpandedFraction(float frac) {
786         setExpandedHeight(getMaxPanelHeight() * frac);
787     }
788 
getExpandedHeight()789     public float getExpandedHeight() {
790         return mExpandedHeight;
791     }
792 
getExpandedFraction()793     public float getExpandedFraction() {
794         return mExpandedFraction;
795     }
796 
isFullyExpanded()797     public boolean isFullyExpanded() {
798         return mExpandedHeight >= getMaxPanelHeight();
799     }
800 
isFullyCollapsed()801     public boolean isFullyCollapsed() {
802         return mExpandedHeight <= 0;
803     }
804 
isCollapsing()805     public boolean isCollapsing() {
806         return mClosing;
807     }
808 
isTracking()809     public boolean isTracking() {
810         return mTracking;
811     }
812 
setBar(PanelBar panelBar)813     public void setBar(PanelBar panelBar) {
814         mBar = panelBar;
815     }
816 
collapse(boolean delayed, float speedUpFactor)817     public void collapse(boolean delayed, float speedUpFactor) {
818         if (DEBUG) logf("collapse: " + this);
819         if (mPeekPending || mPeekAnimator != null) {
820             mCollapseAfterPeek = true;
821             if (mPeekPending) {
822 
823                 // We know that the whole gesture is just a peek triggered by a simple click, so
824                 // better start it now.
825                 removeCallbacks(mPeekRunnable);
826                 mPeekRunnable.run();
827             }
828         } else if (!isFullyCollapsed() && !mTracking && !mClosing) {
829             cancelHeightAnimator();
830             notifyExpandingStarted();
831 
832             // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
833             mClosing = true;
834             if (delayed) {
835                 mNextCollapseSpeedUpFactor = speedUpFactor;
836                 postDelayed(mFlingCollapseRunnable, 120);
837             } else {
838                 fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
839             }
840         }
841     }
842 
843     private final Runnable mFlingCollapseRunnable = new Runnable() {
844         @Override
845         public void run() {
846             fling(0, false /* expand */, mNextCollapseSpeedUpFactor,
847                     false /* expandBecauseOfFalsing */);
848         }
849     };
850 
cancelPeek()851     public void cancelPeek() {
852         boolean cancelled = mPeekPending;
853         if (mPeekAnimator != null) {
854             cancelled = true;
855             mPeekAnimator.cancel();
856         }
857         removeCallbacks(mPeekRunnable);
858         mPeekPending = false;
859 
860         if (cancelled) {
861             // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also
862             // notify mBar that we might have closed ourselves.
863             notifyBarPanelExpansionChanged();
864         }
865     }
866 
expand(final boolean animate)867     public void expand(final boolean animate) {
868         if (!isFullyCollapsed() && !isCollapsing()) {
869             return;
870         }
871 
872         mInstantExpanding = true;
873         mUpdateFlingOnLayout = false;
874         abortAnimations();
875         cancelPeek();
876         if (mTracking) {
877             onTrackingStopped(true /* expands */); // The panel is expanded after this call.
878         }
879         if (mExpanding) {
880             notifyExpandingFinished();
881         }
882         notifyBarPanelExpansionChanged();
883 
884         // Wait for window manager to pickup the change, so we know the maximum height of the panel
885         // then.
886         getViewTreeObserver().addOnGlobalLayoutListener(
887                 new ViewTreeObserver.OnGlobalLayoutListener() {
888                     @Override
889                     public void onGlobalLayout() {
890                         if (!mInstantExpanding) {
891                             getViewTreeObserver().removeOnGlobalLayoutListener(this);
892                             return;
893                         }
894                         if (mStatusBar.getStatusBarWindow().getHeight()
895                                 != mStatusBar.getStatusBarHeight()) {
896                             getViewTreeObserver().removeOnGlobalLayoutListener(this);
897                             if (animate) {
898                                 notifyExpandingStarted();
899                                 fling(0, true /* expand */);
900                             } else {
901                                 setExpandedFraction(1f);
902                             }
903                             mInstantExpanding = false;
904                         }
905                     }
906                 });
907 
908         // Make sure a layout really happens.
909         requestLayout();
910     }
911 
instantCollapse()912     public void instantCollapse() {
913         abortAnimations();
914         setExpandedFraction(0f);
915         if (mExpanding) {
916             notifyExpandingFinished();
917         }
918         if (mInstantExpanding) {
919             mInstantExpanding = false;
920             notifyBarPanelExpansionChanged();
921         }
922     }
923 
abortAnimations()924     private void abortAnimations() {
925         cancelPeek();
926         cancelHeightAnimator();
927         removeCallbacks(mPostCollapseRunnable);
928         removeCallbacks(mFlingCollapseRunnable);
929     }
930 
onClosingFinished()931     protected void onClosingFinished() {
932         mBar.onClosingFinished();
933     }
934 
935 
startUnlockHintAnimation()936     protected void startUnlockHintAnimation() {
937 
938         // We don't need to hint the user if an animation is already running or the user is changing
939         // the expansion.
940         if (mHeightAnimator != null || mTracking) {
941             return;
942         }
943         cancelPeek();
944         notifyExpandingStarted();
945         startUnlockHintAnimationPhase1(new Runnable() {
946             @Override
947             public void run() {
948                 notifyExpandingFinished();
949                 mStatusBar.onHintFinished();
950                 mHintAnimationRunning = false;
951             }
952         });
953         mStatusBar.onUnlockHintStarted();
954         mHintAnimationRunning = true;
955     }
956 
957     /**
958      * Phase 1: Move everything upwards.
959      */
startUnlockHintAnimationPhase1(final Runnable onAnimationFinished)960     private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
961         float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
962         ValueAnimator animator = createHeightAnimator(target);
963         animator.setDuration(250);
964         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
965         animator.addListener(new AnimatorListenerAdapter() {
966             private boolean mCancelled;
967 
968             @Override
969             public void onAnimationCancel(Animator animation) {
970                 mCancelled = true;
971             }
972 
973             @Override
974             public void onAnimationEnd(Animator animation) {
975                 if (mCancelled) {
976                     mHeightAnimator = null;
977                     onAnimationFinished.run();
978                 } else {
979                     startUnlockHintAnimationPhase2(onAnimationFinished);
980                 }
981             }
982         });
983         animator.start();
984         mHeightAnimator = animator;
985         mKeyguardBottomArea.getIndicationView().animate()
986                 .translationY(-mHintDistance)
987                 .setDuration(250)
988                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
989                 .withEndAction(new Runnable() {
990                     @Override
991                     public void run() {
992                         mKeyguardBottomArea.getIndicationView().animate()
993                                 .translationY(0)
994                                 .setDuration(450)
995                                 .setInterpolator(mBounceInterpolator)
996                                 .start();
997                     }
998                 })
999                 .start();
1000     }
1001 
1002     /**
1003      * Phase 2: Bounce down.
1004      */
startUnlockHintAnimationPhase2(final Runnable onAnimationFinished)1005     private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
1006         ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
1007         animator.setDuration(450);
1008         animator.setInterpolator(mBounceInterpolator);
1009         animator.addListener(new AnimatorListenerAdapter() {
1010             @Override
1011             public void onAnimationEnd(Animator animation) {
1012                 mHeightAnimator = null;
1013                 onAnimationFinished.run();
1014                 notifyBarPanelExpansionChanged();
1015             }
1016         });
1017         animator.start();
1018         mHeightAnimator = animator;
1019     }
1020 
createHeightAnimator(float targetHeight)1021     private ValueAnimator createHeightAnimator(float targetHeight) {
1022         ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
1023         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1024             @Override
1025             public void onAnimationUpdate(ValueAnimator animation) {
1026                 setExpandedHeightInternal((Float) animation.getAnimatedValue());
1027             }
1028         });
1029         return animator;
1030     }
1031 
notifyBarPanelExpansionChanged()1032     protected void notifyBarPanelExpansionChanged() {
1033         mBar.panelExpansionChanged(mExpandedFraction, mExpandedFraction > 0f || mPeekPending
1034                 || mPeekAnimator != null || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp()
1035                 || mTracking || mHeightAnimator != null);
1036     }
1037 
isPanelVisibleBecauseOfHeadsUp()1038     protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
1039 
1040     /**
1041      * Gets called when the user performs a click anywhere in the empty area of the panel.
1042      *
1043      * @return whether the panel will be expanded after the action performed by this method
1044      */
onEmptySpaceClick(float x)1045     protected boolean onEmptySpaceClick(float x) {
1046         if (mHintAnimationRunning) {
1047             return true;
1048         }
1049         return onMiddleClicked();
1050     }
1051 
1052     protected final Runnable mPostCollapseRunnable = new Runnable() {
1053         @Override
1054         public void run() {
1055             collapse(false /* delayed */, 1.0f /* speedUpFactor */);
1056         }
1057     };
1058 
onMiddleClicked()1059     protected abstract boolean onMiddleClicked();
1060 
isDozing()1061     protected abstract boolean isDozing();
1062 
dump(FileDescriptor fd, PrintWriter pw, String[] args)1063     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1064         pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
1065                 + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s"
1066                 + "]",
1067                 this.getClass().getSimpleName(),
1068                 getExpandedHeight(),
1069                 getMaxPanelHeight(),
1070                 mClosing?"T":"f",
1071                 mTracking?"T":"f",
1072                 mJustPeeked?"T":"f",
1073                 mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
1074                 mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""),
1075                 mTouchDisabled?"T":"f"
1076         ));
1077     }
1078 
resetViews()1079     public abstract void resetViews();
1080 
getPeekHeight()1081     protected abstract float getPeekHeight();
1082 
getCannedFlingDurationFactor()1083     protected abstract float getCannedFlingDurationFactor();
1084 
1085     /**
1086      * @return whether "Clear all" button will be visible when the panel is fully expanded
1087      */
fullyExpandedClearAllVisible()1088     protected abstract boolean fullyExpandedClearAllVisible();
1089 
isClearAllVisible()1090     protected abstract boolean isClearAllVisible();
1091 
1092     /**
1093      * @return the height of the clear all button, in pixels
1094      */
getClearAllHeight()1095     protected abstract int getClearAllHeight();
1096 
setHeadsUpManager(HeadsUpManager headsUpManager)1097     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
1098         mHeadsUpManager = headsUpManager;
1099     }
1100 }
1101