ExpandHelper.java revision 4222d9a7fb87d73e1443ec1a2de9782b05741af6
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 18package com.android.systemui; 19 20import android.animation.Animator; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.ObjectAnimator; 23import android.content.Context; 24import android.media.AudioManager; 25import android.os.Vibrator; 26import android.util.Log; 27import android.view.Gravity; 28import android.view.MotionEvent; 29import android.view.ScaleGestureDetector; 30import android.view.ScaleGestureDetector.OnScaleGestureListener; 31import android.view.View; 32import android.view.ViewConfiguration; 33 34import com.android.systemui.statusbar.ExpandableNotificationRow; 35import com.android.systemui.statusbar.ExpandableView; 36import com.android.systemui.statusbar.policy.ScrollAdapter; 37 38public class ExpandHelper implements Gefingerpoken { 39 public interface Callback { 40 ExpandableView getChildAtRawPosition(float x, float y); 41 ExpandableView getChildAtPosition(float x, float y); 42 boolean canChildBeExpanded(View v); 43 void setUserExpandedChild(View v, boolean userExpanded); 44 void setUserLockedChild(View v, boolean userLocked); 45 } 46 47 private static final String TAG = "ExpandHelper"; 48 protected static final boolean DEBUG = false; 49 protected static final boolean DEBUG_SCALE = false; 50 private static final long EXPAND_DURATION = 250; 51 52 // Set to false to disable focus-based gestures (spread-finger vertical pull). 53 private static final boolean USE_DRAG = true; 54 // Set to false to disable scale-based gestures (both horizontal and vertical). 55 private static final boolean USE_SPAN = true; 56 // Both gestures types may be active at the same time. 57 // At least one gesture type should be active. 58 // A variant of the screwdriver gesture will emerge from either gesture type. 59 60 // amount of overstretch for maximum brightness expressed in U 61 // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U 62 private static final float STRETCH_INTERVAL = 2f; 63 64 // level of glow for a touch, without overstretch 65 // overstretch fills the range (GLOW_BASE, 1.0] 66 private static final float GLOW_BASE = 0.5f; 67 68 @SuppressWarnings("unused") 69 private Context mContext; 70 71 private boolean mExpanding; 72 private static final int NONE = 0; 73 private static final int BLINDS = 1<<0; 74 private static final int PULL = 1<<1; 75 private static final int STRETCH = 1<<2; 76 private int mExpansionStyle = NONE; 77 private boolean mWatchingForPull; 78 private boolean mHasPopped; 79 private View mEventSource; 80 private View mCurrView; 81 private float mOldHeight; 82 private float mNaturalHeight; 83 private float mInitialTouchFocusY; 84 private float mInitialTouchY; 85 private float mInitialTouchSpan; 86 private float mLastFocusY; 87 private float mLastSpanY; 88 private int mTouchSlop; 89 private int mLastMotionY; 90 private float mPopLimit; 91 private int mPopDuration; 92 private float mPullGestureMinXSpan; 93 private Callback mCallback; 94 private ScaleGestureDetector mSGD; 95 private ViewScaler mScaler; 96 private ObjectAnimator mScaleAnimation; 97 private Vibrator mVibrator; 98 99 private int mSmallSize; 100 private int mLargeSize; 101 private float mMaximumStretch; 102 103 private int mGravity; 104 105 private ScrollAdapter mScrollAdapter; 106 107 private OnScaleGestureListener mScaleGestureListener 108 = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 109 @Override 110 public boolean onScaleBegin(ScaleGestureDetector detector) { 111 if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()"); 112 float focusX = detector.getFocusX(); 113 float focusY = detector.getFocusY(); 114 115 final ExpandableView underFocus = findView(focusX, focusY); 116 startExpanding(underFocus, STRETCH); 117 return mExpanding; 118 } 119 120 @Override 121 public boolean onScale(ScaleGestureDetector detector) { 122 if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mCurrView); 123 return true; 124 } 125 126 @Override 127 public void onScaleEnd(ScaleGestureDetector detector) { 128 } 129 }; 130 131 private class ViewScaler { 132 ExpandableView mView; 133 134 public ViewScaler() {} 135 public void setView(ExpandableView v) { 136 mView = v; 137 } 138 public void setHeight(float h) { 139 if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); 140 mView.setActualHeight((int) h); 141 } 142 public float getHeight() { 143 return mView.getActualHeight(); 144 } 145 public int getNaturalHeight(int maximum) { 146 return Math.min(maximum, mView.getMaxHeight()); 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 */ 158 public ExpandHelper(Context context, Callback callback, int small, int large) { 159 mSmallSize = small; 160 mMaximumStretch = mSmallSize * STRETCH_INTERVAL; 161 mLargeSize = large; 162 mContext = context; 163 mCallback = callback; 164 mScaler = new ViewScaler(); 165 mGravity = Gravity.TOP; 166 mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f); 167 mScaleAnimation.setDuration(EXPAND_DURATION); 168 mPopLimit = mContext.getResources().getDimension(R.dimen.blinds_pop_threshold); 169 mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms); 170 mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min); 171 172 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 173 mTouchSlop = configuration.getScaledTouchSlop(); 174 175 mSGD = new ScaleGestureDetector(context, mScaleGestureListener); 176 } 177 178 private void updateExpansion() { 179 if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()"); 180 // are we scaling or dragging? 181 float span = mSGD.getCurrentSpan() - mInitialTouchSpan; 182 span *= USE_SPAN ? 1f : 0f; 183 float drag = mSGD.getFocusY() - mInitialTouchFocusY; 184 drag *= USE_DRAG ? 1f : 0f; 185 drag *= mGravity == Gravity.BOTTOM ? -1f : 1f; 186 float pull = Math.abs(drag) + Math.abs(span) + 1f; 187 float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull; 188 float target = hand + mOldHeight; 189 float newHeight = clamp(target); 190 mScaler.setHeight(newHeight); 191 192 mLastFocusY = mSGD.getFocusY(); 193 mLastSpanY = mSGD.getCurrentSpan(); 194 } 195 196 private float clamp(float target) { 197 float out = target; 198 out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out); 199 out = out > mNaturalHeight ? mNaturalHeight : out; 200 return out; 201 } 202 203 private ExpandableView findView(float x, float y) { 204 ExpandableView v; 205 if (mEventSource != null) { 206 int[] location = new int[2]; 207 mEventSource.getLocationOnScreen(location); 208 x += location[0]; 209 y += location[1]; 210 v = mCallback.getChildAtRawPosition(x, y); 211 } else { 212 v = mCallback.getChildAtPosition(x, y); 213 } 214 return v; 215 } 216 217 private boolean isInside(View v, float x, float y) { 218 if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")"); 219 220 if (v == null) { 221 if (DEBUG) Log.d(TAG, "isinside null subject"); 222 return false; 223 } 224 if (mEventSource != null) { 225 int[] location = new int[2]; 226 mEventSource.getLocationOnScreen(location); 227 x += location[0]; 228 y += location[1]; 229 if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")"); 230 } 231 int[] location = new int[2]; 232 v.getLocationOnScreen(location); 233 x -= location[0]; 234 y -= location[1]; 235 if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")"); 236 if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")"); 237 boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight()); 238 return inside; 239 } 240 241 public void setEventSource(View eventSource) { 242 mEventSource = eventSource; 243 } 244 245 public void setGravity(int gravity) { 246 mGravity = gravity; 247 } 248 249 public void setScrollAdapter(ScrollAdapter adapter) { 250 mScrollAdapter = adapter; 251 } 252 253 @Override 254 public boolean onInterceptTouchEvent(MotionEvent ev) { 255 final int action = ev.getAction(); 256 if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) + 257 " expanding=" + mExpanding + 258 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 259 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 260 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 261 // check for a spread-finger vertical pull gesture 262 mSGD.onTouchEvent(ev); 263 final int x = (int) mSGD.getFocusX(); 264 final int y = (int) mSGD.getFocusY(); 265 266 mInitialTouchFocusY = y; 267 mInitialTouchSpan = mSGD.getCurrentSpan(); 268 mLastFocusY = mInitialTouchFocusY; 269 mLastSpanY = mInitialTouchSpan; 270 if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan); 271 272 if (mExpanding) { 273 return true; 274 } else { 275 if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) { 276 // we've begun Venetian blinds style expansion 277 return true; 278 } 279 final float xspan = mSGD.getCurrentSpanX(); 280 if ((action == MotionEvent.ACTION_MOVE && 281 xspan > mPullGestureMinXSpan && 282 xspan > mSGD.getCurrentSpanY())) { 283 // detect a vertical pulling gesture with fingers somewhat separated 284 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)"); 285 286 final ExpandableView underFocus = findView(x, y); 287 startExpanding(underFocus, PULL); 288 return true; 289 } 290 if (mScrollAdapter != null && !mScrollAdapter.isScrolledToTop()) { 291 return false; 292 } 293 // Now look for other gestures 294 switch (action & MotionEvent.ACTION_MASK) { 295 case MotionEvent.ACTION_MOVE: { 296 if (mWatchingForPull) { 297 final int yDiff = y - mLastMotionY; 298 if (yDiff > mTouchSlop) { 299 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 300 mLastMotionY = y; 301 final ExpandableView underFocus = findView(x, y); 302 if (startExpanding(underFocus, BLINDS)) { 303 mInitialTouchY = mLastMotionY; 304 mHasPopped = false; 305 } 306 } 307 } 308 break; 309 } 310 311 case MotionEvent.ACTION_DOWN: 312 mWatchingForPull = mScrollAdapter != null && 313 isInside(mScrollAdapter.getHostView(), x, y); 314 mLastMotionY = y; 315 break; 316 317 case MotionEvent.ACTION_CANCEL: 318 case MotionEvent.ACTION_UP: 319 if (DEBUG) Log.d(TAG, "up/cancel"); 320 finishExpanding(false); 321 clearView(); 322 break; 323 } 324 return mExpanding; 325 } 326 } 327 328 @Override 329 public boolean onTouchEvent(MotionEvent ev) { 330 final int action = ev.getActionMasked(); 331 if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) + 332 " expanding=" + mExpanding + 333 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 334 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 335 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 336 337 mSGD.onTouchEvent(ev); 338 339 switch (action) { 340 case MotionEvent.ACTION_MOVE: { 341 if (0 != (mExpansionStyle & BLINDS)) { 342 final float rawHeight = ev.getY() - mInitialTouchY + mOldHeight; 343 final float newHeight = clamp(rawHeight); 344 final boolean wasClosed = (mOldHeight == mSmallSize); 345 boolean isFinished = false; 346 if (rawHeight > mNaturalHeight) { 347 isFinished = true; 348 } 349 if (rawHeight < mSmallSize) { 350 isFinished = true; 351 } 352 353 final float pull = Math.abs(ev.getY() - mInitialTouchY); 354 if (mHasPopped || pull > mPopLimit) { 355 if (!mHasPopped) { 356 vibrate(mPopDuration); 357 mHasPopped = true; 358 } 359 } 360 361 if (mHasPopped) { 362 mScaler.setHeight(newHeight); 363 } 364 365 final int x = (int) mSGD.getFocusX(); 366 final int y = (int) mSGD.getFocusY(); 367 ExpandableView underFocus = findView(x, y); 368 if (isFinished && underFocus != null && underFocus != mCurrView) { 369 finishExpanding(false); // @@@ needed? 370 startExpanding(underFocus, BLINDS); 371 mInitialTouchY = y; 372 mHasPopped = false; 373 } 374 return true; 375 } 376 377 if (mExpanding) { 378 updateExpansion(); 379 return true; 380 } 381 382 break; 383 } 384 385 case MotionEvent.ACTION_POINTER_UP: 386 case MotionEvent.ACTION_POINTER_DOWN: 387 if (DEBUG) Log.d(TAG, "pointer change"); 388 mInitialTouchY += mSGD.getFocusY() - mLastFocusY; 389 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY; 390 break; 391 392 case MotionEvent.ACTION_UP: 393 case MotionEvent.ACTION_CANCEL: 394 if (DEBUG) Log.d(TAG, "up/cancel"); 395 finishExpanding(false); 396 clearView(); 397 break; 398 } 399 return true; 400 } 401 402 /** 403 * @return True if the view is expandable, false otherwise. 404 */ 405 private boolean startExpanding(ExpandableView v, int expandType) { 406 if (!(v instanceof ExpandableNotificationRow)) { 407 return false; 408 } 409 mExpansionStyle = expandType; 410 if (mExpanding && v == mCurrView) { 411 return true; 412 } 413 mExpanding = true; 414 if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v); 415 mCallback.setUserLockedChild(v, true); 416 setView(v); 417 mScaler.setView((ExpandableView) v); 418 mOldHeight = mScaler.getHeight(); 419 if (mCallback.canChildBeExpanded(v)) { 420 if (DEBUG) Log.d(TAG, "working on an expandable child"); 421 mNaturalHeight = mScaler.getNaturalHeight(mLargeSize); 422 } else { 423 if (DEBUG) Log.d(TAG, "working on a non-expandable child"); 424 mNaturalHeight = mOldHeight; 425 } 426 if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight + 427 " mNaturalHeight: " + mNaturalHeight); 428 v.getParent().requestDisallowInterceptTouchEvent(true); 429 return true; 430 } 431 432 private void finishExpanding(boolean force) { 433 if (!mExpanding) return; 434 435 if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mCurrView); 436 437 float currentHeight = mScaler.getHeight(); 438 float targetHeight = mSmallSize; 439 float h = mScaler.getHeight(); 440 final boolean wasClosed = (mOldHeight == mSmallSize); 441 if (wasClosed) { 442 targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize; 443 } else { 444 targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight; 445 } 446 if (mScaleAnimation.isRunning()) { 447 mScaleAnimation.cancel(); 448 } 449 mCallback.setUserExpandedChild(mCurrView, targetHeight == mNaturalHeight); 450 if (targetHeight != currentHeight) { 451 mScaleAnimation.setFloatValues(targetHeight); 452 mScaleAnimation.setupStartValues(); 453 final View scaledView = mCurrView; 454 mScaleAnimation.addListener(new AnimatorListenerAdapter() { 455 @Override 456 public void onAnimationEnd(Animator animation) { 457 mCallback.setUserLockedChild(scaledView, false); 458 mScaleAnimation.removeListener(this); 459 } 460 }); 461 mScaleAnimation.start(); 462 } else { 463 mCallback.setUserLockedChild(mCurrView, false); 464 } 465 466 mExpanding = false; 467 mExpansionStyle = NONE; 468 469 if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed); 470 if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight); 471 if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize); 472 if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight); 473 if (DEBUG) Log.d(TAG, "scale was finished on view: " + mCurrView); 474 } 475 476 private void clearView() { 477 mCurrView = null; 478 479 } 480 481 private void setView(View v) { 482 mCurrView = v; 483 } 484 485 /** 486 * Use this to abort any pending expansions in progress. 487 */ 488 public void cancel() { 489 finishExpanding(true); 490 clearView(); 491 492 // reset the gesture detector 493 mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener); 494 } 495 496 /** 497 * Triggers haptic feedback. 498 */ 499 private synchronized void vibrate(long duration) { 500 if (mVibrator == null) { 501 mVibrator = (android.os.Vibrator) 502 mContext.getSystemService(Context.VIBRATOR_SERVICE); 503 } 504 mVibrator.vibrate(duration, AudioManager.STREAM_SYSTEM); 505 } 506} 507 508