Files
frameworks_base/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
Selim Cinek 816c8e4735 Legacy custom views now get properly layed out again
Since the minheight grew, all notifications grew.
Legacy custom notifications need to stay the same
height though.

Change-Id: I469c448014c3d3c31526466d06a60ac44838b274
2015-11-30 17:45:15 -08:00

599 lines
22 KiB
Java

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.Log;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import com.android.systemui.statusbar.ExpandableNotificationRow;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.policy.ScrollAdapter;
public class ExpandHelper implements Gefingerpoken {
public interface Callback {
ExpandableView getChildAtRawPosition(float x, float y);
ExpandableView getChildAtPosition(float x, float y);
boolean canChildBeExpanded(View v);
void setUserExpandedChild(View v, boolean userExpanded);
void setUserLockedChild(View v, boolean userLocked);
void expansionStateChanged(boolean isExpanding);
}
private static final String TAG = "ExpandHelper";
protected static final boolean DEBUG = false;
protected static final boolean DEBUG_SCALE = false;
private static final float EXPAND_DURATION = 0.3f;
// Set to false to disable focus-based gestures (spread-finger vertical pull).
private static final boolean USE_DRAG = true;
// Set to false to disable scale-based gestures (both horizontal and vertical).
private static final boolean USE_SPAN = true;
// Both gestures types may be active at the same time.
// At least one gesture type should be active.
// A variant of the screwdriver gesture will emerge from either gesture type.
// amount of overstretch for maximum brightness expressed in U
// 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
private static final float STRETCH_INTERVAL = 2f;
@SuppressWarnings("unused")
private Context mContext;
private boolean mExpanding;
private static final int NONE = 0;
private static final int BLINDS = 1<<0;
private static final int PULL = 1<<1;
private static final int STRETCH = 1<<2;
private int mExpansionStyle = NONE;
private boolean mWatchingForPull;
private boolean mHasPopped;
private View mEventSource;
private float mOldHeight;
private float mNaturalHeight;
private float mInitialTouchFocusY;
private float mInitialTouchY;
private float mInitialTouchSpan;
private float mLastFocusY;
private float mLastSpanY;
private int mTouchSlop;
private float mLastMotionY;
private float mPullGestureMinXSpan;
private Callback mCallback;
private ScaleGestureDetector mSGD;
private ViewScaler mScaler;
private ObjectAnimator mScaleAnimation;
private boolean mEnabled = true;
private ExpandableView mResizedView;
private float mCurrentHeight;
private int mSmallSize;
private int mLargeSize;
private float mMaximumStretch;
private boolean mOnlyMovements;
private int mGravity;
private ScrollAdapter mScrollAdapter;
private FlingAnimationUtils mFlingAnimationUtils;
private VelocityTracker mVelocityTracker;
private OnScaleGestureListener mScaleGestureListener
= new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()");
if (!mOnlyMovements) {
startExpanding(mResizedView, STRETCH);
}
return mExpanding;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView);
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
};
private class ViewScaler {
ExpandableView mView;
public ViewScaler() {}
public void setView(ExpandableView v) {
mView = v;
}
public void setHeight(float h) {
if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
mView.setActualHeight((int) h);
mCurrentHeight = h;
}
public float getHeight() {
return mView.getActualHeight();
}
public int getNaturalHeight() {
return mView.getMaxContentHeight();
}
}
/**
* Handle expansion gestures to expand and contract children of the callback.
*
* @param context application context
* @param callback the container that holds the items to be manipulated
* @param small the smallest allowable size for the manuipulated items.
* @param large the largest allowable size for the manuipulated items.
*/
public ExpandHelper(Context context, Callback callback, int small, int large) {
mSmallSize = small;
mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
mLargeSize = large;
mContext = context;
mCallback = callback;
mScaler = new ViewScaler();
mGravity = Gravity.TOP;
mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
final ViewConfiguration configuration = ViewConfiguration.get(mContext);
mTouchSlop = configuration.getScaledTouchSlop();
mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
mFlingAnimationUtils = new FlingAnimationUtils(context, EXPAND_DURATION);
}
private void updateExpansion() {
if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()");
// are we scaling or dragging?
float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
span *= USE_SPAN ? 1f : 0f;
float drag = mSGD.getFocusY() - mInitialTouchFocusY;
drag *= USE_DRAG ? 1f : 0f;
drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
float pull = Math.abs(drag) + Math.abs(span) + 1f;
float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
float target = hand + mOldHeight;
float newHeight = clamp(target);
mScaler.setHeight(newHeight);
mLastFocusY = mSGD.getFocusY();
mLastSpanY = mSGD.getCurrentSpan();
}
private float clamp(float target) {
float out = target;
out = out < mSmallSize ? mSmallSize : out;
out = out > mNaturalHeight ? mNaturalHeight : out;
return out;
}
private ExpandableView findView(float x, float y) {
ExpandableView v;
if (mEventSource != null) {
int[] location = new int[2];
mEventSource.getLocationOnScreen(location);
x += location[0];
y += location[1];
v = mCallback.getChildAtRawPosition(x, y);
} else {
v = mCallback.getChildAtPosition(x, y);
}
return v;
}
private boolean isInside(View v, float x, float y) {
if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")");
if (v == null) {
if (DEBUG) Log.d(TAG, "isinside null subject");
return false;
}
if (mEventSource != null) {
int[] location = new int[2];
mEventSource.getLocationOnScreen(location);
x += location[0];
y += location[1];
if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")");
}
int[] location = new int[2];
v.getLocationOnScreen(location);
x -= location[0];
y -= location[1];
if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")");
if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")");
boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
return inside;
}
public void setEventSource(View eventSource) {
mEventSource = eventSource;
}
public void setGravity(int gravity) {
mGravity = gravity;
}
public void setScrollAdapter(ScrollAdapter adapter) {
mScrollAdapter = adapter;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
return false;
}
trackVelocity(ev);
final int action = ev.getAction();
if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
" expanding=" + mExpanding +
(0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
(0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
(0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
// check for a spread-finger vertical pull gesture
mSGD.onTouchEvent(ev);
final int x = (int) mSGD.getFocusX();
final int y = (int) mSGD.getFocusY();
mInitialTouchFocusY = y;
mInitialTouchSpan = mSGD.getCurrentSpan();
mLastFocusY = mInitialTouchFocusY;
mLastSpanY = mInitialTouchSpan;
if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan);
if (mExpanding) {
mLastMotionY = ev.getRawY();
maybeRecycleVelocityTracker(ev);
return true;
} else {
if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
// we've begun Venetian blinds style expansion
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final float xspan = mSGD.getCurrentSpanX();
if (xspan > mPullGestureMinXSpan &&
xspan > mSGD.getCurrentSpanY() && !mExpanding) {
// detect a vertical pulling gesture with fingers somewhat separated
if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
startExpanding(mResizedView, PULL);
mWatchingForPull = false;
}
if (mWatchingForPull) {
final float yDiff = ev.getRawY() - mInitialTouchY;
if (yDiff > mTouchSlop) {
if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
mWatchingForPull = false;
if (mResizedView != null && !isFullyExpanded(mResizedView)) {
if (startExpanding(mResizedView, BLINDS)) {
mLastMotionY = ev.getRawY();
mInitialTouchY = ev.getRawY();
mHasPopped = false;
}
}
}
}
break;
}
case MotionEvent.ACTION_DOWN:
mWatchingForPull = mScrollAdapter != null &&
isInside(mScrollAdapter.getHostView(), x, y)
&& mScrollAdapter.isScrolledToTop();
mResizedView = findView(x, y);
if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) {
mResizedView = null;
mWatchingForPull = false;
}
mInitialTouchY = ev.getY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (DEBUG) Log.d(TAG, "up/cancel");
finishExpanding(false, getCurrentVelocity());
clearView();
break;
}
mLastMotionY = ev.getRawY();
maybeRecycleVelocityTracker(ev);
return mExpanding;
}
}
private void trackVelocity(MotionEvent event) {
int action = event.getActionMasked();
switch(action) {
case MotionEvent.ACTION_DOWN:
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(event);
break;
case MotionEvent.ACTION_MOVE:
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
break;
default:
break;
}
}
private void maybeRecycleVelocityTracker(MotionEvent event) {
if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|| event.getActionMasked() == MotionEvent.ACTION_UP)) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private float getCurrentVelocity() {
if (mVelocityTracker != null) {
mVelocityTracker.computeCurrentVelocity(1000);
return mVelocityTracker.getYVelocity();
} else {
return 0f;
}
}
public void setEnabled(boolean enable) {
mEnabled = enable;
}
private boolean isEnabled() {
return mEnabled;
}
private boolean isFullyExpanded(ExpandableView underFocus) {
return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight()
&& (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded());
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
return false;
}
trackVelocity(ev);
final int action = ev.getActionMasked();
if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
" expanding=" + mExpanding +
(0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
(0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
(0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
mSGD.onTouchEvent(ev);
final int x = (int) mSGD.getFocusX();
final int y = (int) mSGD.getFocusY();
if (mOnlyMovements) {
mLastMotionY = ev.getRawY();
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mWatchingForPull = mScrollAdapter != null &&
isInside(mScrollAdapter.getHostView(), x, y);
mResizedView = findView(x, y);
mInitialTouchY = ev.getY();
break;
case MotionEvent.ACTION_MOVE: {
if (mWatchingForPull) {
final float yDiff = ev.getRawY() - mInitialTouchY;
if (yDiff > mTouchSlop) {
if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
mWatchingForPull = false;
if (mResizedView != null && !isFullyExpanded(mResizedView)) {
if (startExpanding(mResizedView, BLINDS)) {
mInitialTouchY = ev.getRawY();
mLastMotionY = ev.getRawY();
mHasPopped = false;
}
}
}
}
if (mExpanding && 0 != (mExpansionStyle & BLINDS)) {
final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight;
final float newHeight = clamp(rawHeight);
boolean isFinished = false;
boolean expanded = false;
if (rawHeight > mNaturalHeight) {
isFinished = true;
expanded = true;
}
if (rawHeight < mSmallSize) {
isFinished = true;
expanded = false;
}
if (!mHasPopped) {
if (mEventSource != null) {
mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
mHasPopped = true;
}
mScaler.setHeight(newHeight);
mLastMotionY = ev.getRawY();
if (isFinished) {
mCallback.expansionStateChanged(false);
} else {
mCallback.expansionStateChanged(true);
}
return true;
}
if (mExpanding) {
// Gestural expansion is running
updateExpansion();
mLastMotionY = ev.getRawY();
return true;
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
if (DEBUG) Log.d(TAG, "pointer change");
mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (DEBUG) Log.d(TAG, "up/cancel");
finishExpanding(false, getCurrentVelocity());
clearView();
break;
}
mLastMotionY = ev.getRawY();
maybeRecycleVelocityTracker(ev);
return mResizedView != null;
}
/**
* @return True if the view is expandable, false otherwise.
*/
private boolean startExpanding(ExpandableView v, int expandType) {
if (!(v instanceof ExpandableNotificationRow)) {
return false;
}
mExpansionStyle = expandType;
if (mExpanding && v == mResizedView) {
return true;
}
mExpanding = true;
mCallback.expansionStateChanged(true);
if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
mCallback.setUserLockedChild(v, true);
mScaler.setView(v);
mOldHeight = mScaler.getHeight();
mCurrentHeight = mOldHeight;
boolean canBeExpanded = mCallback.canChildBeExpanded(v);
if (canBeExpanded) {
if (DEBUG) Log.d(TAG, "working on an expandable child");
mNaturalHeight = mScaler.getNaturalHeight();
mSmallSize = v.getMinExpandHeight();
} else {
if (DEBUG) Log.d(TAG, "working on a non-expandable child");
mNaturalHeight = mOldHeight;
}
if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
" mNaturalHeight: " + mNaturalHeight);
return true;
}
private void finishExpanding(boolean force, float velocity) {
if (!mExpanding) return;
if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView);
float currentHeight = mScaler.getHeight();
float h = mScaler.getHeight();
final boolean wasClosed = (mOldHeight == mSmallSize);
boolean nowExpanded;
int naturalHeight = mScaler.getNaturalHeight();
if (wasClosed) {
nowExpanded = (force || currentHeight > mOldHeight);
} else {
nowExpanded = !force && currentHeight >= mOldHeight;
}
nowExpanded |= mNaturalHeight == mSmallSize;
if (mScaleAnimation.isRunning()) {
mScaleAnimation.cancel();
}
mCallback.setUserExpandedChild(mResizedView, nowExpanded);
mCallback.expansionStateChanged(false);
float targetHeight = nowExpanded ? naturalHeight : mSmallSize;
if (targetHeight != currentHeight) {
mScaleAnimation.setFloatValues(targetHeight);
mScaleAnimation.setupStartValues();
final View scaledView = mResizedView;
mScaleAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCallback.setUserLockedChild(scaledView, false);
mScaleAnimation.removeListener(this);
}
});
mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity);
mScaleAnimation.start();
} else {
mCallback.setUserLockedChild(mResizedView, false);
}
mExpanding = false;
mExpansionStyle = NONE;
if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView);
}
private void clearView() {
mResizedView = null;
}
/**
* Use this to abort any pending expansions in progress.
*/
public void cancel() {
finishExpanding(true, 0f /* velocity */);
clearView();
// reset the gesture detector
mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
}
/**
* Change the expansion mode to only observe movements and don't perform any resizing.
* This is needed when the expanding is finished and the scroller kicks in,
* performing an overscroll motion. We only want to shrink it again when we are not
* overscrolled.
*
* @param onlyMovements Should only movements be observed?
*/
public void onlyObserveMovements(boolean onlyMovements) {
mOnlyMovements = onlyMovements;
}
}