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