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