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