16a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler/* 26a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * Copyright (C) 2012 The Android Open Source Project 36a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * 46a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * Licensed under the Apache License, Version 2.0 (the "License"); 56a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * you may not use this file except in compliance with the License. 66a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * You may obtain a copy of the License at 76a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * 86a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * http://www.apache.org/licenses/LICENSE-2.0 96a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * 106a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * Unless required by applicable law or agreed to in writing, software 116a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * distributed under the License is distributed on an "AS IS" BASIS, 126a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * See the License for the specific language governing permissions and 146a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler * limitations under the License. 156a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler */ 166a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 176a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 186a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandlerpackage com.android.systemui; 196a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 208900e631940fdffe7b941b56dc0f17e55345441eRomain Guyimport android.animation.Animator; 218900e631940fdffe7b941b56dc0f17e55345441eRomain Guyimport android.animation.AnimatorListenerAdapter; 226a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandlerimport android.animation.ObjectAnimator; 236a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandlerimport android.content.Context; 24cd686b5b6d4166b510df8e32138479a9559bc117John Spurlockimport android.util.Log; 259b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wrenimport android.view.Gravity; 269ff1510aeba910c22b2e31c29a93b08281feae4fJorim Jaggiimport android.view.HapticFeedbackConstants; 276a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandlerimport android.view.MotionEvent; 286a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandlerimport android.view.ScaleGestureDetector; 29ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandlerimport android.view.ScaleGestureDetector.OnScaleGestureListener; 3028c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggiimport android.view.VelocityTracker; 316a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandlerimport android.view.View; 32b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wrenimport android.view.ViewConfiguration; 336a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 343acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinekimport com.android.internal.annotations.VisibleForTesting; 35be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggiimport com.android.systemui.statusbar.ExpandableNotificationRow; 364222d9a7fb87d73e1443ec1a2de9782b05741af6Jorim Jaggiimport com.android.systemui.statusbar.ExpandableView; 3728c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggiimport com.android.systemui.statusbar.FlingAnimationUtils; 38b6d85ebfe4f9f5d3b7d7ab7b6123af02a0deb516Selim Cinekimport com.android.systemui.statusbar.policy.ScrollAdapter; 39b6d85ebfe4f9f5d3b7d7ab7b6123af02a0deb516Selim Cinek 404222d9a7fb87d73e1443ec1a2de9782b05741af6Jorim Jaggipublic class ExpandHelper implements Gefingerpoken { 416a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public interface Callback { 424222d9a7fb87d73e1443ec1a2de9782b05741af6Jorim Jaggi ExpandableView getChildAtRawPosition(float x, float y); 434222d9a7fb87d73e1443ec1a2de9782b05741af6Jorim Jaggi ExpandableView getChildAtPosition(float x, float y); 4480a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren boolean canChildBeExpanded(View v); 4551c7510e493680b4aca1ed7695b35c52d2cd63ffChris Wren void setUserExpandedChild(View v, boolean userExpanded); 4651c7510e493680b4aca1ed7695b35c52d2cd63ffChris Wren void setUserLockedChild(View v, boolean userLocked); 471408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek void expansionStateChanged(boolean isExpanding); 481b2a05eb00746756c4d37ad76d597d909019e56fSelim Cinek int getMaxExpandHeight(ExpandableView view); 49b0a824687f56b6950338aad169d8d837f8ed657bMady Mellor void setExpansionCancelled(View view); 506a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 516a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 526a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private static final String TAG = "ExpandHelper"; 53e46647d28467ee9e88aafe2951a5736f494235daChris Wren protected static final boolean DEBUG = false; 543c148f106f6625ce247a2c7211682c3a1df89bc9Chris Wren protected static final boolean DEBUG_SCALE = false; 5528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi private static final float EXPAND_DURATION = 0.3f; 56ba925e8ecd9decff5701001a0190042d6797942dChris Wren 574377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler // Set to false to disable focus-based gestures (spread-finger vertical pull). 5889139d74b27305a29ca082c75d94dcbed5f84625Chris Wren private static final boolean USE_DRAG = true; 5989139d74b27305a29ca082c75d94dcbed5f84625Chris Wren // Set to false to disable scale-based gestures (both horizontal and vertical). 6089139d74b27305a29ca082c75d94dcbed5f84625Chris Wren private static final boolean USE_SPAN = true; 6189139d74b27305a29ca082c75d94dcbed5f84625Chris Wren // Both gestures types may be active at the same time. 6289139d74b27305a29ca082c75d94dcbed5f84625Chris Wren // At least one gesture type should be active. 6389139d74b27305a29ca082c75d94dcbed5f84625Chris Wren // A variant of the screwdriver gesture will emerge from either gesture type. 646a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 6580a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren // amount of overstretch for maximum brightness expressed in U 6680a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U 6780a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren private static final float STRETCH_INTERVAL = 2f; 6880a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren 696a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler @SuppressWarnings("unused") 706a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private Context mContext; 716a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 724377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private boolean mExpanding; 734377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private static final int NONE = 0; 744377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private static final int BLINDS = 1<<0; 754377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private static final int PULL = 1<<1; 764377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private static final int STRETCH = 1<<2; 774377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private int mExpansionStyle = NONE; 78b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren private boolean mWatchingForPull; 79b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren private boolean mHasPopped; 805de6e94e36e2adbdd4ebfb5c1903c23c9ea3c388Chris Wren private View mEventSource; 816a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private float mOldHeight; 826a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private float mNaturalHeight; 8389139d74b27305a29ca082c75d94dcbed5f84625Chris Wren private float mInitialTouchFocusY; 84821d6a773554565be391b22a050288260cf209d5Selim Cinek private float mInitialTouchX; 85b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren private float mInitialTouchY; 866a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private float mInitialTouchSpan; 87cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren private float mLastFocusY; 88cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren private float mLastSpanY; 89b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren private int mTouchSlop; 901408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private float mLastMotionY; 914377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private float mPullGestureMinXSpan; 926a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private Callback mCallback; 934377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler private ScaleGestureDetector mSGD; 946a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private ViewScaler mScaler; 95ba925e8ecd9decff5701001a0190042d6797942dChris Wren private ObjectAnimator mScaleAnimation; 961408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private boolean mEnabled = true; 971408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private ExpandableView mResizedView; 981408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private float mCurrentHeight; 996a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 1006a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private int mSmallSize; 1016a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private int mLargeSize; 10280a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren private float mMaximumStretch; 1031408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private boolean mOnlyMovements; 1046a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 1059b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren private int mGravity; 1069b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren 107fab078b01fbad026f006744016272327f7ab116bSelim Cinek private ScrollAdapter mScrollAdapter; 10828c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi private FlingAnimationUtils mFlingAnimationUtils; 10928c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi private VelocityTracker mVelocityTracker; 110b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 111209bede6b9edb9171e5bee4077b48e35004a37b4John Spurlock private OnScaleGestureListener mScaleGestureListener 112ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 113ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler @Override 114ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler public boolean onScaleBegin(ScaleGestureDetector detector) { 115cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()"); 116ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler 1175e8e1c63774da8c013fad18a63eac691cdc9646fJorim Jaggi if (!mOnlyMovements) { 1185e8e1c63774da8c013fad18a63eac691cdc9646fJorim Jaggi startExpanding(mResizedView, STRETCH); 1195e8e1c63774da8c013fad18a63eac691cdc9646fJorim Jaggi } 120ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler return mExpanding; 121ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler } 122ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler 123ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler @Override 124ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler public boolean onScale(ScaleGestureDetector detector) { 1251408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView); 126ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler return true; 127ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler } 128ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler 129ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler @Override 130ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler public void onScaleEnd(ScaleGestureDetector detector) { 131ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler } 132ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler }; 133ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler 1343acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek @VisibleForTesting 1353acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek ObjectAnimator getScaleAnimation() { 1363acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek return mScaleAnimation; 1373acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek } 1383acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek 1396a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler private class ViewScaler { 140be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi ExpandableView mView; 1419b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren 1426a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public ViewScaler() {} 143be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi public void setView(ExpandableView v) { 1446a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler mView = v; 1456a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 1466a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public void setHeight(float h) { 147cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); 148eef842851026a90b6a217d8bc423454fa48df4feSelim Cinek mView.setActualHeight((int) h); 1491408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCurrentHeight = h; 1506a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 1516a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public float getHeight() { 152eef842851026a90b6a217d8bc423454fa48df4feSelim Cinek return mView.getActualHeight(); 1536a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 154388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek public int getNaturalHeight() { 1551b2a05eb00746756c4d37ad76d597d909019e56fSelim Cinek return mCallback.getMaxExpandHeight(mView); 1566a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 1576a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 1586a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 1599b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren /** 1609b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren * Handle expansion gestures to expand and contract children of the callback. 1619b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren * 1629b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren * @param context application context 1639b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren * @param callback the container that holds the items to be manipulated 1649b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren * @param small the smallest allowable size for the manuipulated items. 1659b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren * @param large the largest allowable size for the manuipulated items. 1669b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren */ 1676a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public ExpandHelper(Context context, Callback callback, int small, int large) { 1686a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler mSmallSize = small; 16980a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren mMaximumStretch = mSmallSize * STRETCH_INTERVAL; 1706a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler mLargeSize = large; 1716a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler mContext = context; 1726a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler mCallback = callback; 1736a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler mScaler = new ViewScaler(); 1749b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren mGravity = Gravity.TOP; 175ba925e8ecd9decff5701001a0190042d6797942dChris Wren mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f); 1764377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min); 177ba925e8ecd9decff5701001a0190042d6797942dChris Wren 178b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren final ViewConfiguration configuration = ViewConfiguration.get(mContext); 179b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren mTouchSlop = configuration.getScaledTouchSlop(); 180b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 181ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler mSGD = new ScaleGestureDetector(context, mScaleGestureListener); 18228c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mFlingAnimationUtils = new FlingAnimationUtils(context, EXPAND_DURATION); 1836a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 1845de6e94e36e2adbdd4ebfb5c1903c23c9ea3c388Chris Wren 1853acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek @VisibleForTesting 1863acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek void updateExpansion() { 187cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()"); 1884377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler // are we scaling or dragging? 189cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren float span = mSGD.getCurrentSpan() - mInitialTouchSpan; 1904377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler span *= USE_SPAN ? 1f : 0f; 1914377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler float drag = mSGD.getFocusY() - mInitialTouchFocusY; 1924377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler drag *= USE_DRAG ? 1f : 0f; 1934377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler drag *= mGravity == Gravity.BOTTOM ? -1f : 1f; 1944377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler float pull = Math.abs(drag) + Math.abs(span) + 1f; 1954377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull; 1964377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler float target = hand + mOldHeight; 1974377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler float newHeight = clamp(target); 1984377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mScaler.setHeight(newHeight); 199cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mLastFocusY = mSGD.getFocusY(); 200cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mLastSpanY = mSGD.getCurrentSpan(); 2014377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler } 2024377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 203b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren private float clamp(float target) { 204b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren float out = target; 205388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek out = out < mSmallSize ? mSmallSize : out; 206b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren out = out > mNaturalHeight ? mNaturalHeight : out; 207b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren return out; 208b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 209b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 2104222d9a7fb87d73e1443ec1a2de9782b05741af6Jorim Jaggi private ExpandableView findView(float x, float y) { 2114222d9a7fb87d73e1443ec1a2de9782b05741af6Jorim Jaggi ExpandableView v; 212b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren if (mEventSource != null) { 213b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren int[] location = new int[2]; 214b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren mEventSource.getLocationOnScreen(location); 2154377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler x += location[0]; 2164377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler y += location[1]; 217b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren v = mCallback.getChildAtRawPosition(x, y); 218b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } else { 219b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren v = mCallback.getChildAtPosition(x, y); 220b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 221b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren return v; 222b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 223b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 224b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren private boolean isInside(View v, float x, float y) { 225cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")"); 226b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 227b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren if (v == null) { 228cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "isinside null subject"); 229b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren return false; 230b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 231b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren if (mEventSource != null) { 232b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren int[] location = new int[2]; 233b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren mEventSource.getLocationOnScreen(location); 2344377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler x += location[0]; 2354377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler y += location[1]; 236cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")"); 237b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 238b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren int[] location = new int[2]; 239b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren v.getLocationOnScreen(location); 2404377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler x -= location[0]; 2414377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler y -= location[1]; 242cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")"); 243cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")"); 244b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight()); 245b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren return inside; 246b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 247b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 2485de6e94e36e2adbdd4ebfb5c1903c23c9ea3c388Chris Wren public void setEventSource(View eventSource) { 2495de6e94e36e2adbdd4ebfb5c1903c23c9ea3c388Chris Wren mEventSource = eventSource; 2505de6e94e36e2adbdd4ebfb5c1903c23c9ea3c388Chris Wren } 2515de6e94e36e2adbdd4ebfb5c1903c23c9ea3c388Chris Wren 2529b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren public void setGravity(int gravity) { 2539b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren mGravity = gravity; 2549b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren } 2559b2cd15f0fed990f532f35590c2a2896b90dc7fcChris Wren 256fab078b01fbad026f006744016272327f7ab116bSelim Cinek public void setScrollAdapter(ScrollAdapter adapter) { 257fab078b01fbad026f006744016272327f7ab116bSelim Cinek mScrollAdapter = adapter; 258b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 259b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 2604377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler @Override 2616a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public boolean onInterceptTouchEvent(MotionEvent ev) { 2621408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (!isEnabled()) { 2631408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek return false; 2641408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 26528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi trackVelocity(ev); 2664377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler final int action = ev.getAction(); 267cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) + 2684377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler " expanding=" + mExpanding + 2694377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 2704377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 2714377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 2724377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler // check for a spread-finger vertical pull gesture 2734377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mSGD.onTouchEvent(ev); 2744377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler final int x = (int) mSGD.getFocusX(); 2754377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler final int y = (int) mSGD.getFocusY(); 276cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren 277cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mInitialTouchFocusY = y; 278cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mInitialTouchSpan = mSGD.getCurrentSpan(); 279cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mLastFocusY = mInitialTouchFocusY; 280cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mLastSpanY = mInitialTouchSpan; 281cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan); 282cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren 2834377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler if (mExpanding) { 2841408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 28528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi maybeRecycleVelocityTracker(ev); 286b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren return true; 287b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } else { 2884377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) { 2894377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler // we've begun Venetian blinds style expansion 2904377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler return true; 2914377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler } 292b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren switch (action & MotionEvent.ACTION_MASK) { 293b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren case MotionEvent.ACTION_MOVE: { 2941408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final float xspan = mSGD.getCurrentSpanX(); 2951408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (xspan > mPullGestureMinXSpan && 2961408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek xspan > mSGD.getCurrentSpanY() && !mExpanding) { 2971408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek // detect a vertical pulling gesture with fingers somewhat separated 2981408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)"); 2991408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek startExpanding(mResizedView, PULL); 3001408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mWatchingForPull = false; 3011408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 302b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren if (mWatchingForPull) { 3031408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final float yDiff = ev.getRawY() - mInitialTouchY; 304821d6a773554565be391b22a050288260cf209d5Selim Cinek final float xDiff = ev.getRawX() - mInitialTouchX; 305821d6a773554565be391b22a050288260cf209d5Selim Cinek if (yDiff > mTouchSlop && yDiff > Math.abs(xDiff)) { 306cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 3071408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mWatchingForPull = false; 3081408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (mResizedView != null && !isFullyExpanded(mResizedView)) { 3091408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (startExpanding(mResizedView, BLINDS)) { 3101408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 3111408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mInitialTouchY = ev.getRawY(); 3121408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mHasPopped = false; 3131408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 314b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 315b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 316b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 317b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren break; 318b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 319b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 320b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren case MotionEvent.ACTION_DOWN: 321de3b1a2b2d3056cb7caa5cdaac51fc9d80e1e3c5Chris Wren mWatchingForPull = mScrollAdapter != null && 3221408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek isInside(mScrollAdapter.getHostView(), x, y) 3231408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek && mScrollAdapter.isScrolledToTop(); 3241408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mResizedView = findView(x, y); 325e53e6bbb82b411f99083e4a6d2071fde45d68d53Selim Cinek if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) { 326d2281151ee18f5fd530bc062c59b3f77082317bfSelim Cinek mResizedView = null; 327d2281151ee18f5fd530bc062c59b3f77082317bfSelim Cinek mWatchingForPull = false; 328d2281151ee18f5fd530bc062c59b3f77082317bfSelim Cinek } 329fe090658b691f8a960bf0492371ec8b52ed4430fSelim Cinek mInitialTouchY = ev.getRawY(); 330fe090658b691f8a960bf0492371ec8b52ed4430fSelim Cinek mInitialTouchX = ev.getRawX(); 331b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren break; 332b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 333b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren case MotionEvent.ACTION_CANCEL: 334b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren case MotionEvent.ACTION_UP: 335cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "up/cancel"); 336c25af40002eb9de4e30b801b35e9e5b2474aafdfSelim Cinek finishExpanding(ev.getActionMasked() == MotionEvent.ACTION_CANCEL /* forceAbort */, 337740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek getCurrentVelocity()); 3384377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler clearView(); 339b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren break; 340b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 3411408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 34228c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi maybeRecycleVelocityTracker(ev); 3434377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler return mExpanding; 344b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 3456a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 3466a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 34728c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi private void trackVelocity(MotionEvent event) { 34828c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi int action = event.getActionMasked(); 34928c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi switch(action) { 35028c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi case MotionEvent.ACTION_DOWN: 35128c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi if (mVelocityTracker == null) { 35228c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker = VelocityTracker.obtain(); 35328c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } else { 35428c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker.clear(); 35528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 35628c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker.addMovement(event); 35728c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi break; 35828c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi case MotionEvent.ACTION_MOVE: 3591b7f51ebc66fbb2cbe8a468790bbbfd397d66964Selim Cinek if (mVelocityTracker == null) { 3601b7f51ebc66fbb2cbe8a468790bbbfd397d66964Selim Cinek mVelocityTracker = VelocityTracker.obtain(); 3611b7f51ebc66fbb2cbe8a468790bbbfd397d66964Selim Cinek } 36228c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker.addMovement(event); 36328c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi break; 36428c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi default: 36528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi break; 36628c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 36728c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 36828c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi 36928c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi private void maybeRecycleVelocityTracker(MotionEvent event) { 370c503896a4d0cab029bca56cf7ac18ae182729a0aJorim Jaggi if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL 371c503896a4d0cab029bca56cf7ac18ae182729a0aJorim Jaggi || event.getActionMasked() == MotionEvent.ACTION_UP)) { 37228c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker.recycle(); 37328c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker = null; 37428c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 37528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 37628c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi 37728c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi private float getCurrentVelocity() { 37828c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi if (mVelocityTracker != null) { 37928c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mVelocityTracker.computeCurrentVelocity(1000); 38028c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi return mVelocityTracker.getYVelocity(); 38128c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } else { 38228c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi return 0f; 38328c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 38428c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi } 38528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi 3861408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek public void setEnabled(boolean enable) { 3871408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mEnabled = enable; 3881408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 3891408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek 3901408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private boolean isEnabled() { 3911408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek return mEnabled; 3921408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 3931408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek 3941408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek private boolean isFullyExpanded(ExpandableView underFocus) { 395388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight() 396388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded()); 3971408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 3981408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek 3994377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler @Override 4006a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler public boolean onTouchEvent(MotionEvent ev) { 401740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek if (!isEnabled() && !mExpanding) { 402740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek // In case we're expanding we still want to finish the current motion. 4031408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek return false; 4041408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 40528c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi trackVelocity(ev); 406cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren final int action = ev.getActionMasked(); 407cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) + 4084377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler " expanding=" + mExpanding + 4094377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 4104377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 4114377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 4124377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 4134377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mSGD.onTouchEvent(ev); 4141408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final int x = (int) mSGD.getFocusX(); 4151408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final int y = (int) mSGD.getFocusY(); 4164377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 4171408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (mOnlyMovements) { 4181408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 4191408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek return false; 4201408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 4216a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler switch (action) { 4221408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek case MotionEvent.ACTION_DOWN: 4231408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mWatchingForPull = mScrollAdapter != null && 4241408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek isInside(mScrollAdapter.getHostView(), x, y); 4251408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mResizedView = findView(x, y); 426fe090658b691f8a960bf0492371ec8b52ed4430fSelim Cinek mInitialTouchX = ev.getRawX(); 427fe090658b691f8a960bf0492371ec8b52ed4430fSelim Cinek mInitialTouchY = ev.getRawY(); 4281408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek break; 429b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren case MotionEvent.ACTION_MOVE: { 4301408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (mWatchingForPull) { 4311408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final float yDiff = ev.getRawY() - mInitialTouchY; 432821d6a773554565be391b22a050288260cf209d5Selim Cinek final float xDiff = ev.getRawX() - mInitialTouchX; 433821d6a773554565be391b22a050288260cf209d5Selim Cinek if (yDiff > mTouchSlop && yDiff > Math.abs(xDiff)) { 4341408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 4351408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mWatchingForPull = false; 4361408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (mResizedView != null && !isFullyExpanded(mResizedView)) { 4371408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (startExpanding(mResizedView, BLINDS)) { 4381408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mInitialTouchY = ev.getRawY(); 4391408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 4401408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mHasPopped = false; 4411408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 4421408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 4431408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 4441408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 4451408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (mExpanding && 0 != (mExpansionStyle & BLINDS)) { 4461408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight; 44786d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren final float newHeight = clamp(rawHeight); 44886d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren boolean isFinished = false; 4491408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek boolean expanded = false; 45086d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren if (rawHeight > mNaturalHeight) { 45186d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren isFinished = true; 4521408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek expanded = true; 45386d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren } 45486d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren if (rawHeight < mSmallSize) { 45586d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren isFinished = true; 4561408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek expanded = false; 45786d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren } 45886d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren 4591408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (!mHasPopped) { 4609ff1510aeba910c22b2e31c29a93b08281feae4fJorim Jaggi if (mEventSource != null) { 4619ff1510aeba910c22b2e31c29a93b08281feae4fJorim Jaggi mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 4629ff1510aeba910c22b2e31c29a93b08281feae4fJorim Jaggi } 4631408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mHasPopped = true; 46486d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren } 46586d00fb40ae5cd01ce5a2e228e6de777eae6dee8Chris Wren 4661408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mScaler.setHeight(newHeight); 4671408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 4681408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (isFinished) { 4691408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCallback.expansionStateChanged(false); 4701408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } else { 4711408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCallback.expansionStateChanged(true); 472b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 473b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren return true; 474b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 4754377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 4764377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler if (mExpanding) { 4771408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek 4781408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek // Gestural expansion is running 4794377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler updateExpansion(); 4801408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 4814377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler return true; 4824377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler } 4834377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 484b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren break; 485b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } 486cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren 487cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren case MotionEvent.ACTION_POINTER_UP: 488cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren case MotionEvent.ACTION_POINTER_DOWN: 489cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "pointer change"); 490cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mInitialTouchY += mSGD.getFocusY() - mLastFocusY; 491cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY; 492cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren break; 493cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren 4946a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler case MotionEvent.ACTION_UP: 4956a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler case MotionEvent.ACTION_CANCEL: 496cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "up/cancel"); 497740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek finishExpanding(!isEnabled() || ev.getActionMasked() == MotionEvent.ACTION_CANCEL, 498740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek getCurrentVelocity()); 49980a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren clearView(); 5006a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler break; 5016a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 5021408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mLastMotionY = ev.getRawY(); 50328c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi maybeRecycleVelocityTracker(ev); 504787a0af8ebba004a6f1cd3bfe8c78d851003d227Jorim Jaggi return mResizedView != null; 5056a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 5064377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 507be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi /** 508be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi * @return True if the view is expandable, false otherwise. 509be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi */ 5103acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek @VisibleForTesting 5113acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek boolean startExpanding(ExpandableView v, int expandType) { 512be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi if (!(v instanceof ExpandableNotificationRow)) { 513be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi return false; 514be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi } 515cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren mExpansionStyle = expandType; 5161408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (mExpanding && v == mResizedView) { 517be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi return true; 518cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren } 5194377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mExpanding = true; 5201408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCallback.expansionStateChanged(true); 521cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v); 5224377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mCallback.setUserLockedChild(v, true); 5231408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mScaler.setView(v); 5244377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mOldHeight = mScaler.getHeight(); 5251408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCurrentHeight = mOldHeight; 526388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek boolean canBeExpanded = mCallback.canChildBeExpanded(v); 527388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek if (canBeExpanded) { 528cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "working on an expandable child"); 529388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek mNaturalHeight = mScaler.getNaturalHeight(); 530567e845d99840a6e556595739a15e16132eb2f1eSelim Cinek mSmallSize = v.getCollapsedHeight(); 531b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren } else { 532cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "working on a non-expandable child"); 5334377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mNaturalHeight = mOldHeight; 5346a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 535cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight + 5364377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler " mNaturalHeight: " + mNaturalHeight); 537be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi return true; 5386a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 5396a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 540740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek /** 541740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek * Finish the current expand motion 542740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek * @param forceAbort whether the expansion should be forcefully aborted and returned to the old 543740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek * state 544740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek * @param velocity the velocity this was expanded/ collapsed with 545740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek */ 5463acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek @VisibleForTesting 5473acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek void finishExpanding(boolean forceAbort, float velocity) { 5484377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler if (!mExpanding) return; 5494377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 5501408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView); 551cea520747f344204a78db9d3f7f1abe3f695f49fChris Wren 5523ddab0dcc1039137f05a28ff86477601a223a0faChris Wren float currentHeight = mScaler.getHeight(); 5536a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler final boolean wasClosed = (mOldHeight == mSmallSize); 554388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek boolean nowExpanded; 555740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek if (!forceAbort) { 556740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek if (wasClosed) { 557740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek nowExpanded = currentHeight > mOldHeight && velocity >= 0; 558740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek } else { 559740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek nowExpanded = currentHeight >= mOldHeight || velocity > 0; 560740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek } 561740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek nowExpanded |= mNaturalHeight == mSmallSize; 5626a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } else { 563740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek nowExpanded = !wasClosed; 5646a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 565ba925e8ecd9decff5701001a0190042d6797942dChris Wren if (mScaleAnimation.isRunning()) { 566ba925e8ecd9decff5701001a0190042d6797942dChris Wren mScaleAnimation.cancel(); 567ba925e8ecd9decff5701001a0190042d6797942dChris Wren } 5681408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCallback.expansionStateChanged(false); 569740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek int naturalHeight = mScaler.getNaturalHeight(); 570388df6dd3d07376ecd7446cae36e1486cd313171Selim Cinek float targetHeight = nowExpanded ? naturalHeight : mSmallSize; 571740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek if (targetHeight != currentHeight && mEnabled) { 5723ddab0dcc1039137f05a28ff86477601a223a0faChris Wren mScaleAnimation.setFloatValues(targetHeight); 5733ddab0dcc1039137f05a28ff86477601a223a0faChris Wren mScaleAnimation.setupStartValues(); 5741408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek final View scaledView = mResizedView; 575d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek final boolean expand = nowExpanded; 576be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi mScaleAnimation.addListener(new AnimatorListenerAdapter() { 577d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek public boolean mCancelled; 578d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek 579be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi @Override 580be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi public void onAnimationEnd(Animator animation) { 581d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek if (!mCancelled) { 582d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek mCallback.setUserExpandedChild(scaledView, expand); 5833acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek if (!mExpanding) { 5843acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek mScaler.setView(null); 5853acdc8e7fc7e4e21f2ffb13b55d258f4a9cc460eSelim Cinek } 586b0a824687f56b6950338aad169d8d837f8ed657bMady Mellor } else { 587b0a824687f56b6950338aad169d8d837f8ed657bMady Mellor mCallback.setExpansionCancelled(scaledView); 588d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek } 589be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi mCallback.setUserLockedChild(scaledView, false); 590be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi mScaleAnimation.removeListener(this); 591be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi } 592d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek 593d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek @Override 594d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek public void onAnimationCancel(Animator animation) { 595d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek mCancelled = true; 596d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek } 597be565dfc1c17b7ddafa9753851b8f82849fd3f42Jorim Jaggi }); 598d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek velocity = nowExpanded == velocity >= 0 ? velocity : 0; 59928c0b714ac7f6c98d63aab106447bfd1e727fae2Jorim Jaggi mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity); 6003ddab0dcc1039137f05a28ff86477601a223a0faChris Wren mScaleAnimation.start(); 601343e6e258ab6a9f647eabebaed05ce3acafd2ff1Selim Cinek } else { 602c25af40002eb9de4e30b801b35e9e5b2474aafdfSelim Cinek if (targetHeight != currentHeight) { 603c25af40002eb9de4e30b801b35e9e5b2474aafdfSelim Cinek mScaler.setHeight(targetHeight); 604c25af40002eb9de4e30b801b35e9e5b2474aafdfSelim Cinek } 605d191a1789e7bcd890557fbc6fb1d4efda97601eeSelim Cinek mCallback.setUserExpandedChild(mResizedView, nowExpanded); 6061408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mCallback.setUserLockedChild(mResizedView, false); 607f306d9b498c2c81ad52083a0825e037f2757daf7Selim Cinek mScaler.setView(null); 6083ddab0dcc1039137f05a28ff86477601a223a0faChris Wren } 6094377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 6104377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mExpanding = false; 6114377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler mExpansionStyle = NONE; 6124377d1494cab30ab299f6065cf6857df7367db3bDaniel Sandler 613cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed); 614cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight); 615cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize); 616cd686b5b6d4166b510df8e32138479a9559bc117John Spurlock if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight); 6171408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView); 61880a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren } 61980a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren 62080a76276dc9440ffad30dc4c820eb7d65f4df368Chris Wren private void clearView() { 6211408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mResizedView = null; 6226a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler } 6236a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler 624b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren /** 625ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler * Use this to abort any pending expansions in progress. 626ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler */ 627ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler public void cancel() { 628740c1114764e341857d32b486d3b1ad985033a3bSelim Cinek finishExpanding(true /* forceAbort */, 0f /* velocity */); 629ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler clearView(); 630ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler 631ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler // reset the gesture detector 632ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener); 633ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler } 634ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler 635ac47ff70c322614ff2ca9ad82fe41338daf55877Daniel Sandler /** 6361408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek * Change the expansion mode to only observe movements and don't perform any resizing. 6371408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek * This is needed when the expanding is finished and the scroller kicks in, 6381408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek * performing an overscroll motion. We only want to shrink it again when we are not 6391408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek * overscrolled. 6401408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek * 6411408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek * @param onlyMovements Should only movements be observed? 6421408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek */ 6431408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek public void onlyObserveMovements(boolean onlyMovements) { 6441408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek mOnlyMovements = onlyMovements; 6451408eb5a58d669933c701e347fd3498ceab70f3cSelim Cinek } 6466a858c347f4d4e5db4c8f00d5e285967631b71caDaniel Sandler} 647b4e2c48b4d75e7d68209412152011441fb6deda3Chris Wren 648