Home | History | Annotate | Download | only in systemui
      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 
     18 package com.android.systemui;
     19 
     20 import android.animation.Animator;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.animation.AnimatorSet;
     23 import android.animation.ObjectAnimator;
     24 import android.content.Context;
     25 import android.os.Vibrator;
     26 import android.util.Slog;
     27 import android.view.Gravity;
     28 import android.view.MotionEvent;
     29 import android.view.ScaleGestureDetector;
     30 import android.view.View;
     31 import android.view.ViewConfiguration;
     32 import android.view.ViewGroup;
     33 import android.view.View.OnClickListener;
     34 
     35 import java.util.Stack;
     36 
     37 public class ExpandHelper implements Gefingerpoken, OnClickListener {
     38     public interface Callback {
     39         View getChildAtRawPosition(float x, float y);
     40         View getChildAtPosition(float x, float y);
     41         boolean canChildBeExpanded(View v);
     42         boolean setUserExpandedChild(View v, boolean userExpanded);
     43         boolean setUserLockedChild(View v, boolean userLocked);
     44     }
     45 
     46     private static final String TAG = "ExpandHelper";
     47     protected static final boolean DEBUG = false;
     48     protected static final boolean DEBUG_SCALE = false;
     49     protected static final boolean DEBUG_GLOW = false;
     50     private static final long EXPAND_DURATION = 250;
     51     private static final long GLOW_DURATION = 150;
     52 
     53     // Set to false to disable focus-based gestures (spread-finger vertical pull).
     54     private static final boolean USE_DRAG = true;
     55     // Set to false to disable scale-based gestures (both horizontal and vertical).
     56     private static final boolean USE_SPAN = true;
     57     // Both gestures types may be active at the same time.
     58     // At least one gesture type should be active.
     59     // A variant of the screwdriver gesture will emerge from either gesture type.
     60 
     61     // amount of overstretch for maximum brightness expressed in U
     62     // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
     63     private static final float STRETCH_INTERVAL = 2f;
     64 
     65     // level of glow for a touch, without overstretch
     66     // overstretch fills the range (GLOW_BASE, 1.0]
     67     private static final float GLOW_BASE = 0.5f;
     68 
     69     @SuppressWarnings("unused")
     70     private Context mContext;
     71 
     72     private boolean mExpanding;
     73     private static final int NONE    = 0;
     74     private static final int BLINDS  = 1<<0;
     75     private static final int PULL    = 1<<1;
     76     private static final int STRETCH = 1<<2;
     77     private int mExpansionStyle = NONE;
     78     private boolean mWatchingForPull;
     79     private boolean mHasPopped;
     80     private View mEventSource;
     81     private View mCurrView;
     82     private View mCurrViewTopGlow;
     83     private View mCurrViewBottomGlow;
     84     private float mOldHeight;
     85     private float mNaturalHeight;
     86     private float mInitialTouchFocusY;
     87     private float mInitialTouchY;
     88     private float mInitialTouchSpan;
     89     private int mTouchSlop;
     90     private int mLastMotionY;
     91     private float mPopLimit;
     92     private int mPopDuration;
     93     private float mPullGestureMinXSpan;
     94     private Callback mCallback;
     95     private ScaleGestureDetector mSGD;
     96     private ViewScaler mScaler;
     97     private ObjectAnimator mScaleAnimation;
     98     private AnimatorSet mGlowAnimationSet;
     99     private ObjectAnimator mGlowTopAnimation;
    100     private ObjectAnimator mGlowBottomAnimation;
    101     private Vibrator mVibrator;
    102 
    103     private int mSmallSize;
    104     private int mLargeSize;
    105     private float mMaximumStretch;
    106 
    107     private int mGravity;
    108 
    109     private View mScrollView;
    110 
    111     private class ViewScaler {
    112         View mView;
    113 
    114         public ViewScaler() {}
    115         public void setView(View v) {
    116             mView = v;
    117         }
    118         public void setHeight(float h) {
    119             if (DEBUG_SCALE) Slog.v(TAG, "SetHeight: setting to " + h);
    120             ViewGroup.LayoutParams lp = mView.getLayoutParams();
    121             lp.height = (int)h;
    122             mView.setLayoutParams(lp);
    123             mView.requestLayout();
    124         }
    125         public float getHeight() {
    126             int height = mView.getLayoutParams().height;
    127             if (height < 0) {
    128                 height = mView.getMeasuredHeight();
    129             }
    130             return height;
    131         }
    132         public int getNaturalHeight(int maximum) {
    133             ViewGroup.LayoutParams lp = mView.getLayoutParams();
    134             if (DEBUG_SCALE) Slog.v(TAG, "Inspecting a child of type: " +
    135                     mView.getClass().getName());
    136             int oldHeight = lp.height;
    137             lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
    138             mView.setLayoutParams(lp);
    139             mView.measure(
    140                     View.MeasureSpec.makeMeasureSpec(mView.getMeasuredWidth(),
    141                                                      View.MeasureSpec.EXACTLY),
    142                     View.MeasureSpec.makeMeasureSpec(maximum,
    143                                                      View.MeasureSpec.AT_MOST));
    144             lp.height = oldHeight;
    145             mView.setLayoutParams(lp);
    146             return mView.getMeasuredHeight();
    147         }
    148     }
    149 
    150     /**
    151      * Handle expansion gestures to expand and contract children of the callback.
    152      *
    153      * @param context application context
    154      * @param callback the container that holds the items to be manipulated
    155      * @param small the smallest allowable size for the manuipulated items.
    156      * @param large the largest allowable size for the manuipulated items.
    157      * @param scoller if non-null also manipulate the scroll position to obey the gravity.
    158      */
    159     public ExpandHelper(Context context, Callback callback, int small, int large) {
    160         mSmallSize = small;
    161         mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
    162         mLargeSize = large;
    163         mContext = context;
    164         mCallback = callback;
    165         mScaler = new ViewScaler();
    166         mGravity = Gravity.TOP;
    167         mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
    168         mScaleAnimation.setDuration(EXPAND_DURATION);
    169         mPopLimit = mContext.getResources().getDimension(R.dimen.blinds_pop_threshold);
    170         mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms);
    171         mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
    172 
    173         AnimatorListenerAdapter glowVisibilityController = new AnimatorListenerAdapter() {
    174             @Override
    175             public void onAnimationStart(Animator animation) {
    176                 View target = (View) ((ObjectAnimator) animation).getTarget();
    177                 if (target.getAlpha() <= 0.0f) {
    178                     target.setVisibility(View.VISIBLE);
    179                 }
    180             }
    181 
    182             @Override
    183             public void onAnimationEnd(Animator animation) {
    184                 View target = (View) ((ObjectAnimator) animation).getTarget();
    185                 if (target.getAlpha() <= 0.0f) {
    186                     target.setVisibility(View.INVISIBLE);
    187                 }
    188             }
    189         };
    190 
    191         mGlowTopAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
    192         mGlowTopAnimation.addListener(glowVisibilityController);
    193         mGlowBottomAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
    194         mGlowBottomAnimation.addListener(glowVisibilityController);
    195         mGlowAnimationSet = new AnimatorSet();
    196         mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation);
    197         mGlowAnimationSet.setDuration(GLOW_DURATION);
    198 
    199         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
    200         mTouchSlop = configuration.getScaledTouchSlop();
    201 
    202         mSGD = new ScaleGestureDetector(context,
    203                                          new ScaleGestureDetector.SimpleOnScaleGestureListener() {
    204             @Override
    205             public boolean onScaleBegin(ScaleGestureDetector detector) {
    206                 if (DEBUG_SCALE) Slog.v(TAG, "onscalebegin()");
    207                 float focusX = detector.getFocusX();
    208                 float focusY = detector.getFocusY();
    209 
    210                 // your fingers have to be somewhat close to the bounds of the view in question
    211                 mInitialTouchFocusY = focusY;
    212                 mInitialTouchSpan = Math.abs(detector.getCurrentSpan());
    213                 if (DEBUG_SCALE) Slog.d(TAG, "got mInitialTouchSpan: (" + mInitialTouchSpan + ")");
    214 
    215                 final View underFocus = findView(focusX, focusY);
    216                 if (underFocus != null) {
    217                     startExpanding(underFocus, STRETCH);
    218                 }
    219                 return mExpanding;
    220             }
    221 
    222             @Override
    223             public boolean onScale(ScaleGestureDetector detector) {
    224                 if (DEBUG_SCALE) Slog.v(TAG, "onscale() on " + mCurrView);
    225                 updateExpansion();
    226                 return true;
    227             }
    228 
    229             @Override
    230             public void onScaleEnd(ScaleGestureDetector detector) {
    231                 if (DEBUG_SCALE) Slog.v(TAG, "onscaleend()");
    232                 // I guess we're alone now
    233                 if (DEBUG_SCALE) Slog.d(TAG, "scale end");
    234                 finishExpanding(false);
    235                 clearView();
    236             }
    237         });
    238     }
    239 
    240     private void updateExpansion() {
    241         // are we scaling or dragging?
    242         float span = Math.abs(mSGD.getCurrentSpan()) - mInitialTouchSpan;
    243         span *= USE_SPAN ? 1f : 0f;
    244         float drag = mSGD.getFocusY() - mInitialTouchFocusY;
    245         drag *= USE_DRAG ? 1f : 0f;
    246         drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
    247         float pull = Math.abs(drag) + Math.abs(span) + 1f;
    248         float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
    249         float target = hand + mOldHeight;
    250         float newHeight = clamp(target);
    251         mScaler.setHeight(newHeight);
    252 
    253         setGlow(calculateGlow(target, newHeight));
    254     }
    255 
    256     private float clamp(float target) {
    257         float out = target;
    258         out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out);
    259         out = out > mNaturalHeight ? mNaturalHeight : out;
    260         return out;
    261     }
    262 
    263     private View findView(float x, float y) {
    264         View v = null;
    265         if (mEventSource != null) {
    266             int[] location = new int[2];
    267             mEventSource.getLocationOnScreen(location);
    268             x += location[0];
    269             y += location[1];
    270             v = mCallback.getChildAtRawPosition(x, y);
    271         } else {
    272             v = mCallback.getChildAtPosition(x, y);
    273         }
    274         return v;
    275     }
    276 
    277     private boolean isInside(View v, float x, float y) {
    278         if (DEBUG) Slog.d(TAG, "isinside (" + x + ", " + y + ")");
    279 
    280         if (v == null) {
    281             if (DEBUG) Slog.d(TAG, "isinside null subject");
    282             return false;
    283         }
    284         if (mEventSource != null) {
    285             int[] location = new int[2];
    286             mEventSource.getLocationOnScreen(location);
    287             x += location[0];
    288             y += location[1];
    289             if (DEBUG) Slog.d(TAG, "  to global (" + x + ", " + y + ")");
    290         }
    291         int[] location = new int[2];
    292         v.getLocationOnScreen(location);
    293         x -= location[0];
    294         y -= location[1];
    295         if (DEBUG) Slog.d(TAG, "  to local (" + x + ", " + y + ")");
    296         if (DEBUG) Slog.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
    297         boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
    298         return inside;
    299     }
    300 
    301     public void setEventSource(View eventSource) {
    302         mEventSource = eventSource;
    303     }
    304 
    305     public void setGravity(int gravity) {
    306         mGravity = gravity;
    307     }
    308 
    309     public void setScrollView(View scrollView) {
    310         mScrollView = scrollView;
    311     }
    312 
    313     private float calculateGlow(float target, float actual) {
    314         // glow if overscale
    315         if (DEBUG_GLOW) Slog.d(TAG, "target: " + target + " actual: " + actual);
    316         float stretch = Math.abs((target - actual) / mMaximumStretch);
    317         float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
    318         if (DEBUG_GLOW) Slog.d(TAG, "stretch: " + stretch + " strength: " + strength);
    319         return (GLOW_BASE + strength * (1f - GLOW_BASE));
    320     }
    321 
    322     public void setGlow(float glow) {
    323         if (!mGlowAnimationSet.isRunning() || glow == 0f) {
    324             if (mGlowAnimationSet.isRunning()) {
    325                 mGlowAnimationSet.end();
    326             }
    327             if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) {
    328                 if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) {
    329                     // animate glow in and out
    330                     mGlowTopAnimation.setTarget(mCurrViewTopGlow);
    331                     mGlowBottomAnimation.setTarget(mCurrViewBottomGlow);
    332                     mGlowTopAnimation.setFloatValues(glow);
    333                     mGlowBottomAnimation.setFloatValues(glow);
    334                     mGlowAnimationSet.setupStartValues();
    335                     mGlowAnimationSet.start();
    336                 } else {
    337                     // set it explicitly in reponse to touches.
    338                     mCurrViewTopGlow.setAlpha(glow);
    339                     mCurrViewBottomGlow.setAlpha(glow);
    340                     handleGlowVisibility();
    341                 }
    342             }
    343         }
    344     }
    345 
    346     private void handleGlowVisibility() {
    347         mCurrViewTopGlow.setVisibility(mCurrViewTopGlow.getAlpha() <= 0.0f ?
    348                 View.INVISIBLE : View.VISIBLE);
    349         mCurrViewBottomGlow.setVisibility(mCurrViewBottomGlow.getAlpha() <= 0.0f ?
    350                 View.INVISIBLE : View.VISIBLE);
    351     }
    352 
    353     @Override
    354     public boolean onInterceptTouchEvent(MotionEvent ev) {
    355         final int action = ev.getAction();
    356         if (DEBUG_SCALE) Slog.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
    357                          " expanding=" + mExpanding +
    358                          (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
    359                          (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
    360                          (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
    361         // check for a spread-finger vertical pull gesture
    362         mSGD.onTouchEvent(ev);
    363         final int x = (int) mSGD.getFocusX();
    364         final int y = (int) mSGD.getFocusY();
    365         if (mExpanding) {
    366             return true;
    367         } else {
    368             if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
    369                 // we've begun Venetian blinds style expansion
    370                 return true;
    371             }
    372             final float xspan = mSGD.getCurrentSpanX();
    373             if ((action == MotionEvent.ACTION_MOVE &&
    374                     xspan > mPullGestureMinXSpan &&
    375                     xspan > mSGD.getCurrentSpanY())) {
    376                 // detect a vertical pulling gesture with fingers somewhat separated
    377                 if (DEBUG_SCALE) Slog.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
    378 
    379                 mInitialTouchFocusY = y;
    380 
    381                 final View underFocus = findView(x, y);
    382                 if (underFocus != null) {
    383                     startExpanding(underFocus, PULL);
    384                 }
    385                 return true;
    386             }
    387             if (mScrollView != null && mScrollView.getScrollY() > 0) {
    388                 return false;
    389             }
    390             // Now look for other gestures
    391             switch (action & MotionEvent.ACTION_MASK) {
    392             case MotionEvent.ACTION_MOVE: {
    393                 if (mWatchingForPull) {
    394                     final int yDiff = y - mLastMotionY;
    395                     if (yDiff > mTouchSlop) {
    396                         if (DEBUG) Slog.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
    397                         mLastMotionY = y;
    398                         final View underFocus = findView(x, y);
    399                         if (underFocus != null) {
    400                             startExpanding(underFocus, BLINDS);
    401                             mInitialTouchY = mLastMotionY;
    402                             mHasPopped = false;
    403                         }
    404                     }
    405                 }
    406                 break;
    407             }
    408 
    409             case MotionEvent.ACTION_DOWN:
    410                 mWatchingForPull = isInside(mScrollView, x, y);
    411                 mLastMotionY = y;
    412                 break;
    413 
    414             case MotionEvent.ACTION_CANCEL:
    415             case MotionEvent.ACTION_UP:
    416                 if (DEBUG) Slog.d(TAG, "up/cancel");
    417                 finishExpanding(false);
    418                 clearView();
    419                 break;
    420             }
    421             return mExpanding;
    422         }
    423     }
    424 
    425     @Override
    426     public boolean onTouchEvent(MotionEvent ev) {
    427         final int action = ev.getAction();
    428         if (DEBUG_SCALE) Slog.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
    429                 " expanding=" + mExpanding +
    430                 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
    431                 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
    432                 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
    433 
    434         mSGD.onTouchEvent(ev);
    435 
    436         switch (action) {
    437             case MotionEvent.ACTION_MOVE: {
    438                 if (0 != (mExpansionStyle & BLINDS)) {
    439                     final float rawHeight = ev.getY() - mInitialTouchY + mOldHeight;
    440                     final float newHeight = clamp(rawHeight);
    441                     final boolean wasClosed = (mOldHeight == mSmallSize);
    442                     boolean isFinished = false;
    443                     if (rawHeight > mNaturalHeight) {
    444                         isFinished = true;
    445                     }
    446                     if (rawHeight < mSmallSize) {
    447                         isFinished = true;
    448                     }
    449 
    450                     final float pull = Math.abs(ev.getY() - mInitialTouchY);
    451                     if (mHasPopped || pull > mPopLimit) {
    452                         if (!mHasPopped) {
    453                             vibrate(mPopDuration);
    454                             mHasPopped = true;
    455                         }
    456                     }
    457 
    458                     if (mHasPopped) {
    459                         mScaler.setHeight(newHeight);
    460                         setGlow(GLOW_BASE);
    461                     } else {
    462                         setGlow(calculateGlow(4f * pull, 0f));
    463                     }
    464 
    465                     final int x = (int) mSGD.getFocusX();
    466                     final int y = (int) mSGD.getFocusY();
    467                     View underFocus = findView(x, y);
    468                     if (isFinished && underFocus != null && underFocus != mCurrView) {
    469                         finishExpanding(false); // @@@ needed?
    470                         startExpanding(underFocus, BLINDS);
    471                         mInitialTouchY = y;
    472                         mHasPopped = false;
    473                     }
    474                     return true;
    475                 }
    476 
    477                 if (mExpanding) {
    478                     updateExpansion();
    479                     return true;
    480                 }
    481 
    482                 break;
    483             }
    484             case MotionEvent.ACTION_UP:
    485             case MotionEvent.ACTION_CANCEL:
    486                 if (DEBUG) Slog.d(TAG, "up/cancel");
    487                 finishExpanding(false);
    488                 clearView();
    489                 break;
    490         }
    491         return true;
    492     }
    493 
    494     private void startExpanding(View v, int expandType) {
    495         mExpanding = true;
    496         mExpansionStyle = expandType;
    497         if (DEBUG) Slog.d(TAG, "scale type " + expandType + " beginning on view: " + v);
    498         mCallback.setUserLockedChild(v, true);
    499         setView(v);
    500         setGlow(GLOW_BASE);
    501         mScaler.setView(v);
    502         mOldHeight = mScaler.getHeight();
    503         if (mCallback.canChildBeExpanded(v)) {
    504             if (DEBUG) Slog.d(TAG, "working on an expandable child");
    505             mNaturalHeight = mScaler.getNaturalHeight(mLargeSize);
    506         } else {
    507             if (DEBUG) Slog.d(TAG, "working on a non-expandable child");
    508             mNaturalHeight = mOldHeight;
    509         }
    510         if (DEBUG) Slog.d(TAG, "got mOldHeight: " + mOldHeight +
    511                     " mNaturalHeight: " + mNaturalHeight);
    512         v.getParent().requestDisallowInterceptTouchEvent(true);
    513     }
    514 
    515     private void finishExpanding(boolean force) {
    516         if (!mExpanding) return;
    517 
    518         float currentHeight = mScaler.getHeight();
    519         float targetHeight = mSmallSize;
    520         float h = mScaler.getHeight();
    521         final boolean wasClosed = (mOldHeight == mSmallSize);
    522         if (wasClosed) {
    523             targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize;
    524         } else {
    525             targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight;
    526         }
    527         if (mScaleAnimation.isRunning()) {
    528             mScaleAnimation.cancel();
    529         }
    530         setGlow(0f);
    531         mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight);
    532         if (targetHeight != currentHeight) {
    533             mScaleAnimation.setFloatValues(targetHeight);
    534             mScaleAnimation.setupStartValues();
    535             mScaleAnimation.start();
    536         }
    537         mCallback.setUserLockedChild(mCurrView, false);
    538 
    539         mExpanding = false;
    540         mExpansionStyle = NONE;
    541 
    542         if (DEBUG) Slog.d(TAG, "scale was finished on view: " + mCurrView);
    543     }
    544 
    545     private void clearView() {
    546         mCurrView = null;
    547         mCurrViewTopGlow = null;
    548         mCurrViewBottomGlow = null;
    549     }
    550 
    551     private void setView(View v) {
    552         mCurrView = v;
    553         if (v instanceof ViewGroup) {
    554             ViewGroup g = (ViewGroup) v;
    555             mCurrViewTopGlow = g.findViewById(R.id.top_glow);
    556             mCurrViewBottomGlow = g.findViewById(R.id.bottom_glow);
    557             if (DEBUG) {
    558                 String debugLog = "Looking for glows: " +
    559                         (mCurrViewTopGlow != null ? "found top " : "didn't find top") +
    560                         (mCurrViewBottomGlow != null ? "found bottom " : "didn't find bottom");
    561                 Slog.v(TAG,  debugLog);
    562             }
    563         }
    564     }
    565 
    566     @Override
    567     public void onClick(View v) {
    568         startExpanding(v, STRETCH);
    569         finishExpanding(true);
    570         clearView();
    571     }
    572 
    573     /**
    574      * Triggers haptic feedback.
    575      */
    576     private synchronized void vibrate(long duration) {
    577         if (mVibrator == null) {
    578             mVibrator = (android.os.Vibrator)
    579                     mContext.getSystemService(Context.VIBRATOR_SERVICE);
    580         }
    581         mVibrator.vibrate(duration);
    582     }
    583 }
    584 
    585