1 /* 2 * Copyright (C) 2018 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 package androidx.constraintlayout.motion.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.RectF; 22 import android.util.AttributeSet; 23 import android.util.Log; 24 import android.util.Xml; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import androidx.constraintlayout.widget.R; 30 import androidx.core.widget.NestedScrollView; 31 32 import org.xmlpull.v1.XmlPullParser; 33 34 import java.util.Arrays; 35 36 /** 37 * This class is used to manage Touch behaviour 38 * 39 * 40 */ 41 42 class TouchResponse { 43 private static final String TAG = "TouchResponse"; 44 private static final boolean DEBUG = false; 45 private int mTouchAnchorSide = 0; 46 private int mTouchSide = 0; 47 private int mOnTouchUp = 0; 48 private int mTouchAnchorId = MotionScene.UNSET; 49 private int mTouchRegionId = MotionScene.UNSET; 50 private int mLimitBoundsTo = MotionScene.UNSET; 51 private float mTouchAnchorY = 0.5f; 52 private float mTouchAnchorX = 0.5f; 53 float mRotateCenterX = 0.5f; 54 float mRotateCenterY = 0.5f; 55 private int mRotationCenterId = MotionScene.UNSET; 56 boolean mIsRotateMode = false; 57 private float mTouchDirectionX = 0; 58 private float mTouchDirectionY = 1; 59 private boolean mDragStarted = false; 60 private float[] mAnchorDpDt = new float[2]; 61 private int[] mTempLoc = new int[2]; 62 private float mLastTouchX, mLastTouchY; 63 private final MotionLayout mMotionLayout; 64 private static final int SEC_TO_MILLISECONDS = 1000; 65 private static final float EPSILON = 0.0000001f; 66 67 private static final float[][] TOUCH_SIDES = { 68 {0.5f, 0.0f}, // top 69 {0.0f, 0.5f}, // left 70 {1.0f, 0.5f}, // right 71 {0.5f, 1.0f}, // bottom 72 {0.5f, 0.5f}, // middle 73 {0.0f, 0.5f}, // start (dynamically updated) 74 {1.0f, 0.5f}, // end (dynamically updated) 75 }; 76 private static final float[][] TOUCH_DIRECTION = { 77 {0.0f, -1.0f}, // up 78 {0.0f, 1.0f}, // down 79 {-1.0f, 0.0f}, // left 80 {1.0f, 0.0f}, // right 81 {-1.0f, 0.0f}, // start (dynamically updated) 82 {1.0f, 0.0f}, // end (dynamically updated) 83 }; 84 @SuppressWarnings("unused") 85 private static final int TOUCH_UP = 0; 86 @SuppressWarnings("unused") 87 private static final int TOUCH_DOWN = 1; 88 private static final int TOUCH_LEFT = 2; 89 private static final int TOUCH_RIGHT = 3; 90 private static final int TOUCH_START = 4; 91 private static final int TOUCH_END = 5; 92 93 @SuppressWarnings("unused") 94 private static final int SIDE_TOP = 0; 95 private static final int SIDE_LEFT = 1; 96 private static final int SIDE_RIGHT = 2; 97 @SuppressWarnings("unused") 98 private static final int SIDE_BOTTOM = 3; 99 @SuppressWarnings("unused") 100 private static final int SIDE_MIDDLE = 4; 101 private static final int SIDE_START = 5; 102 private static final int SIDE_END = 6; 103 104 private float mMaxVelocity = 4; 105 private float mMaxAcceleration = 1.2f; 106 private boolean mMoveWhenScrollAtTop = true; 107 private float mDragScale = 1f; 108 private int mFlags = 0; 109 static final int FLAG_DISABLE_POST_SCROLL = 1; 110 static final int FLAG_DISABLE_SCROLL = 2; 111 static final int FLAG_SUPPORT_SCROLL_UP = 4; 112 113 private float mDragThreshold = 10; 114 private float mSpringDamping = 10; 115 private float mSpringMass = 1; 116 private float mSpringStiffness = Float.NaN; 117 private float mSpringStopThreshold = Float.NaN; 118 private int mSpringBoundary = 0; 119 private int mAutoCompleteMode = COMPLETE_MODE_CONTINUOUS_VELOCITY; 120 public static final int COMPLETE_MODE_CONTINUOUS_VELOCITY = 0; 121 public static final int COMPLETE_MODE_SPRING = 1; 122 TouchResponse(Context context, MotionLayout layout, XmlPullParser parser)123 TouchResponse(Context context, MotionLayout layout, XmlPullParser parser) { 124 mMotionLayout = layout; 125 fillFromAttributeList(context, Xml.asAttributeSet(parser)); 126 } 127 TouchResponse(MotionLayout layout, OnSwipe onSwipe)128 TouchResponse(MotionLayout layout, OnSwipe onSwipe) { 129 mMotionLayout = layout; 130 mTouchAnchorId = onSwipe.getTouchAnchorId(); 131 mTouchAnchorSide = onSwipe.getTouchAnchorSide(); 132 if (mTouchAnchorSide != -1) { 133 mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0]; 134 mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1]; 135 } 136 mTouchSide = onSwipe.getDragDirection(); 137 if (mTouchSide < TOUCH_DIRECTION.length) { 138 mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0]; 139 mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1]; 140 } else { 141 mTouchDirectionX = mTouchDirectionY = Float.NaN; 142 mIsRotateMode = true; 143 } 144 mMaxVelocity = onSwipe.getMaxVelocity(); 145 mMaxAcceleration = onSwipe.getMaxAcceleration(); 146 mMoveWhenScrollAtTop = onSwipe.getMoveWhenScrollAtTop(); 147 mDragScale = onSwipe.getDragScale(); 148 mDragThreshold = onSwipe.getDragThreshold(); 149 mTouchRegionId = onSwipe.getTouchRegionId(); 150 mOnTouchUp = onSwipe.getOnTouchUp(); 151 mFlags = onSwipe.getNestedScrollFlags(); 152 mLimitBoundsTo = onSwipe.getLimitBoundsTo(); 153 mRotationCenterId = onSwipe.getRotationCenterId(); 154 mSpringBoundary = onSwipe.getSpringBoundary(); 155 mSpringDamping = onSwipe.getSpringDamping(); 156 mSpringMass = onSwipe.getSpringMass(); 157 mSpringStiffness = onSwipe.getSpringStiffness(); 158 mSpringStopThreshold = onSwipe.getSpringStopThreshold(); 159 mAutoCompleteMode = onSwipe.getAutoCompleteMode(); 160 } 161 setRTL(boolean rtl)162 public void setRTL(boolean rtl) { 163 if (rtl) { 164 TOUCH_DIRECTION[TOUCH_START] = TOUCH_DIRECTION[TOUCH_RIGHT]; 165 TOUCH_DIRECTION[TOUCH_END] = TOUCH_DIRECTION[TOUCH_LEFT]; 166 TOUCH_SIDES[SIDE_START] = TOUCH_SIDES[SIDE_RIGHT]; 167 TOUCH_SIDES[SIDE_END] = TOUCH_SIDES[SIDE_LEFT]; 168 } else { 169 TOUCH_DIRECTION[TOUCH_START] = TOUCH_DIRECTION[TOUCH_LEFT]; 170 TOUCH_DIRECTION[TOUCH_END] = TOUCH_DIRECTION[TOUCH_RIGHT]; 171 TOUCH_SIDES[SIDE_START] = TOUCH_SIDES[SIDE_LEFT]; 172 TOUCH_SIDES[SIDE_END] = TOUCH_SIDES[SIDE_RIGHT]; 173 } 174 175 mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0]; 176 mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1]; 177 if (mTouchSide >= TOUCH_DIRECTION.length) { 178 return; 179 } 180 mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0]; 181 mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1]; 182 } 183 fillFromAttributeList(Context context, AttributeSet attrs)184 private void fillFromAttributeList(Context context, AttributeSet attrs) { 185 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OnSwipe); 186 fill(a); 187 a.recycle(); 188 } 189 fill(TypedArray a)190 private void fill(TypedArray a) { 191 final int count = a.getIndexCount(); 192 for (int i = 0; i < count; i++) { 193 int attr = a.getIndex(i); 194 if (attr == R.styleable.OnSwipe_touchAnchorId) { 195 mTouchAnchorId = a.getResourceId(attr, mTouchAnchorId); 196 } else if (attr == R.styleable.OnSwipe_touchAnchorSide) { 197 mTouchAnchorSide = a.getInt(attr, mTouchAnchorSide); 198 mTouchAnchorX = TOUCH_SIDES[mTouchAnchorSide][0]; 199 mTouchAnchorY = TOUCH_SIDES[mTouchAnchorSide][1]; 200 } else if (attr == R.styleable.OnSwipe_dragDirection) { 201 mTouchSide = a.getInt(attr, mTouchSide); 202 if (mTouchSide < TOUCH_DIRECTION.length) { 203 mTouchDirectionX = TOUCH_DIRECTION[mTouchSide][0]; 204 mTouchDirectionY = TOUCH_DIRECTION[mTouchSide][1]; 205 } else { 206 mTouchDirectionX = mTouchDirectionY = Float.NaN; 207 mIsRotateMode = true; 208 } 209 } else if (attr == R.styleable.OnSwipe_maxVelocity) { 210 mMaxVelocity = a.getFloat(attr, mMaxVelocity); 211 } else if (attr == R.styleable.OnSwipe_maxAcceleration) { 212 mMaxAcceleration = a.getFloat(attr, mMaxAcceleration); 213 } else if (attr == R.styleable.OnSwipe_moveWhenScrollAtTop) { 214 mMoveWhenScrollAtTop = a.getBoolean(attr, mMoveWhenScrollAtTop); 215 } else if (attr == R.styleable.OnSwipe_dragScale) { 216 mDragScale = a.getFloat(attr, mDragScale); 217 } else if (attr == R.styleable.OnSwipe_dragThreshold) { 218 mDragThreshold = a.getFloat(attr, mDragThreshold); 219 } else if (attr == R.styleable.OnSwipe_touchRegionId) { 220 mTouchRegionId = a.getResourceId(attr, mTouchRegionId); 221 } else if (attr == R.styleable.OnSwipe_onTouchUp) { 222 mOnTouchUp = a.getInt(attr, mOnTouchUp); 223 } else if (attr == R.styleable.OnSwipe_nestedScrollFlags) { 224 mFlags = a.getInteger(attr, 0); 225 } else if (attr == R.styleable.OnSwipe_limitBoundsTo) { 226 mLimitBoundsTo = a.getResourceId(attr, 0); 227 } else if (attr == R.styleable.OnSwipe_rotationCenterId) { 228 mRotationCenterId = a.getResourceId(attr, mRotationCenterId); 229 } else if (attr == R.styleable.OnSwipe_springDamping) { 230 mSpringDamping = a.getFloat(attr, mSpringDamping); 231 } else if (attr == R.styleable.OnSwipe_springMass) { 232 mSpringMass = a.getFloat(attr, mSpringMass); 233 } else if (attr == R.styleable.OnSwipe_springStiffness) { 234 mSpringStiffness = a.getFloat(attr, mSpringStiffness); 235 } else if (attr == R.styleable.OnSwipe_springStopThreshold) { 236 mSpringStopThreshold = a.getFloat(attr, mSpringStopThreshold); 237 } else if (attr == R.styleable.OnSwipe_springBoundary) { 238 mSpringBoundary = a.getInt(attr, mSpringBoundary); 239 } else if (attr == R.styleable.OnSwipe_autoCompleteMode) { 240 mAutoCompleteMode = a.getInt(attr, mAutoCompleteMode); 241 } 242 243 } 244 } 245 setUpTouchEvent(float lastTouchX, float lastTouchY)246 void setUpTouchEvent(float lastTouchX, float lastTouchY) { 247 mLastTouchX = lastTouchX; 248 mLastTouchY = lastTouchY; 249 mDragStarted = false; 250 } 251 252 /** 253 * @param event 254 * @param velocityTracker 255 * @param currentState 256 * @param motionScene 257 */ processTouchRotateEvent(MotionEvent event, MotionLayout.MotionTracker velocityTracker, int currentState, MotionScene motionScene)258 void processTouchRotateEvent(MotionEvent event, 259 MotionLayout.MotionTracker velocityTracker, 260 int currentState, 261 MotionScene motionScene) { 262 velocityTracker.addMovement(event); 263 switch (event.getAction()) { 264 case MotionEvent.ACTION_DOWN: 265 266 mLastTouchX = event.getRawX(); 267 mLastTouchY = event.getRawY(); 268 269 mDragStarted = false; 270 break; 271 case MotionEvent.ACTION_MOVE: 272 @SuppressWarnings("unused") 273 float dy = event.getRawY() - mLastTouchY; 274 @SuppressWarnings("unused") 275 float dx = event.getRawX() - mLastTouchX; 276 277 float drag; 278 279 float rcx = mMotionLayout.getWidth() / 2.0f; 280 float rcy = mMotionLayout.getHeight() / 2.0f; 281 if (mRotationCenterId != MotionScene.UNSET) { 282 View v = mMotionLayout.findViewById(mRotationCenterId); 283 mMotionLayout.getLocationOnScreen(mTempLoc); 284 rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f; 285 rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f; 286 } else if (mTouchAnchorId != MotionScene.UNSET) { 287 MotionController mc = mMotionLayout.getMotionController(mTouchAnchorId); 288 View v = mMotionLayout.findViewById(mc.getAnimateRelativeTo()); 289 if (v == null) { 290 Log.e(TAG, "could not find view to animate to"); 291 } else { 292 mMotionLayout.getLocationOnScreen(mTempLoc); 293 rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f; 294 rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f; 295 } 296 } 297 float relativePosX = event.getRawX() - rcx; 298 float relativePosY = event.getRawY() - rcy; 299 300 double angle1 = Math.atan2(event.getRawY() - rcy, event.getRawX() - rcx); 301 double angle2 = Math.atan2(mLastTouchY - rcy, mLastTouchX - rcx); 302 drag = (float) ((angle1 - angle2) * 180.0f / Math.PI); 303 if (drag > 330) { 304 drag -= 360; 305 } else if (drag < -330) { 306 drag += 360; 307 } 308 309 if (Math.abs(drag) > 0.01 || mDragStarted) { 310 float pos = mMotionLayout.getProgress(); 311 if (!mDragStarted) { 312 mDragStarted = true; 313 mMotionLayout.setProgress(pos); 314 } 315 if (mTouchAnchorId != MotionScene.UNSET) { 316 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, 317 mTouchAnchorX, mTouchAnchorY, mAnchorDpDt); 318 mAnchorDpDt[1] = (float) Math.toDegrees(mAnchorDpDt[1]); 319 } else { 320 mAnchorDpDt[1] = 360; 321 } 322 float change = drag * mDragScale / mAnchorDpDt[1]; 323 324 pos = Math.max(Math.min(pos + change, 1), 0); 325 float current = mMotionLayout.getProgress(); 326 327 if (pos != current) { 328 if (current == 0.0f || current == 1.0f) { 329 mMotionLayout.endTrigger(current == 0.0f); 330 } 331 mMotionLayout.setProgress(pos); 332 velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS); 333 float tvx = velocityTracker.getXVelocity(); 334 float tvy = velocityTracker.getYVelocity(); 335 float angularVelocity = // v*sin(angle)/r 336 (float) (Math.hypot(tvy, tvx) 337 * Math.sin(Math.atan2(tvy, tvx) - angle1) 338 / Math.hypot(relativePosX, relativePosY)); 339 mMotionLayout.mLastVelocity = (float) Math.toDegrees(angularVelocity); 340 } else { 341 mMotionLayout.mLastVelocity = 0; 342 } 343 mLastTouchX = event.getRawX(); 344 mLastTouchY = event.getRawY(); 345 } 346 347 break; 348 case MotionEvent.ACTION_UP: 349 mDragStarted = false; 350 velocityTracker.computeCurrentVelocity(16); 351 352 float tvx = velocityTracker.getXVelocity(); 353 float tvy = velocityTracker.getYVelocity(); 354 float currentPos = mMotionLayout.getProgress(); 355 float pos = currentPos; 356 rcx = mMotionLayout.getWidth() / 2.0f; 357 rcy = mMotionLayout.getHeight() / 2.0f; 358 if (mRotationCenterId != MotionScene.UNSET) { 359 View v = mMotionLayout.findViewById(mRotationCenterId); 360 mMotionLayout.getLocationOnScreen(mTempLoc); 361 rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f; 362 rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f; 363 } else if (mTouchAnchorId != MotionScene.UNSET) { 364 MotionController mc = mMotionLayout.getMotionController(mTouchAnchorId); 365 View v = mMotionLayout.findViewById(mc.getAnimateRelativeTo()); 366 mMotionLayout.getLocationOnScreen(mTempLoc); 367 rcx = mTempLoc[0] + (v.getLeft() + v.getRight()) / 2.0f; 368 rcy = mTempLoc[1] + (v.getTop() + v.getBottom()) / 2.0f; 369 } 370 relativePosX = event.getRawX() - rcx; 371 relativePosY = event.getRawY() - rcy; 372 angle1 = Math.toDegrees(Math.atan2(relativePosY, relativePosX)); 373 374 if (mTouchAnchorId != MotionScene.UNSET) { 375 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, 376 mTouchAnchorX, mTouchAnchorY, mAnchorDpDt); 377 mAnchorDpDt[1] = (float) Math.toDegrees(mAnchorDpDt[1]); 378 } else { 379 mAnchorDpDt[1] = 360; 380 } 381 angle2 = Math.toDegrees(Math.atan2(tvy + relativePosY, tvx + relativePosX)); 382 drag = (float) (angle2 - angle1); 383 float velocity_tweek = SEC_TO_MILLISECONDS / 16f; 384 float angularVelocity = drag * velocity_tweek; 385 if (!Float.isNaN(angularVelocity)) { 386 pos += 3 * angularVelocity * mDragScale / mAnchorDpDt[1]; // TODO calibrate vel 387 } 388 if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) { 389 angularVelocity = (float) angularVelocity * mDragScale / mAnchorDpDt[1]; 390 float target = (pos < 0.5) ? 0.0f : 1.0f; 391 392 if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) { 393 if (currentPos + angularVelocity < 0) { 394 angularVelocity = Math.abs(angularVelocity); 395 } 396 target = 1; 397 } 398 if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) { 399 if (currentPos + angularVelocity > 1) { 400 angularVelocity = -Math.abs(angularVelocity); 401 } 402 target = 0; 403 } 404 mMotionLayout.touchAnimateTo(mOnTouchUp, target , 405 3 * angularVelocity); 406 if (0.0f >= currentPos || 1.0f <= currentPos) { 407 mMotionLayout.setState(MotionLayout.TransitionState.FINISHED); 408 } 409 } else if (0.0f >= pos || 1.0f <= pos) { 410 mMotionLayout.setState(MotionLayout.TransitionState.FINISHED); 411 } 412 break; 413 } 414 415 } 416 417 /** 418 * Process touch events 419 * 420 * @param event The event coming from the touch 421 * @param currentState 422 * @param motionScene The relevant MotionScene 423 */ processTouchEvent(MotionEvent event, MotionLayout.MotionTracker velocityTracker, int currentState, MotionScene motionScene)424 void processTouchEvent(MotionEvent event, 425 MotionLayout.MotionTracker velocityTracker, 426 int currentState, 427 MotionScene motionScene) { 428 if (DEBUG) { 429 Log.v(TAG, Debug.getLocation() + " best processTouchEvent For "); 430 } 431 if (mIsRotateMode) { 432 processTouchRotateEvent(event, velocityTracker, currentState, motionScene); 433 return; 434 } 435 velocityTracker.addMovement(event); 436 switch (event.getAction()) { 437 case MotionEvent.ACTION_DOWN: 438 mLastTouchX = event.getRawX(); 439 mLastTouchY = event.getRawY(); 440 mDragStarted = false; 441 break; 442 case MotionEvent.ACTION_MOVE: 443 float dy = event.getRawY() - mLastTouchY; 444 float dx = event.getRawX() - mLastTouchX; 445 float drag = dx * mTouchDirectionX + dy * mTouchDirectionY; 446 if (DEBUG) { 447 Log.v(TAG, "# dx = " + dx + " = " + event.getRawX() + " - " + mLastTouchX); 448 Log.v(TAG, "# drag = " + drag); 449 } 450 if (Math.abs(drag) > mDragThreshold || mDragStarted) { 451 if (DEBUG) { 452 Log.v(TAG, "# ACTION_MOVE mDragStarted "); 453 } 454 float pos = mMotionLayout.getProgress(); 455 if (!mDragStarted) { 456 mDragStarted = true; 457 mMotionLayout.setProgress(pos); 458 if (DEBUG) { 459 Log.v(TAG, "# ACTION_MOVE progress <- " + pos); 460 } 461 } 462 if (mTouchAnchorId != MotionScene.UNSET) { 463 464 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, 465 mTouchAnchorY, mAnchorDpDt); 466 if (DEBUG) { 467 Log.v(TAG, Debug.getLocation() + " mAnchorDpDt " 468 + Arrays.toString(mAnchorDpDt)); 469 } 470 } else { 471 if (DEBUG) { 472 Log.v(TAG, Debug.getLocation() + " NO ANCHOR "); 473 } 474 float minSize = Math.min(mMotionLayout.getWidth(), 475 mMotionLayout.getHeight()); 476 mAnchorDpDt[1] = minSize * mTouchDirectionY; 477 mAnchorDpDt[0] = minSize * mTouchDirectionX; 478 } 479 480 float movmentInDir = mTouchDirectionX * mAnchorDpDt[0] 481 + mTouchDirectionY * mAnchorDpDt[1]; 482 if (DEBUG) { 483 Log.v(TAG, "# ACTION_MOVE movmentInDir <- " + movmentInDir + " "); 484 485 Log.v(TAG, "# ACTION_MOVE mAnchorDpDt = " + mAnchorDpDt[0] 486 + " , " + mAnchorDpDt[1]); 487 Log.v(TAG, "# ACTION_MOVE mTouchDir = " + mTouchDirectionX 488 + " , " + mTouchDirectionY); 489 490 } 491 movmentInDir *= mDragScale; 492 493 if (Math.abs(movmentInDir) < 0.01) { 494 mAnchorDpDt[0] = .01f; 495 mAnchorDpDt[1] = .01f; 496 497 } 498 float change; 499 if (mTouchDirectionX != 0) { 500 change = dx / mAnchorDpDt[0]; 501 } else { 502 change = dy / mAnchorDpDt[1]; 503 } 504 if (DEBUG) { 505 Log.v(TAG, "# ACTION_MOVE CHANGE = " + change); 506 } 507 508 pos = Math.max(Math.min(pos + change, 1), 0); 509 510 if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) { 511 pos = Math.max(pos, 0.01f); 512 } 513 if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) { 514 pos = Math.min(pos, 0.99f); 515 } 516 517 float current = mMotionLayout.getProgress(); 518 if (pos != current) { 519 if (current == 0.0f || current == 1.0f) { 520 mMotionLayout.endTrigger(current == 0.0f); 521 } 522 mMotionLayout.setProgress(pos); 523 if (DEBUG) { 524 Log.v(TAG, "# ACTION_MOVE progress <- " + pos); 525 } 526 velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS); 527 float tvx = velocityTracker.getXVelocity(); 528 float tvy = velocityTracker.getYVelocity(); 529 float velocity = (mTouchDirectionX != 0) ? tvx / mAnchorDpDt[0] 530 : tvy / mAnchorDpDt[1]; 531 mMotionLayout.mLastVelocity = velocity; 532 } else { 533 mMotionLayout.mLastVelocity = 0; 534 } 535 mLastTouchX = event.getRawX(); 536 mLastTouchY = event.getRawY(); 537 } 538 break; 539 case MotionEvent.ACTION_UP: 540 mDragStarted = false; 541 velocityTracker.computeCurrentVelocity(SEC_TO_MILLISECONDS); 542 float tvx = velocityTracker.getXVelocity(); 543 float tvy = velocityTracker.getYVelocity(); 544 float currentPos = mMotionLayout.getProgress(); 545 float pos = currentPos; 546 547 if (DEBUG) { 548 Log.v(TAG, "# ACTION_UP progress = " + pos); 549 } 550 if (mTouchAnchorId != MotionScene.UNSET) { 551 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, 552 mTouchAnchorX, mTouchAnchorY, mAnchorDpDt); 553 } else { 554 float minSize = Math.min(mMotionLayout.getWidth(), mMotionLayout.getHeight()); 555 mAnchorDpDt[1] = minSize * mTouchDirectionY; 556 mAnchorDpDt[0] = minSize * mTouchDirectionX; 557 } 558 @SuppressWarnings("unused") 559 float movmentInDir = mTouchDirectionX * mAnchorDpDt[0] 560 + mTouchDirectionY * mAnchorDpDt[1]; 561 float velocity; 562 if (mTouchDirectionX != 0) { 563 velocity = tvx / mAnchorDpDt[0]; 564 } else { 565 velocity = tvy / mAnchorDpDt[1]; 566 } 567 if (DEBUG) { 568 Log.v(TAG, "# ACTION_UP tvy = " + tvy); 569 Log.v(TAG, "# ACTION_UP mTouchDirectionX = " + mTouchDirectionX); 570 Log.v(TAG, "# ACTION_UP velocity = " + velocity); 571 } 572 573 if (!Float.isNaN(velocity)) { 574 pos += velocity / 3; // TODO calibration & animation speed based on velocity 575 } 576 if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) { 577 float target = (pos < 0.5) ? 0.0f : 1.0f; 578 579 if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_START) { 580 if (currentPos + velocity < 0) { 581 velocity = Math.abs(velocity); 582 } 583 target = 1; 584 } 585 if (mOnTouchUp == MotionLayout.TOUCH_UP_NEVER_TO_END) { 586 if (currentPos + velocity > 1) { 587 velocity = -Math.abs(velocity); 588 } 589 target = 0; 590 } 591 592 mMotionLayout.touchAnimateTo(mOnTouchUp, target, velocity); 593 if (0.0f >= currentPos || 1.0f <= currentPos) { 594 mMotionLayout.setState(MotionLayout.TransitionState.FINISHED); 595 } 596 } else if (0.0f >= pos || 1.0f <= pos) { 597 mMotionLayout.setState(MotionLayout.TransitionState.FINISHED); 598 599 } 600 break; 601 } 602 } 603 setDown(float lastTouchX, float lastTouchY)604 void setDown(float lastTouchX, float lastTouchY) { 605 mLastTouchX = lastTouchX; 606 mLastTouchY = lastTouchY; 607 } 608 609 /** 610 * Calculate if a drag in this direction results in an increase or decrease in progress. 611 * 612 * @param dx drag direction in x 613 * @param dy drag direction in y 614 * @return the change in progress given that dx and dy 615 */ getProgressDirection(float dx, float dy)616 float getProgressDirection(float dx, float dy) { 617 float pos = mMotionLayout.getProgress(); 618 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, mTouchAnchorY, mAnchorDpDt); 619 float velocity; 620 if (mTouchDirectionX != 0) { 621 if (mAnchorDpDt[0] == 0) { 622 mAnchorDpDt[0] = EPSILON; 623 } 624 velocity = dx * mTouchDirectionX / mAnchorDpDt[0]; 625 } else { 626 if (mAnchorDpDt[1] == 0) { 627 mAnchorDpDt[1] = EPSILON; 628 } 629 velocity = dy * mTouchDirectionY / mAnchorDpDt[1]; 630 } 631 return velocity; 632 } 633 scrollUp(float dx, float dy)634 void scrollUp(float dx, float dy) { 635 mDragStarted = false; 636 637 float pos = mMotionLayout.getProgress(); 638 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, mTouchAnchorX, mTouchAnchorY, mAnchorDpDt); 639 @SuppressWarnings("unused") 640 float movmentInDir = mTouchDirectionX * mAnchorDpDt[0] + mTouchDirectionY * mAnchorDpDt[1]; 641 float velocity; 642 if (mTouchDirectionX != 0) { 643 velocity = dx * mTouchDirectionX / mAnchorDpDt[0]; 644 } else { 645 velocity = dy * mTouchDirectionY / mAnchorDpDt[1]; 646 } 647 if (!Float.isNaN(velocity)) { 648 pos += velocity / 3; // TODO calibration & animation speed based on velocity 649 } 650 if (pos != 0.0f && pos != 1.0f && mOnTouchUp != MotionLayout.TOUCH_UP_STOP) { 651 mMotionLayout.touchAnimateTo(mOnTouchUp, (pos < 0.5) ? 0.0f : 1.0f, velocity); 652 } 653 } 654 scrollMove(float dx, float dy)655 void scrollMove(float dx, float dy) { 656 @SuppressWarnings("unused") 657 float drag = dx * mTouchDirectionX + dy * mTouchDirectionY; 658 if (true) { // Todo evaluate || Math.abs(drag) > 10 || mDragStarted) { 659 float pos = mMotionLayout.getProgress(); 660 if (!mDragStarted) { 661 mDragStarted = true; 662 mMotionLayout.setProgress(pos); 663 } 664 mMotionLayout.getAnchorDpDt(mTouchAnchorId, pos, 665 mTouchAnchorX, mTouchAnchorY, mAnchorDpDt); 666 float movmentInDir = mTouchDirectionX * mAnchorDpDt[0] 667 + mTouchDirectionY * mAnchorDpDt[1]; 668 669 if (Math.abs(movmentInDir) < 0.01) { 670 mAnchorDpDt[0] = .01f; 671 mAnchorDpDt[1] = .01f; 672 673 } 674 float change; 675 if (mTouchDirectionX != 0) { 676 change = dx * mTouchDirectionX / mAnchorDpDt[0]; 677 } else { 678 change = dy * mTouchDirectionY / mAnchorDpDt[1]; 679 } 680 pos = Math.max(Math.min(pos + change, 1), 0); 681 682 if (pos != mMotionLayout.getProgress()) { 683 mMotionLayout.setProgress(pos); 684 if (DEBUG) { 685 Log.v(TAG, "# ACTION_UP progress <- " + pos); 686 } 687 } 688 689 } 690 } 691 setupTouch()692 void setupTouch() { 693 694 View view = null; 695 if (mTouchAnchorId != -1) { 696 view = mMotionLayout.findViewById(mTouchAnchorId); 697 if (view == null) { 698 Log.e(TAG, "cannot find TouchAnchorId @id/" 699 + Debug.getName(mMotionLayout.getContext(), mTouchAnchorId)); 700 } 701 } 702 if (view instanceof NestedScrollView) { 703 final NestedScrollView sv = (NestedScrollView) view; 704 sv.setOnTouchListener(new View.OnTouchListener() { 705 @Override 706 public boolean onTouch(View view, MotionEvent motionEvent) { 707 return false; 708 } 709 }); 710 sv.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() { 711 712 @Override 713 public void onScrollChange(NestedScrollView v, 714 int scrollX, 715 int scrollY, 716 int oldScrollX, 717 int oldScrollY) { 718 719 } 720 }); 721 } 722 } 723 724 /** 725 * set the id of the anchor 726 * 727 * @param id 728 */ setAnchorId(int id)729 public void setAnchorId(int id) { 730 mTouchAnchorId = id; 731 } 732 733 /** 734 * Get the view being used as anchor 735 * 736 * @return 737 */ getAnchorId()738 public int getAnchorId() { 739 return mTouchAnchorId; 740 } 741 742 /** 743 * Set the location in the view to be the touch anchor 744 * 745 * @param x location in x 0 = left, 1 = right 746 * @param y location in y 0 = top, 1 = bottom 747 */ setTouchAnchorLocation(float x, float y)748 public void setTouchAnchorLocation(float x, float y) { 749 mTouchAnchorX = x; 750 mTouchAnchorY = y; 751 } 752 753 /** 754 * Sets the maximum velocity allowed on touch up. 755 * Velocity is the rate of change in "progress" per second. 756 * 757 * @param velocity in progress per second 1 = one second to do the entire animation 758 */ setMaxVelocity(float velocity)759 public void setMaxVelocity(float velocity) { 760 mMaxVelocity = velocity; 761 } 762 763 /** 764 * set the maximum Acceleration allowed for a motion. 765 * Acceleration is the rate of change velocity per second. 766 * 767 * @param acceleration 768 */ setMaxAcceleration(float acceleration)769 public void setMaxAcceleration(float acceleration) { 770 mMaxAcceleration = acceleration; 771 } 772 getMaxAcceleration()773 float getMaxAcceleration() { 774 return mMaxAcceleration; 775 } 776 777 /** 778 * Gets the maximum velocity allowed on touch up. 779 * Velocity is the rate of change in "progress" per second. 780 * 781 * @return 782 */ getMaxVelocity()783 public float getMaxVelocity() { 784 return mMaxVelocity; 785 } 786 getMoveWhenScrollAtTop()787 boolean getMoveWhenScrollAtTop() { 788 return mMoveWhenScrollAtTop; 789 } 790 791 /** 792 * Get how the drag progress will return to the start or end state on touch up. 793 * Can be ether COMPLETE_MODE_CONTINUOUS_VELOCITY (default) or COMPLETE_MODE_SPRING 794 * @return 795 */ getAutoCompleteMode()796 public int getAutoCompleteMode() { 797 return mAutoCompleteMode; 798 } 799 /** 800 * set how the drag progress will return to the start or end state on touch up. 801 * 802 * 803 * @return 804 */ setAutoCompleteMode(int autoCompleteMode)805 void setAutoCompleteMode(int autoCompleteMode) { 806 mAutoCompleteMode = autoCompleteMode; 807 } 808 809 /** 810 * This calculates the bounds of the mTouchRegionId view. 811 * This reuses rect for efficiency as this class will be called many times. 812 * 813 * @param layout The layout containing the view (findViewId) 814 * @param rect the rectangle to fill provided so this function does not have to create memory 815 * @return the rect or null 816 */ getTouchRegion(ViewGroup layout, RectF rect)817 RectF getTouchRegion(ViewGroup layout, RectF rect) { 818 if (mTouchRegionId == MotionScene.UNSET) { 819 return null; 820 } 821 View view = layout.findViewById(mTouchRegionId); 822 if (view == null) { 823 return null; 824 } 825 rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 826 return rect; 827 } 828 getTouchRegionId()829 int getTouchRegionId() { 830 return mTouchRegionId; 831 } 832 833 /** 834 * This calculates the bounds of the mTouchRegionId view. 835 * This reuses rect for efficiency as this class will be called many times. 836 * 837 * @param layout The layout containing the view (findViewId) 838 * @param rect the rectangle to fill provided for memory efficiency 839 * @return the rect or null 840 */ getLimitBoundsTo(ViewGroup layout, RectF rect)841 RectF getLimitBoundsTo(ViewGroup layout, RectF rect) { 842 if (mLimitBoundsTo == MotionScene.UNSET) { 843 return null; 844 } 845 View view = layout.findViewById(mLimitBoundsTo); 846 if (view == null) { 847 return null; 848 } 849 rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 850 return rect; 851 } 852 getLimitBoundsToId()853 int getLimitBoundsToId() { 854 return mLimitBoundsTo; 855 } 856 dot(float dx, float dy)857 float dot(float dx, float dy) { 858 return dx * mTouchDirectionX + dy * mTouchDirectionY; 859 } 860 861 @Override toString()862 public String toString() { 863 return Float.isNaN(mTouchDirectionX) ? "rotation" 864 : (mTouchDirectionX + " , " + mTouchDirectionY); 865 } 866 867 /** 868 * flags to control 869 * 870 * @return 871 */ getFlags()872 public int getFlags() { 873 return mFlags; 874 } 875 setTouchUpMode(int touchUpMode)876 public void setTouchUpMode(int touchUpMode) { 877 mOnTouchUp = touchUpMode; 878 } 879 880 /** 881 * the stiffness of the spring if using spring 882 * K in "a = (-k*x-c*v)/m" equation for the acceleration of a spring 883 * @return NaN if not set 884 */ getSpringStiffness()885 public float getSpringStiffness() { 886 return mSpringStiffness; 887 } 888 889 /** 890 * the Mass of the spring if using spring 891 * m in "a = (-k*x-c*v)/m" equation for the acceleration of a spring 892 * @return default is 1 893 */ getSpringMass()894 public float getSpringMass() { 895 return mSpringMass; 896 } 897 898 /** 899 * the damping of the spring if using spring 900 * c in "a = (-k*x-c*v)/m" equation for the acceleration of a spring 901 * @return NaN if not set 902 */ getSpringDamping()903 public float getSpringDamping() { 904 return mSpringDamping; 905 } 906 907 /** 908 * The threshold below 909 * @return NaN if not set 910 */ getSpringStopThreshold()911 public float getSpringStopThreshold() { 912 return mSpringStopThreshold; 913 } 914 915 /** 916 * The spring's behaviour when it hits 0 or 1. It can be made ot overshoot or bounce 917 * overshoot = 0 918 * bounceStart = 1 919 * bounceEnd = 2 920 * bounceBoth = 3 921 * @return Bounce mode 922 */ getSpringBoundary()923 public int getSpringBoundary() { 924 return mSpringBoundary; 925 } 926 isDragStarted()927 boolean isDragStarted() { 928 return mDragStarted; 929 } 930 931 } 932