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 static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 20 21 import static androidx.constraintlayout.motion.widget.MotionScene.Transition.TRANSITION_FLAG_FIRST_DRAW; 22 import static androidx.constraintlayout.motion.widget.MotionScene.Transition.TRANSITION_FLAG_INTERCEPT_TOUCH; 23 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID; 24 import static androidx.constraintlayout.widget.ConstraintSet.UNSET; 25 26 import android.annotation.SuppressLint; 27 import android.content.Context; 28 import android.content.res.TypedArray; 29 import android.graphics.Canvas; 30 import android.graphics.DashPathEffect; 31 import android.graphics.Matrix; 32 import android.graphics.Paint; 33 import android.graphics.Path; 34 import android.graphics.Rect; 35 import android.graphics.RectF; 36 import android.os.Build; 37 import android.os.Bundle; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.util.SparseArray; 41 import android.util.SparseBooleanArray; 42 import android.util.SparseIntArray; 43 import android.view.Display; 44 import android.view.MotionEvent; 45 import android.view.VelocityTracker; 46 import android.view.View; 47 import android.view.ViewGroup; 48 import android.view.animation.Interpolator; 49 import android.widget.TextView; 50 51 import androidx.annotation.IdRes; 52 import androidx.constraintlayout.core.motion.utils.KeyCache; 53 import androidx.constraintlayout.core.widgets.ConstraintAnchor; 54 import androidx.constraintlayout.core.widgets.ConstraintWidget; 55 import androidx.constraintlayout.core.widgets.ConstraintWidgetContainer; 56 import androidx.constraintlayout.core.widgets.Flow; 57 import androidx.constraintlayout.core.widgets.Helper; 58 import androidx.constraintlayout.core.widgets.Placeholder; 59 import androidx.constraintlayout.motion.utils.StopLogic; 60 import androidx.constraintlayout.motion.utils.ViewState; 61 import androidx.constraintlayout.widget.Barrier; 62 import androidx.constraintlayout.widget.ConstraintHelper; 63 import androidx.constraintlayout.widget.ConstraintLayout; 64 import androidx.constraintlayout.widget.ConstraintSet; 65 import androidx.constraintlayout.widget.Constraints; 66 import androidx.constraintlayout.widget.R; 67 import androidx.core.view.NestedScrollingParent3; 68 69 import org.jspecify.annotations.NonNull; 70 import org.jspecify.annotations.Nullable; 71 72 import java.util.ArrayList; 73 import java.util.Arrays; 74 import java.util.HashMap; 75 import java.util.List; 76 import java.util.concurrent.CopyOnWriteArrayList; 77 78 /** 79 * A subclass of ConstraintLayout that supports animating between 80 * various states <b>Added in 2.0</b> 81 * <p> 82 * A {@code MotionLayout} is a subclass of {@link ConstraintLayout} 83 * which supports transitions between between various states ({@link ConstraintSet}) 84 * defined in {@link MotionScene}s. 85 * <p> 86 * <b>Note:</b> {@code MotionLayout} is available as a support library that you can use 87 * on Android systems starting with API level 14 (ICS). 88 * </p> 89 * <p> 90 * {@code MotionLayout} links to and requires a {@link MotionScene} file. 91 * The file contains one top level tag "MotionScene" 92 * <h2>LayoutDescription</h2> 93 * <table summary="LayoutDescription"> 94 * <tr> 95 * <th>Tags</th><th>Description</th> 96 * </tr> 97 * <tr> 98 * <td>{@code <StateSet> }</td> 99 * <td>Describes states supported by the system (optional)</td> 100 * </tr> 101 * <tr> 102 * <td>{@code <ConstraintSet> }</td> 103 * <td>Describes a constraint set</td> 104 * </tr> 105 * <tr> 106 * <td>{@code <Transition> }</td> 107 * <td>Describes a transition between two states or ConstraintSets</td> 108 * </tr> 109 * <tr> 110 * <td>{@code <ViewTransition> }</td> 111 * <td>Describes a transition of a View within a states or ConstraintSets</td> 112 * </tr> 113 * </table> 114 * 115 * <h2>Transition</h2> 116 * <table summary="Transition attributes & tags"> 117 * <tr> 118 * <th>Attributes</th><th>Description</th> 119 * </tr> 120 * <tr> 121 * <td>android:id</td> 122 * <td>The id of the Transition</td> 123 * </tr> 124 * <tr> 125 * <td>constraintSetStart</td> 126 * <td>ConstraintSet to be used as the start constraints or a 127 * layout file to get the constraint from</td> 128 * </tr> 129 * <tr> 130 * <td>constraintSetEnd</td> 131 * <td>ConstraintSet to be used as the end constraints or a 132 * layout file to get the constraint from</td> 133 * </tr> 134 * <tr> 135 * <td>motionInterpolator</td> 136 * <td>The ability to set an overall interpolation (easeInOut, linear, etc.)</td> 137 * </tr> 138 * <tr> 139 * <td>duration</td> 140 * <td>Length of time to take to perform the transition</td> 141 * </tr> 142 * <tr> 143 * <td>staggered</td> 144 * <td>Overrides the Manhattan distance from the top most view in the list of views. 145 * <ul> 146 * <li>For any view of stagger value {@code S(Vi)}</li> 147 * <li>With the transition stagger value of {@code TS} (from 0.0 - 1.0)</li> 148 * <li>The duration of the animation is {@code duration}</li> 149 * <li>The views animation duration {@code DS = duration * (1 -TS)}</li> 150 * <li>Call the stagger fraction {@code SFi = (S(Vi) - S(V0)) / (S(Vn) - S(V0))}</li> 151 * <li>The view starts animating at: {@code (duration-DS) * SFi}</li> 152 * </ul> 153 * </td> 154 * </tr> 155 * <tr> 156 * <td>pathMotionArc</td> 157 * <td>The path will move in arc (quarter ellipses) 158 * key words {startVertical | startHorizontal | flip | none }</td> 159 * </tr> 160 * <tr> 161 * <td>autoTransition</td> 162 * <td>automatically transition from one state to another. 163 * key words {none, jumpToStart, jumpToEnd, animateToStart, animateToEnd}</td> 164 * </tr> 165 * </tr> 166 * <tr> 167 * <td>transitionFlags</td> 168 * <td>flags that adjust the behaviour of Transitions. supports {none, beginOnFirstDraw} 169 * begin on first draw forces the transition's clock to start when it is first 170 * displayed not when the begin is called</td> 171 * </tr> 172 * </tr> 173 * <tr> 174 * <td>layoutDuringTransition</td> 175 * <td>Configures MotionLayout on how to react to requestLayout calls during transitions. 176 * Allowed values are {ignoreRequest, honorRequest}</td> 177 * </tr> 178 * <tr> 179 * <td>{@code <OnSwipe> }</td> 180 * <td>Adds support for touch handling (optional)</td> 181 * </tr> 182 * <tr> 183 * <td>{@code <OnClick> }</td> 184 * <td>Adds support for triggering transition (optional)</td> 185 * </tr> 186 * <tr> 187 * <td>{@code <KeyFrameSet> }</td> 188 * <td>Describes a set of Key object which modify the animation between constraint sets.</td> 189 * </tr> 190 * </table> 191 * 192 * <ul> 193 * <li>A transition is typically defined by specifying its start and end ConstraintSets. 194 * You also have the possibility to not specify them, in which case such transition 195 * will become a Default transition. 196 * That Default transition will be applied between any state change that isn't 197 * explicitly covered by a transition.</li> 198 * <li>The starting state of the MotionLayout is defined to be the constraintSetStart of the first 199 * transition.</li> 200 * <li>If no transition is specified (or only a default Transition) 201 * the MotionLayout tag must contain 202 * a app:currentState to define the starting state of the MotionLayout</li> 203 * </ul> 204 * 205 * <h2>ViewTransition</h2> 206 * <table summary="Transition attributes & tags"> 207 * <tr> 208 * <th>Attributes</th><th>Description</th> 209 * </tr> 210 * <tr> 211 * <td>android:id</td> 212 * <td>The id of the ViewTransition</td> 213 * </tr> 214 * <tr> 215 * <td>viewTransitionMode</td> 216 * <td>currentState, allStates, noState transition affect the state of the view 217 * in the current constraintSet or all ConstraintSets or non 218 * if noState the ViewTransitions are run asynchronous</td> 219 * </tr> 220 * <tr> 221 * <td>onStateTransition</td> 222 * <td>actionDown or actionUp run transition if on touch down or 223 * up if view matches motionTarget</td> 224 * </tr> 225 * <tr> 226 * <td>motionInterpolator</td> 227 * <td>The ability to set an overall interpolation 228 * key words {easeInOut, linear, etc.}</td> 229 * </tr> 230 * <tr> 231 * <td>duration</td> 232 * <td>Length of time to take to perform the {@code ViewTransition}</td> 233 * </tr> 234 * <tr> 235 * <td>pathMotionArc</td> 236 * <td>The path will move in arc (quarter ellipses) 237 * key words {startVertical | startHorizontal | flip | none }</td> 238 * </tr> 239 * <tr> 240 * <td>motionTarget</td> 241 * <td>Apply ViewTransition matching this string or id.</td> 242 * </tr> 243 * </tr> 244 * <tr> 245 * <td>setsTag</td> 246 * <td>set this tag at end of transition</td> 247 * </tr> 248 * </tr> 249 * <tr> 250 * <td>clearsTag</td> 251 * <td>clears this tag at end of transition</td> 252 * </tr> 253 * <tr> 254 * <td>ifTagSet</td> 255 * <td>run transition if this tag is set on view</td> 256 * </tr> 257 * <tr> 258 * <td>ifTagNotSet</td> 259 * <td>run transition if this tag is not set on view/td> 260 * </tr> 261 * <tr> 262 * <td>{@code <OnSwipe> }</td> 263 * <td>Adds support for touch handling (optional)</td> 264 * </tr> 265 * <tr> 266 * <td>{@code <OnClick> }</td> 267 * <td>Adds support for triggering transition (optional)</td> 268 * </tr> 269 * <tr> 270 * <td>{@code <KeyFrameSet> }</td> 271 * <td>Describes a set of Key object which modify the animation between constraint sets.</td> 272 * </tr> 273 * </table> 274 * 275 * <ul> 276 * <li>A Transition is typically defined by specifying its start and end ConstraintSets. 277 * You also have the possibility to not specify them, in which case such transition 278 * will become a Default transition. 279 * That Default transition will be applied between any state change that isn't 280 * explicitly covered by a transition.</li> 281 * <li>The starting state of the MotionLayout is defined to be the constraintSetStart of the first 282 * transition.</li> 283 * <li>If no transition is specified (or only a default Transition) the 284 * MotionLayout tag must contain 285 * a app:currentState to define the starting state of the MotionLayout</li> 286 * </ul> 287 * 288 * 289 * <p> 290 * <h2>OnSwipe (optional)</h2> 291 * <table summary="OnSwipe attributes"> 292 * <tr> 293 * <th>Attributes</th><th>Description</th> 294 * </tr> 295 * <tr> 296 * <td>touchAnchorId</td> 297 * <td>Have the drag act as if it is moving the "touchAnchorSide" of this object</td> 298 * </tr> 299 * <tr> 300 * <td>touchRegionId</td> 301 * <td>Limits the region that the touch can be start in to the bounds of this view 302 * (even if the view is invisible)</td> 303 * </tr> 304 * <tr> 305 * <td>touchAnchorSide</td> 306 * <td>The side of the object to move with {top|left|right|bottom}</td> 307 * </tr> 308 * <tr> 309 * <td>maxVelocity</td> 310 * <td>limit the maximum velocity (in progress/sec) of the animation will on touch up. 311 * Default 4</td> 312 * </tr> 313 * <tr> 314 * <td>dragDirection</td> 315 * <td>which side to swipe from {dragUp|dragDown|dragLeft|dragRight}</td> 316 * </tr> 317 * <tr> 318 * <td>maxAcceleration</td> 319 * <td>how quickly the animation will accelerate 320 * (progress/sec/sec) and decelerate on touch up. Default 1.2</td> 321 * </tr> 322 * <tr> 323 * <td>dragScale</td> 324 * <td>scale factor to adjust the swipe by. (e.g. 0.5 would require you to move 2x as much)</td> 325 * </tr> 326 * <td>dragThreshold</td> 327 * <td>How much to drag before swipe gesture runs. 328 * Important for mult-direction swipe. Default is 10. 1 is very sensitive.</td> 329 * </tr> 330 * <tr> 331 * <td>moveWhenScrollAtTop</td> 332 * <td>If the swipe is scrolling and View (such as RecyclerView or NestedScrollView) 333 * do scroll and transition happen at the same time</td> 334 * </tr> 335 * <tr> 336 * <td>onTouchUp</td> 337 * <td>Support for various swipe modes 338 * autoComplete,autoCompleteToStart,autoCompleteToEnd,stop,decelerate,decelerateAndComplete</td> 339 * </tr> 340 * </table> 341 * 342 * <p> 343 * <h2>OnClick (optional)</h2> 344 * <table summary="OnClick attributes"> 345 * <tr> 346 * <th>Attributes</th><th>Description</th> 347 * </tr> 348 * <tr> 349 * <td>motionTarget</td> 350 * <td>What view triggers Transition.</td> 351 * </tr> 352 * <tr> 353 * <td>clickAction</td> 354 * <td>Direction for buttons to move the animation. 355 * Or (|) combination of: toggle, transitionToEnd, transitionToStart, jumpToEnd, jumpToStart</td> 356 * </tr> 357 * </table> 358 * 359 * <p> 360 * <h2>StateSet</h2> 361 * <table summary="StateSet tags & attributes"> 362 * <tr> 363 * <td>defaultState</td> 364 * <td>The constraint set or layout to use</td> 365 * </tr> 366 * <tr> 367 * <td>{@code <State> }</td> 368 * <td>The side of the object to move</td> 369 * </tr> 370 * </table> 371 * 372 * <p> 373 * <h2>State</h2> 374 * <table summary="State attributes"> 375 * <tr> 376 * <td>android:id</td> 377 * <td>Id of the State</td> 378 * </tr> 379 * <tr> 380 * <td>constraints</td> 381 * <td>Id of the ConstraintSet or the Layout file</td> 382 * </tr> 383 * <tr> 384 * <td>{@code <Variant> }</td> 385 * <td>a different constraintSet/layout to choose if the with or height matches</td> 386 * </tr> 387 * </table> 388 * 389 * <h2>Variant</h2> 390 * <table summary="Variant attributes" > 391 * <tr> 392 * <td>region_widthLessThan</td> 393 * <td>Match if width less than</td> 394 * </tr> 395 * <tr> 396 * <td>region_widthMoreThan</td> 397 * <td>Match if width more than</td> 398 * </tr> 399 * <tr> 400 * <td>region_heightLessThan</td> 401 * <td>Match if height less than</td> 402 * </tr> 403 * <tr> 404 * <td>region_heightMoreThan</td> 405 * <td>Match if height more than</td> 406 * </tr> 407 * <tr> 408 * <td>constraints</td> 409 * <td>Id of the ConstraintSet or layout</td> 410 * </tr> 411 * </table> 412 * 413 * <p> 414 * <h2>ConstraintSet</h2> 415 * <table summary="StateSet tags & attributes"> 416 * <tr> 417 * <td>android:id</td> 418 * <td>The id of the ConstraintSet</td> 419 * </tr> 420 * <tr> 421 * <td>deriveConstraintsFrom</td> 422 * <td>The id of another constraintSet which defines the constraints not define in this set. 423 * If not specified the layout defines the undefined constraints.</td> 424 * </tr> 425 * <tr> 426 * <td>{@code <Constraint> }</td> 427 * <td>A ConstraintLayout Constraints + other attributes associated with a view</td> 428 * </tr> 429 * </table> 430 * 431 * <p> 432 * <h2>Constraint</h2> 433 * <p> Constraint supports two forms: <p>1: All of ConstraintLayout + the ones listed below + 434 * {@code <CustomAttribute> }.</p> 435 * <p>Or</p><p> 436 * 2: Combination of tags: {@code <Layout> <PropertySet> <Transform> <Motion> <CustomAttribute> }. 437 * The advantage of using these is that if not present the attributes are taken from the base 438 * layout file. This saves from replicating all the layout tags if only a Motion tag is needed. 439 * If <Layout> is used then all layout attributes in the base are ignored. </p> 440 * </p> 441 * <table summary="Constraint attributes"> 442 * <tr> 443 * <td>android:id</td> 444 * <td>Id of the View</td> 445 * </tr> 446 * <tr> 447 * <td>[ConstraintLayout attributes]</td> 448 * <td>Any attribute that is part of ConstraintLayout layout is allowed</td> 449 * </tr> 450 * <tr> 451 * <td>[Standard View attributes]</td> 452 * <td>A collection of view attributes supported by the system (see below)</td> 453 * </tr> 454 * <tr> 455 * <td>transitionEasing</td> 456 * <td>define an easing curve to be used when animating from this point 457 * (e.g. {@code curve(1.0,0,0,1.0)}) 458 * or key words {standard | accelerate | decelerate | linear}</td> 459 * </tr> 460 * <tr> 461 * <td>pathMotionArc</td> 462 * <td>the path will move in arc (quarter ellipses) 463 * or key words {startVertical | startHorizontal | none }</td> 464 * </tr> 465 * <tr> 466 * <td>transitionPathRotate</td> 467 * <td>(float) rotate object relative to path taken</td> 468 * </tr> 469 * <tr> 470 * <td>drawPath</td> 471 * <td>draw the path the layout will animate animate</td> 472 * </tr> 473 * <tr> 474 * <td>progress</td> 475 * <td>call method setProgress(float) on this view 476 * (used to talk to nested ConstraintLayouts etc.)</td> 477 * </tr> 478 * <tr> 479 * <td>{@code <CustomAttribute> }</td> 480 * <td>call a set"name" method via reflection</td> 481 * </tr> 482 * <tr> 483 * <td>{@code <Layout> }</td> 484 * <td>Attributes for the ConstraintLayout e.g. layout_constraintTop_toTopOf</td> 485 * </tr> 486 * <tr> 487 * <td>{@code <PropertySet> }</td> 488 * <td>currently only visibility, alpha, motionProgress,layout_constraintTag.</td> 489 * </tr> 490 * <tr> 491 * <td>{@code <Transform> }</td> 492 * <td>All the view transform API such as android:rotation.</td> 493 * </tr> 494 * <tr> 495 * <td>{@code <Motion> }</td> 496 * <td>Motion Layout control commands such as transitionEasing and pathMotionArc</td> 497 * </tr> 498 * </table> 499 * 500 * <p> 501 * <p> 502 * <h2>Layout</h2> 503 * <table summary="Variant attributes" > 504 * <tr> 505 * <td>[ConstraintLayout attributes]</td> 506 * <td>Also see {@link ConstraintLayout.LayoutParams 507 * ConstraintLayout.LayoutParams} for attributes</td> 508 * </tr> 509 * </table> 510 * 511 * <h2>PropertySet</h2> 512 * <table summary="Variant attributes" > 513 * <tr> 514 * <td>visibility</td> 515 * <td>set the Visibility of the view. One of Visible, invisible or gone</td> 516 * </tr> 517 * <tr> 518 * <td>alpha</td> 519 * <td>setAlpha value</td> 520 * </tr> 521 * <tr> 522 * <td>motionProgress</td> 523 * <td>using reflection call setProgress</td> 524 * </tr> 525 * <tr> 526 * <td>layout_constraintTag</td> 527 * <td>a tagging string to identify the type of object</td> 528 * </tr> 529 * </table> 530 * 531 * 532 * <h2>Transform</h2> 533 * <table summary="Variant attributes" > 534 * <tr> 535 * <td>android:elevation</td> 536 * <td>base z depth of the view.</td> 537 * </tr> 538 * <tr> 539 * <td>android:rotation</td> 540 * <td>rotation of the view, in degrees.</td> 541 * </tr> 542 * <tr> 543 * <td>android:rotationX</td> 544 * <td>rotation of the view around the x axis, in degrees.</td> 545 * </tr> 546 * <tr> 547 * <td>android:rotationY</td> 548 * <td>rotation of the view around the y axis, in degrees.</td> 549 * </tr> 550 * <tr> 551 * <td>android:scaleX</td> 552 * <td>scale of the view in the x direction</td> 553 * </tr> 554 * <tr> 555 * <td>android:scaleY</td> 556 * <td>scale of the view in the y direction.</td> 557 * </tr> 558 * <tr> 559 * <td>android:translationX</td> 560 * <td>translation in x of the view. This value is added post-layout to the left 561 * property of the view, which is set by its layout.</td> 562 * </tr> 563 * <tr> 564 * <td>android:translationY</td> 565 * <td>translation in y of the view. This value is added post-layout to th e top 566 * property of the view, which is set by its layout</td> 567 * </tr> 568 * <tr> 569 * <td>android:translationZ</td> 570 * <td>translation in z of the view. This value is added to its elevation.</td> 571 * </tr> 572 * </table> 573 * 574 * 575 * <h2>Motion</h2> 576 * <table summary="Variant attributes" > 577 * <tr> 578 * <td>transitionEasing</td> 579 * <td>Defines an acceleration curve.</td> 580 * </tr> 581 * <tr> 582 * <td>pathMotionArc</td> 583 * <td>Says the object should move in a quarter ellipse 584 * unless the motion is vertical or horizontal</td> 585 * </tr> 586 * <tr> 587 * <td>motionPathRotate</td> 588 * <td>set the rotation to the path of the object + this angle.</td> 589 * </tr> 590 * <tr> 591 * <td>drawPath</td> 592 * <td>Debugging utility to draw the motion of the path</td> 593 * </tr> 594 * </table> 595 * 596 * 597 * <h2>CustomAttribute</h2> 598 * <table summary="Variant attributes" > 599 * <tr> 600 * <td>attributeName</td> 601 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td> 602 * </tr> 603 * <tr> 604 * <td>customColorValue</td> 605 * <td>The value is a color looking setMyAttr(int )</td> 606 * </tr> 607 * <tr> 608 * <td>customIntegerValue</td> 609 * <td>The value is an integer looking setMyAttr(int )</td> 610 * </tr> 611 * <tr> 612 * <td>customFloatValue</td> 613 * <td>The value is a float looking setMyAttr(float )</td> 614 * </tr> 615 * <tr> 616 * <td>customStringValue</td> 617 * <td>The value is a String looking setMyAttr(String )</td> 618 * </tr> 619 * <tr> 620 * <td>customDimension</td> 621 * <td>The value is a dimension looking setMyAttr(float )</td> 622 * </tr> 623 * <tr> 624 * <td>customBoolean</td> 625 * <td>The value is true or false looking setMyAttr(boolean )</td> 626 * </tr> 627 * </table> 628 * 629 * <p> 630 * <p> 631 * <h2>KeyFrameSet</h2> 632 * <p> This is the container for a collection of Key objects (such as KeyPosition) which provide 633 * information about how the views should move </p> 634 * <table summary="StateSet tags & attributes"> 635 * <tr> 636 * <td>{@code <KeyPosition>}</td> 637 * <td>Controls the layout position during animation</td> 638 * </tr> 639 * <tr> 640 * <td>{@code <KeyAttribute>}</td> 641 * <td>Controls the post layout properties during animation</td> 642 * </tr> 643 * <tr> 644 * <td>{@code <KeyCycle>}</td> 645 * <td>Controls oscillations with respect to position 646 * of post layout properties during animation</td> 647 * </tr> 648 * <tr> 649 * <td>{@code <KeyTimeCycle>}</td> 650 * <td>Controls oscillations with respect to time of post layout properties during animation</td> 651 * </tr> 652 * <tr> 653 * <td>{@code <KeyTrigger>}</td> 654 * <td>trigger callbacks into code at fixed point during the animation</td> 655 * </tr> 656 * </table> 657 * 658 * <p> 659 * <h2>KeyPosition</h2> 660 * <table summary="KeyPosition attributes"> 661 * <tr> 662 * <td>motionTarget</td> 663 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td> 664 * </tr> 665 * <tr> 666 * <td>framePosition</td> 667 * <td>The point along the interpolation 0 = start 100 = end</td> 668 * </tr 669 * <tr> 670 * <td>transitionEasing</td> 671 * <td>define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0, 1.0)) 672 * or key words {standard | accelerate | decelerate | linear } 673 * </td> 674 * </tr> 675 * <tr> 676 * <td>pathMotionArc</td> 677 * <td>The path will move in arc (quarter ellipses) 678 * key words {startVertical | startHorizontal | flip | none }</td> 679 * </tr> 680 * <tr> 681 * <td>keyPositionType</td> 682 * <td>how this keyframe's deviation for linear path is calculated 683 * {deltaRelative | pathRelative|parentRelative}</td> 684 * </tr> 685 * <tr> 686 * <td>percentX</td> 687 * <td>(float) percent distance from start to end along 688 * X axis (deltaRelative) or along the path in pathRelative</td> 689 * </tr> 690 * <tr> 691 * <td>percentY</td> 692 * <td>(float) Percent distance from start to end along Y axis 693 * (deltaRelative) or perpendicular to path in pathRelative</td> 694 * </tr> 695 * <tr> 696 * <td>percentWidth</td> 697 * <td>(float) Percent of change in the width. 698 * Note if the width does not change this has no effect.This overrides sizePercent.</td> 699 * </tr> 700 * <tr> 701 * <td>percentHeight</td> 702 * <td>(float) Percent of change in the width. 703 * Note if the width does not change this has no effect.This overrides sizePercent.</td> 704 * </tr> 705 * <tr> 706 * <td>curveFit</td> 707 * <td>path is traced</td> 708 * </tr> 709 * <tr> 710 * <td>drawPath</td> 711 * <td>Draw the path of the objects layout takes useful for debugging</td> 712 * </tr> 713 * <tr> 714 * <td>sizePercent</td> 715 * <td>If the view changes size this controls how growth of the size. 716 * (for fixed size objects use KeyAttributes scaleX/X)</td> 717 * </tr> 718 * <tr> 719 * <td>curveFit</td> 720 * <td>selects a path based on straight lines or a path based on a 721 * monotonic spline {linear|spline}</td> 722 * </tr> 723 * </table> 724 * 725 * <p> 726 * <p> 727 * <h2>KeyAttribute</h2> 728 * <table summary="KeyAttribute attributes"> 729 * <tr> 730 * <td>motionTarget</td> 731 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td> 732 * </tr> 733 * <tr> 734 * <td>framePosition</td> 735 * <td>The point along the interpolation 0 = start 100 = end</td> 736 * </tr> 737 * <tr> 738 * <td>curveFit</td> 739 * <td>selects a path based on straight lines or a path 740 * based on a monotonic spline {linear|spline}</td> 741 * </tr> 742 * <tr> 743 * <td>transitionEasing</td> 744 * <td>Define an easing curve to be used when animating from this point (e.g. curve(1.0,0,0, 1.0)) 745 * or key words {standard | accelerate | decelerate | linear } 746 * </td> 747 * </tr> 748 * <tr> 749 * <td>transitionPathRotate</td> 750 * <td>(float) rotate object relative to path taken</td> 751 * </tr> 752 * <tr> 753 * <td>drawPath</td> 754 * <td>draw the path the layout will animate animate</td> 755 * </tr> 756 * <tr> 757 * <td>motionProgress</td> 758 * <td>call method setProgress(float) on this view 759 * (used to talk to nested ConstraintLayouts etc.)</td> 760 * </tr> 761 * <tr> 762 * <td>[standard view attributes](except visibility)</td> 763 * <td>A collection of post layout view attributes see below </td> 764 * </tr> 765 * <tr> 766 * <p> 767 * <tr> 768 * <td>{@code <CustomAttribute> }</td> 769 * <td>call a set"name" method via reflection</td> 770 * </tr> 771 * </table> 772 * 773 * <h2>CustomAttribute</h2> 774 * <table summary="Variant attributes" > 775 * <tr> 776 * <td>attributeName</td> 777 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td> 778 * </tr> 779 * <tr> 780 * <td>customColorValue</td> 781 * <td>The value is a color looking setMyAttr(int )</td> 782 * </tr> 783 * <tr> 784 * <td>customIntegerValue</td> 785 * <td>The value is an integer looking setMyAttr(int )</td> 786 * </tr> 787 * <tr> 788 * <td>customFloatValue</td> 789 * <td>The value is a float looking setMyAttr(float )</td> 790 * </tr> 791 * <tr> 792 * <td>customStringValue</td> 793 * <td>The value is a String looking setMyAttr(String )</td> 794 * </tr> 795 * <tr> 796 * <td>customDimension</td> 797 * <td>The value is a dimension looking setMyAttr(float )</td> 798 * </tr> 799 * <tr> 800 * <td>customBoolean</td> 801 * <td>The value is true or false looking setMyAttr(boolean )</td> 802 * </tr> 803 * </table> 804 * 805 * </p> 806 * <p> 807 * <h2>KeyCycle</h2> 808 * <table summary="Constraint attributes"> 809 * <tr> 810 * <td>motionTarget</td> 811 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td> 812 * </tr> 813 * <tr> 814 * <td>framePosition</td> 815 * <td>The point along the interpolation 0 = start 100 = end</td> 816 * </tr> 817 * <tr> 818 * <td>[Standard View attributes]</td> 819 * <td>A collection of view attributes supported by the system (see below)</td> 820 * </tr> 821 * <tr> 822 * <td>waveShape</td> 823 * <td>The shape of the wave to generate 824 * {sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce}</td> 825 * </tr> 826 * <tr> 827 * <td>wavePeriod</td> 828 * <td>The number of cycles to loop near this region</td> 829 * </tr> 830 * <tr> 831 * <td>waveOffset</td> 832 * <td>offset value added to the attribute</td> 833 * </tr> 834 * <tr> 835 * <td>transitionPathRotate</td> 836 * <td>Cycles applied to rotation relative to the path the view is travelling</td> 837 * </tr> 838 * <tr> 839 * <td>progress</td> 840 * <td>call method setProgress(float) on this view 841 * (used to talk to nested ConstraintLayouts etc.)</td> 842 * </tr> 843 * <tr> 844 * <td>{@code <CustomAttribute> }</td> 845 * <td>call a set"name" method via reflection (limited to floats)</td> 846 * </tr> 847 * </table> 848 * 849 * <h2>CustomAttribute</h2> 850 * <table summary="Variant attributes" > 851 * <tr> 852 * <td>attributeName</td> 853 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td> 854 * </tr> 855 * <tr> 856 * <tr> 857 * <td>customFloatValue</td> 858 * <td>The value is a float looking setMyAttr(float )</td> 859 * </tr> 860 * <tr> 861 * </table> 862 * 863 * <h2>KeyTimeCycle</h2> 864 * <table summary="Constraint attributes"> 865 * <tr> 866 * <td>motionTarget</td> 867 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td> 868 * </tr> 869 * <tr> 870 * <td>framePosition</td> 871 * <td>The point along the interpolation 0 = start 100 = end</td> 872 * </tr> 873 * <tr> 874 * <td>[Standard View attributes]</td> 875 * <td>A collection of view attributes supported by the system (see below)</td> 876 * </tr> 877 * <tr> 878 * <td>waveShape</td> 879 * <td>The shape of the wave to generate 880 * {sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce}</td> 881 * </tr> 882 * <tr> 883 * <td>wavePeriod</td> 884 * <td>The number of cycles per second</td> 885 * </tr> 886 * <tr> 887 * <td>waveOffset</td> 888 * <td>offset value added to the attribute</td> 889 * </tr> 890 * <tr> 891 * <td>transitionPathRotate</td> 892 * <td>Cycles applied to rotation relative to the path the view is travelling</td> 893 * </tr> 894 * <tr> 895 * <td>progress</td> 896 * <td>call method setProgress(float) on this view 897 * (used to talk to nested ConstraintLayouts etc.)</td> 898 * </tr> 899 * <tr> 900 * <td>{@code <CustomAttribute> }</td> 901 * <td>call a set"name" method via reflection (limited to floats)</td> 902 * </tr> 903 * </table> 904 * 905 * <h2>CustomAttribute</h2> 906 * <table summary="Variant attributes" > 907 * <tr> 908 * <td>attributeName</td> 909 * <td>The name of the attribute. Case sensitive. ( MyAttr will look for method setMyAttr(...)</td> 910 * </tr> 911 * <tr> 912 * <tr> 913 * <td>customFloatValue</td> 914 * <td>The value is a float looking setMyAttr(float )</td> 915 * </tr> 916 * <tr> 917 * </table> 918 * 919 * <h2>KeyTrigger</h2> 920 * <table summary="KeyTrigger attributes"> 921 * <tr> 922 * <td>motionTarget</td> 923 * <td>Id of the View or a regular expression to match layout_ConstraintTag</td> 924 * </tr> 925 * <tr> 926 * <td>framePosition</td> 927 * <td>The point along the interpolation 0 = start 100 = end</td> 928 * </tr 929 * <tr> 930 * <td>onCross</td> 931 * <td>(method name) on crossing this position call this methods on the t arget 932 * </td> 933 * </tr> 934 * <tr> 935 * <td>onPositiveCross</td> 936 * <td>(method name) on forward crossing of the framePosition call this methods on the target</td> 937 * </tr> 938 * <tr> 939 * <td>onNegativeCross/td> 940 * <td>(method name) backward crossing of the framePosition call this methods on the target</td> 941 * </tr> 942 * <tr> 943 * <td>viewTransitionOnCross</td> 944 * <td>(ViewTransition Id) start a NoState view transition on crossing or hitting target 945 * </td> 946 * </tr> 947 * <tr> 948 * <td>viewTransitionOnPositiveCross</td> 949 * <td>(ViewTransition Id) start a NoState view transition forward crossing of the 950 * framePosition or entering target</td> 951 * </tr> 952 * <tr> 953 * <td>viewTransitionOnNegativeCross/td> 954 * <td>(ViewTransition Id) start a NoState view transition backward crossing of the 955 * framePosition or leaving target</td> 956 * </tr> 957 * <tr> 958 * <td>triggerSlack</td> 959 * <td>(float) do not call trigger again if the framePosition has not moved this 960 * fraction away from the trigger point</td> 961 * </tr> 962 * <tr> 963 * <td>triggerId</td> 964 * <td>(id) call the TransitionListener with this trigger id</td> 965 * </tr> 966 * <tr> 967 * <td>motion_postLayoutCollision</td> 968 * <td>Define motion pre or post layout. Post layout is more expensive but captures 969 * KeyAttributes or KeyCycle motions.</td> 970 * </tr> 971 * <tr> 972 * <td>motion_triggerOnCollision</td> 973 * <td>(id) Trigger if the motionTarget collides with the other motionTarget</td> 974 * </tr> 975 * </table> 976 * 977 * </p> 978 * <p> 979 * <h2>Standard attributes</h2> 980 * <table summary="Constraint attributes"> 981 * <tr> 982 * <td>android:visibility</td> 983 * <td>Android view attribute that</td> 984 * </tr> 985 * <tr> 986 * <td>android:alpha</td> 987 * <td>Android view attribute that</td> 988 * </tr> 989 * <tr> 990 * <td>android:elevation</td> 991 * <td>base z depth of the view.</td> 992 * </tr> 993 * <tr> 994 * <td>android:rotation</td> 995 * <td>rotation of the view, in degrees.</td> 996 * </tr> 997 * <tr> 998 * <td>android:rotationX</td> 999 * <td>rotation of the view around the x axis, in degrees.</td> 1000 * </tr> 1001 * <tr> 1002 * <td>android:rotationY</td> 1003 * <td>rotation of the view around the y axis, in degrees.</td> 1004 * </tr> 1005 * <tr> 1006 * <td>android:scaleX</td> 1007 * <td>scale of the view in the x direction.</td> 1008 * </tr> 1009 * <tr> 1010 * <td>android:scaleY</td> 1011 * <td>scale of the view in the y direction.</td> 1012 * </tr> 1013 * <tr> 1014 * <td>android:translationX</td> 1015 * <td>translation in x of the view.</td> 1016 * </tr> 1017 * <tr> 1018 * <td>android:translationY</td> 1019 * <td>translation in y of the view.</td> 1020 * </tr> 1021 * <tr> 1022 * <td>android:translationZ</td> 1023 * <td>translation in z of the view.</td> 1024 * </tr> 1025 * <p> 1026 * </table> 1027 * 1028 * </p> 1029 */ 1030 public class MotionLayout extends ConstraintLayout implements 1031 NestedScrollingParent3 { 1032 1033 public static final int TOUCH_UP_COMPLETE = 0; 1034 public static final int TOUCH_UP_COMPLETE_TO_START = 1; 1035 public static final int TOUCH_UP_COMPLETE_TO_END = 2; 1036 public static final int TOUCH_UP_STOP = 3; 1037 public static final int TOUCH_UP_DECELERATE = 4; 1038 public static final int TOUCH_UP_DECELERATE_AND_COMPLETE = 5; 1039 public static final int TOUCH_UP_NEVER_TO_START = 6; 1040 public static final int TOUCH_UP_NEVER_TO_END = 7; 1041 1042 static final String TAG = "MotionLayout"; 1043 private static final boolean DEBUG = false; 1044 1045 public static boolean IS_IN_EDIT_MODE; 1046 1047 MotionScene mScene; 1048 Interpolator mInterpolator; 1049 Interpolator mProgressInterpolator = null; 1050 float mLastVelocity = 0; 1051 private int mBeginState = UNSET; 1052 int mCurrentState = UNSET; 1053 private int mEndState = UNSET; 1054 private int mLastWidthMeasureSpec = 0; 1055 private int mLastHeightMeasureSpec = 0; 1056 private boolean mInteractionEnabled = true; 1057 1058 HashMap<View, MotionController> mFrameArrayList = new HashMap<>(); 1059 1060 private long mAnimationStartTime = 0; 1061 private float mTransitionDuration = 1f; 1062 float mTransitionPosition = 0.0f; 1063 float mTransitionLastPosition = 0.0f; 1064 private long mTransitionLastTime; 1065 float mTransitionGoalPosition = 0.0f; 1066 private boolean mTransitionInstantly; 1067 boolean mInTransition = false; 1068 boolean mIndirectTransition = false; 1069 private TransitionListener mTransitionListener; 1070 private float mLastPos; 1071 private float mLastY; 1072 public static final int DEBUG_SHOW_NONE = 0; 1073 public static final int DEBUG_SHOW_PROGRESS = 1; 1074 public static final int DEBUG_SHOW_PATH = 2; 1075 int mDebugPath = 0; 1076 // variable used in painting the debug 1077 static final int MAX_KEY_FRAMES = 50; 1078 DevModeDraw mDevModeDraw; 1079 private boolean mTemporalInterpolator = false; 1080 private StopLogic mStopLogic = new StopLogic(); 1081 private DecelerateInterpolator mDecelerateLogic = new DecelerateInterpolator(); 1082 1083 private DesignTool mDesignTool; 1084 1085 boolean mFirstDown = true; 1086 1087 int mOldWidth; 1088 int mOldHeight; 1089 int mLastLayoutWidth; 1090 int mLastLayoutHeight; 1091 1092 boolean mUndergoingMotion = false; 1093 float mScrollTargetDX; 1094 float mScrollTargetDY; 1095 long mScrollTargetTime; 1096 float mScrollTargetDT; 1097 private boolean mKeepAnimating = false; 1098 1099 private ArrayList<MotionHelper> mOnShowHelpers = null; 1100 private ArrayList<MotionHelper> mOnHideHelpers = null; 1101 private ArrayList<MotionHelper> mDecoratorsHelpers = null; 1102 private CopyOnWriteArrayList<TransitionListener> mTransitionListeners = null; 1103 private int mFrames = 0; 1104 private long mLastDrawTime = -1; 1105 private float mLastFps = 0; 1106 private int mListenerState = 0; 1107 private float mListenerPosition = 0.0f; 1108 boolean mIsAnimating = false; 1109 1110 public static final int VELOCITY_POST_LAYOUT = 0; 1111 public static final int VELOCITY_LAYOUT = 1; 1112 public static final int VELOCITY_STATIC_POST_LAYOUT = 2; 1113 public static final int VELOCITY_STATIC_LAYOUT = 3; 1114 1115 protected boolean mMeasureDuringTransition = false; 1116 int mStartWrapWidth; 1117 int mStartWrapHeight; 1118 int mEndWrapWidth; 1119 int mEndWrapHeight; 1120 int mWidthMeasureMode; 1121 int mHeightMeasureMode; 1122 float mPostInterpolationPosition; 1123 private KeyCache mKeyCache = new KeyCache(); 1124 private boolean mInLayout = false; 1125 private StateCache mStateCache; 1126 private Runnable mOnComplete = null; 1127 private int[] mScheduledTransitionTo = null; 1128 int mScheduledTransitions = 0; 1129 private boolean mInRotation = false; 1130 int mRotatMode = 0; 1131 HashMap<View, ViewState> mPreRotate = new HashMap<>(); 1132 private int mPreRotateWidth; 1133 private int mPreRotateHeight; 1134 private int mPreviouseRotation; 1135 Rect mTempRect = new Rect(); 1136 private boolean mDelayedApply = false; 1137 getMotionController(int mTouchAnchorId)1138 MotionController getMotionController(int mTouchAnchorId) { 1139 return mFrameArrayList.get(findViewById(mTouchAnchorId)); 1140 } 1141 1142 enum TransitionState { 1143 UNDEFINED, 1144 SETUP, 1145 MOVING, 1146 FINISHED; 1147 }; 1148 1149 TransitionState mTransitionState = TransitionState.UNDEFINED; 1150 private static final float EPSILON = 0.00001f; 1151 MotionLayout(@onNull Context context)1152 public MotionLayout(@NonNull Context context) { 1153 super(context); 1154 init(null); 1155 } 1156 MotionLayout(@onNull Context context, @Nullable AttributeSet attrs)1157 public MotionLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 1158 super(context, attrs); 1159 init(attrs); 1160 } 1161 MotionLayout(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)1162 public MotionLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 1163 super(context, attrs, defStyleAttr); 1164 init(attrs); 1165 } 1166 1167 /** 1168 * Subclasses can override to define testClasses 1169 * 1170 * @return 1171 */ getNanoTime()1172 protected long getNanoTime() { 1173 return System.nanoTime(); 1174 } 1175 1176 /** 1177 * Subclasses can override to build test frameworks 1178 * 1179 * @return 1180 */ obtainVelocityTracker()1181 protected MotionTracker obtainVelocityTracker() { 1182 return MyTracker.obtain(); 1183 } 1184 1185 /** 1186 * Disable the transition based on transitionID 1187 * @param transitionID 1188 * @param enable 1189 */ enableTransition(int transitionID, boolean enable)1190 public void enableTransition(int transitionID, boolean enable) { 1191 MotionScene.Transition t = getTransition(transitionID); 1192 if (enable) { 1193 t.setEnabled(true); 1194 return; 1195 } else { 1196 if (t == mScene.mCurrentTransition) { // disabling current transition 1197 List<MotionScene.Transition> transitions = 1198 mScene.getTransitionsWithState(mCurrentState); 1199 for (MotionScene.Transition transition : transitions) { 1200 if (transition.isEnabled()) { 1201 mScene.mCurrentTransition = transition; 1202 break; 1203 } 1204 } 1205 } 1206 t.setEnabled(false); 1207 } 1208 } 1209 1210 /** 1211 * Subclasses can override to build test frameworks 1212 */ 1213 protected interface MotionTracker { recycle()1214 void recycle(); 1215 clear()1216 void clear(); 1217 addMovement(MotionEvent event)1218 void addMovement(MotionEvent event); 1219 computeCurrentVelocity(int units)1220 void computeCurrentVelocity(int units); 1221 computeCurrentVelocity(int units, float maxVelocity)1222 void computeCurrentVelocity(int units, float maxVelocity); 1223 getXVelocity()1224 float getXVelocity(); 1225 getYVelocity()1226 float getYVelocity(); 1227 getXVelocity(int id)1228 float getXVelocity(int id); 1229 getYVelocity(int id)1230 float getYVelocity(int id); 1231 } 1232 setState(TransitionState newState)1233 void setState(TransitionState newState) { 1234 if (DEBUG) { 1235 Debug.logStack(TAG, mTransitionState + " -> " + newState + " " 1236 + Debug.getName(getContext(), mCurrentState), 2); 1237 } 1238 if (newState == TransitionState.FINISHED && mCurrentState == UNSET) { 1239 return; 1240 } 1241 TransitionState oldState = mTransitionState; 1242 mTransitionState = newState; 1243 1244 if (oldState == TransitionState.MOVING && newState == TransitionState.MOVING) { 1245 fireTransitionChange(); 1246 } 1247 switch (oldState) { 1248 case UNDEFINED: 1249 case SETUP: 1250 if (newState == TransitionState.MOVING) { 1251 fireTransitionChange(); 1252 } 1253 if (newState == TransitionState.FINISHED) { 1254 fireTransitionCompleted(); 1255 } 1256 break; 1257 case MOVING: 1258 if (newState == TransitionState.FINISHED) { 1259 fireTransitionCompleted(); 1260 } 1261 break; 1262 case FINISHED: 1263 break; 1264 } 1265 } 1266 1267 private static class MyTracker implements MotionTracker { 1268 VelocityTracker mTracker; 1269 private static MyTracker sMe = new MyTracker(); 1270 obtain()1271 public static MyTracker obtain() { 1272 sMe.mTracker = VelocityTracker.obtain(); 1273 return sMe; 1274 } 1275 1276 @Override recycle()1277 public void recycle() { 1278 if (mTracker != null) { 1279 mTracker.recycle(); 1280 mTracker = null; // not allowed to call after recycle 1281 } 1282 } 1283 1284 @Override clear()1285 public void clear() { 1286 if (mTracker != null) { 1287 mTracker.clear(); 1288 } 1289 } 1290 1291 @Override addMovement(MotionEvent event)1292 public void addMovement(MotionEvent event) { 1293 if (mTracker != null) { 1294 mTracker.addMovement(event); 1295 } 1296 } 1297 1298 @Override computeCurrentVelocity(int units)1299 public void computeCurrentVelocity(int units) { 1300 if (mTracker != null) { 1301 mTracker.computeCurrentVelocity(units); 1302 } 1303 } 1304 1305 @Override computeCurrentVelocity(int units, float maxVelocity)1306 public void computeCurrentVelocity(int units, float maxVelocity) { 1307 if (mTracker != null) { 1308 mTracker.computeCurrentVelocity(units, maxVelocity); 1309 } 1310 } 1311 1312 @Override getXVelocity()1313 public float getXVelocity() { 1314 if (mTracker != null) { 1315 return mTracker.getXVelocity(); 1316 } 1317 return 0; 1318 } 1319 1320 @Override getYVelocity()1321 public float getYVelocity() { 1322 if (mTracker != null) { 1323 return mTracker.getYVelocity(); 1324 } 1325 return 0; 1326 } 1327 1328 @Override getXVelocity(int id)1329 public float getXVelocity(int id) { 1330 if (mTracker != null) { 1331 return mTracker.getXVelocity(id); 1332 } 1333 return 0; 1334 } 1335 1336 @Override getYVelocity(int id)1337 public float getYVelocity(int id) { 1338 if (mTracker != null) { 1339 return getYVelocity(id); 1340 } 1341 return 0; 1342 } 1343 } 1344 1345 /** 1346 * sets the state to start in. To be used during OnCreate 1347 * 1348 * @param beginId the id of the start constraint set 1349 */ setStartState(int beginId)1350 void setStartState(int beginId) { 1351 if (!isAttachedToWindow()) { 1352 if (mStateCache == null) { 1353 mStateCache = new StateCache(); 1354 } 1355 mStateCache.setStartState(beginId); 1356 mStateCache.setEndState(beginId); 1357 return; 1358 } 1359 mCurrentState = beginId; 1360 } 1361 1362 /** 1363 * Set a transition explicitly between two constraint sets 1364 * 1365 * @param beginId the id of the start constraint set 1366 * @param endId the id of the end constraint set 1367 */ setTransition(int beginId, int endId)1368 public void setTransition(int beginId, int endId) { 1369 if (!isAttachedToWindow()) { 1370 if (mStateCache == null) { 1371 mStateCache = new StateCache(); 1372 } 1373 mStateCache.setStartState(beginId); 1374 mStateCache.setEndState(endId); 1375 return; 1376 } 1377 1378 if (mScene != null) { 1379 mBeginState = beginId; 1380 mEndState = endId; 1381 if (DEBUG) { 1382 Log.v(TAG, Debug.getLocation() + " setTransition " 1383 + Debug.getName(getContext(), beginId) + " -> " 1384 + Debug.getName(getContext(), endId)); 1385 } 1386 mScene.setTransition(beginId, endId); 1387 mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(beginId), 1388 mScene.getConstraintSet(endId)); 1389 rebuildScene(); 1390 mTransitionLastPosition = 0; 1391 transitionToStart(); 1392 } 1393 } 1394 1395 /** 1396 * Set a transition explicitly to a Transition that has an ID 1397 * The transition must have been named with android:id=... 1398 * 1399 * @param transitionId the id to set 1400 */ setTransition(int transitionId)1401 public void setTransition(int transitionId) { 1402 if (mScene != null) { 1403 MotionScene.Transition transition = getTransition(transitionId); 1404 mBeginState = transition.getStartConstraintSetId(); 1405 mEndState = transition.getEndConstraintSetId(); 1406 1407 if (!isAttachedToWindow()) { 1408 if (mStateCache == null) { 1409 mStateCache = new StateCache(); 1410 } 1411 mStateCache.setStartState(mBeginState); 1412 mStateCache.setEndState(mEndState); 1413 return; 1414 } 1415 1416 if (DEBUG) { 1417 Log.v(TAG, Debug.getLocation() + " setTransition " 1418 + Debug.getName(getContext(), mBeginState) + " -> " 1419 + Debug.getName(getContext(), mEndState) 1420 + " current=" + Debug.getName(getContext(), mCurrentState)); 1421 } 1422 1423 float pos = Float.NaN; 1424 if (mCurrentState == mBeginState) { 1425 pos = 0; 1426 } else if (mCurrentState == mEndState) { 1427 pos = 1; 1428 } 1429 mScene.setTransition(transition); 1430 mModel.initFrom(mLayoutWidget, 1431 mScene.getConstraintSet(mBeginState), 1432 mScene.getConstraintSet(mEndState)); 1433 rebuildScene(); 1434 1435 if (mTransitionLastPosition != pos) { 1436 // If the last drawn position isn't the same, 1437 // we might have to make sure we apply the corresponding constraintset. 1438 if (pos == 0) { 1439 endTrigger(true); 1440 mScene.getConstraintSet(mBeginState).applyTo(this); 1441 } else if (pos == 1) { 1442 endTrigger(false); 1443 mScene.getConstraintSet(mEndState).applyTo(this); 1444 } 1445 } 1446 1447 mTransitionLastPosition = Float.isNaN(pos) ? 0 : pos; 1448 1449 if (Float.isNaN(pos)) { 1450 Log.v(TAG, Debug.getLocation() + " transitionToStart "); 1451 transitionToStart(); 1452 } else { 1453 setProgress(pos); 1454 } 1455 } 1456 } 1457 setTransition(MotionScene.Transition transition)1458 protected void setTransition(MotionScene.Transition transition) { 1459 mScene.setTransition(transition); 1460 setState(TransitionState.SETUP); 1461 if (mCurrentState == mScene.getEndId()) { 1462 mTransitionLastPosition = 1.0f; 1463 mTransitionPosition = 1.0f; 1464 mTransitionGoalPosition = 1; 1465 } else { 1466 mTransitionLastPosition = 0; 1467 mTransitionPosition = 0f; 1468 mTransitionGoalPosition = 0; 1469 } 1470 mTransitionLastTime = 1471 transition.isTransitionFlag(TRANSITION_FLAG_FIRST_DRAW) ? -1 : getNanoTime(); 1472 if (DEBUG) { 1473 Log.v(TAG, Debug.getLocation() + " new mTransitionLastPosition = " 1474 + mTransitionLastPosition + ""); 1475 Log.v(TAG, Debug.getLocation() + " setTransition was " 1476 + Debug.getName(getContext(), mBeginState) 1477 + " -> " + Debug.getName(getContext(), mEndState)); 1478 } 1479 int newBeginState = mScene.getStartId(); 1480 int newEndState = mScene.getEndId(); 1481 if (newBeginState == mBeginState && newEndState == mEndState) { 1482 return; 1483 } 1484 mBeginState = newBeginState; 1485 mEndState = newEndState; 1486 mScene.setTransition(mBeginState, mEndState); 1487 1488 if (DEBUG) { 1489 Log.v(TAG, Debug.getLocation() + " setTransition now " 1490 + Debug.getName(getContext(), mBeginState) + " -> " 1491 + Debug.getName(getContext(), mEndState)); 1492 } 1493 1494 mModel.initFrom(mLayoutWidget, 1495 mScene.getConstraintSet(mBeginState), 1496 mScene.getConstraintSet(mEndState)); 1497 mModel.setMeasuredId(mBeginState, mEndState); 1498 mModel.reEvaluateState(); 1499 1500 rebuildScene(); 1501 } 1502 1503 /** 1504 * This overrides ConstraintLayout and only accepts a MotionScene. 1505 * 1506 * @param motionScene The resource id, or 0 to reset the MotionScene. 1507 */ 1508 @Override loadLayoutDescription(int motionScene)1509 public void loadLayoutDescription(int motionScene) { 1510 if (motionScene != 0) { 1511 try { 1512 mScene = new MotionScene(getContext(), this, motionScene); 1513 if (mCurrentState == UNSET && mScene != null) { 1514 mCurrentState = mScene.getStartId(); 1515 mBeginState = mScene.getStartId(); 1516 mEndState = mScene.getEndId(); 1517 } 1518 if (isAttachedToWindow()) { 1519 try { 1520 Display display = getDisplay(); 1521 mPreviouseRotation = (display == null) ? 0 : display.getRotation(); 1522 1523 if (mScene != null) { 1524 ConstraintSet cSet = mScene.getConstraintSet(mCurrentState); 1525 mScene.readFallback(this); 1526 if (mDecoratorsHelpers != null) { 1527 for (MotionHelper mh : mDecoratorsHelpers) { 1528 mh.onFinishedMotionScene(this); 1529 } 1530 } 1531 if (cSet != null) { 1532 cSet.applyTo(this); 1533 } 1534 mBeginState = mCurrentState; 1535 } 1536 onNewStateAttachHandlers(); 1537 if (mStateCache != null) { 1538 if (mDelayedApply) { 1539 post(new Runnable() { 1540 @Override 1541 public void run() { 1542 mStateCache.apply(); 1543 } 1544 }); 1545 } else { 1546 mStateCache.apply(); 1547 } 1548 } else { 1549 if (mScene != null && mScene.mCurrentTransition != null) { 1550 if (mScene.mCurrentTransition.getAutoTransition() 1551 == MotionScene.Transition.AUTO_ANIMATE_TO_END) { 1552 transitionToEnd(); 1553 setState(TransitionState.SETUP); 1554 setState(TransitionState.MOVING); 1555 } 1556 } 1557 1558 } 1559 } catch (Exception ex) { 1560 throw new IllegalArgumentException("unable to parse MotionScene file", ex); 1561 } 1562 } else { 1563 mScene = null; 1564 } 1565 1566 } catch (Exception ex) { 1567 throw new IllegalArgumentException("unable to parse MotionScene file", ex); 1568 } 1569 } else { 1570 mScene = null; 1571 } 1572 } 1573 1574 /** 1575 * Set the State of the Constraint layout. Causing it to load a particular ConstraintSet. 1576 * for states with variants the variant with matching 1577 * width and height constraintSet will be chosen 1578 * 1579 * @param id set the state width and height 1580 * @param screenWidth 1581 * @param screenHeight 1582 */ 1583 @Override setState(int id, int screenWidth, int screenHeight)1584 public void setState(int id, int screenWidth, int screenHeight) { 1585 setState(TransitionState.SETUP); 1586 mCurrentState = id; 1587 mBeginState = UNSET; 1588 mEndState = UNSET; 1589 if (mConstraintLayoutSpec != null) { 1590 mConstraintLayoutSpec.updateConstraints(id, screenWidth, screenHeight); 1591 } else if (mScene != null) { 1592 mScene.getConstraintSet(id).applyTo(this); 1593 } 1594 } 1595 1596 /** 1597 * Set the transition position between 0 an 1 1598 * 1599 * @param pos 1600 */ setInterpolatedProgress(float pos)1601 public void setInterpolatedProgress(float pos) { 1602 if (mScene != null) { 1603 setState(TransitionState.MOVING); 1604 Interpolator interpolator = mScene.getInterpolator(); 1605 if (interpolator != null) { 1606 setProgress(interpolator.getInterpolation(pos)); 1607 return; 1608 } 1609 } 1610 setProgress(pos); 1611 } 1612 1613 /** 1614 * Set the transition position between 0 an 1 1615 * 1616 * @param pos 1617 * @param velocity 1618 */ setProgress(float pos, float velocity)1619 public void setProgress(float pos, float velocity) { 1620 if (!isAttachedToWindow()) { 1621 if (mStateCache == null) { 1622 mStateCache = new StateCache(); 1623 } 1624 mStateCache.setProgress(pos); 1625 mStateCache.setVelocity(velocity); 1626 return; 1627 } 1628 setProgress(pos); 1629 setState(TransitionState.MOVING); 1630 mLastVelocity = velocity; 1631 if (velocity != 0.0f) { 1632 animateTo(velocity > 0 ? 1 : 0); 1633 } else if (pos != 0f && pos != 1f) { 1634 animateTo(pos > 0.5f ? 1 : 0); 1635 } 1636 } 1637 1638 /////////////////////// use to cache the state 1639 class StateCache { 1640 float mProgress = Float.NaN; 1641 float mVelocity = Float.NaN; 1642 int mStartState = UNSET; 1643 int mEndState = UNSET; 1644 final String mKeyProgress = "motion.progress"; 1645 final String mKeyVelocity = "motion.velocity"; 1646 final String mKeyStartState = "motion.StartState"; 1647 final String mKeyEndState = "motion.EndState"; 1648 apply()1649 void apply() { 1650 if (this.mStartState != UNSET || this.mEndState != UNSET) { 1651 if (this.mStartState == UNSET) { 1652 transitionToState(mEndState); 1653 } else if (this.mEndState == UNSET) { 1654 setState(this.mStartState, -1, -1); 1655 } else { 1656 setTransition(mStartState, mEndState); 1657 } 1658 setState(TransitionState.SETUP); 1659 } 1660 if (Float.isNaN(this.mVelocity)) { 1661 if (Float.isNaN(this.mProgress)) { 1662 return; 1663 } 1664 MotionLayout.this.setProgress(this.mProgress); 1665 return; 1666 } 1667 MotionLayout.this.setProgress(this.mProgress, mVelocity); 1668 this.mProgress = Float.NaN; 1669 this.mVelocity = Float.NaN; 1670 this.mStartState = UNSET; 1671 this.mEndState = UNSET; 1672 } 1673 getTransitionState()1674 public Bundle getTransitionState() { 1675 Bundle bundle = new Bundle(); 1676 bundle.putFloat(mKeyProgress, this.mProgress); 1677 bundle.putFloat(mKeyVelocity, this.mVelocity); 1678 bundle.putInt(mKeyStartState, this.mStartState); 1679 bundle.putInt(mKeyEndState, this.mEndState); 1680 return bundle; 1681 } 1682 setTransitionState(Bundle bundle)1683 public void setTransitionState(Bundle bundle) { 1684 this.mProgress = bundle.getFloat(mKeyProgress); 1685 this.mVelocity = bundle.getFloat(mKeyVelocity); 1686 this.mStartState = bundle.getInt(mKeyStartState); 1687 this.mEndState = bundle.getInt(mKeyEndState); 1688 } 1689 setProgress(float progress)1690 public void setProgress(float progress) { 1691 this.mProgress = progress; 1692 } 1693 setEndState(int endState)1694 public void setEndState(int endState) { 1695 this.mEndState = endState; 1696 } 1697 setVelocity(float mVelocity)1698 public void setVelocity(float mVelocity) { 1699 this.mVelocity = mVelocity; 1700 } 1701 setStartState(int startState)1702 public void setStartState(int startState) { 1703 this.mStartState = startState; 1704 } 1705 recordState()1706 public void recordState() { 1707 mEndState = MotionLayout.this.mEndState; 1708 mStartState = MotionLayout.this.mBeginState; 1709 mVelocity = MotionLayout.this.getVelocity(); 1710 mProgress = MotionLayout.this.getProgress(); 1711 } 1712 } 1713 1714 /** 1715 * Set the transition state as a bundle 1716 */ setTransitionState(Bundle bundle)1717 public void setTransitionState(Bundle bundle) { 1718 if (mStateCache == null) { 1719 mStateCache = new StateCache(); 1720 } 1721 mStateCache.setTransitionState(bundle); 1722 if (isAttachedToWindow()) { 1723 mStateCache.apply(); 1724 } 1725 } 1726 1727 /** 1728 * @return bundle containing start and end state 1729 */ getTransitionState()1730 public Bundle getTransitionState() { 1731 if (mStateCache == null) { 1732 mStateCache = new StateCache(); 1733 } 1734 mStateCache.recordState(); 1735 return mStateCache.getTransitionState(); 1736 } 1737 1738 /** 1739 * Set the transition position between 0 an 1 1740 * 1741 * @param pos the position in the transition from 0...1 1742 */ setProgress(float pos)1743 public void setProgress(float pos) { 1744 if (pos < 0.0f || pos > 1.0f) { 1745 Log.w(TAG, "Warning! Progress is defined for values between 0.0 and 1.0 inclusive"); 1746 } 1747 if (!isAttachedToWindow()) { 1748 if (mStateCache == null) { 1749 mStateCache = new StateCache(); 1750 } 1751 mStateCache.setProgress(pos); 1752 return; 1753 } 1754 if (DEBUG) { 1755 String str = getContext().getResources().getResourceName(mBeginState) + " -> "; 1756 str += getContext().getResources().getResourceName(mEndState) + ":" + getProgress(); 1757 Log.v(TAG, Debug.getLocation() + " > " + str); 1758 Debug.logStack(TAG, " Progress = " + pos, 3); 1759 } 1760 1761 if (pos <= 0f) { 1762 if (mTransitionLastPosition == 1.0f && mCurrentState == mEndState) { 1763 setState(TransitionState.MOVING); // fire a transient moving as jumping start to end 1764 } 1765 1766 mCurrentState = mBeginState; 1767 if (mTransitionLastPosition == 0.0f) { 1768 setState(TransitionState.FINISHED); 1769 } 1770 } else if (pos >= 1.0f) { 1771 if (mTransitionLastPosition == 0.0f && mCurrentState == mBeginState) { 1772 setState(TransitionState.MOVING); // fire a transient moving as jumping end to start 1773 } 1774 1775 mCurrentState = mEndState; 1776 if (mTransitionLastPosition == 1.0f) { 1777 setState(TransitionState.FINISHED); 1778 } 1779 } else { 1780 mCurrentState = UNSET; 1781 setState(TransitionState.MOVING); 1782 } 1783 1784 if (mScene == null) { 1785 return; 1786 } 1787 1788 mTransitionInstantly = true; 1789 mTransitionGoalPosition = pos; 1790 mTransitionPosition = pos; 1791 mTransitionLastTime = -1; 1792 mAnimationStartTime = -1; 1793 mInterpolator = null; 1794 1795 mInTransition = true; 1796 invalidate(); 1797 } 1798 1799 /** 1800 * Create a transition view for every view 1801 */ setupMotionViews()1802 private void setupMotionViews() { 1803 int n = getChildCount(); 1804 1805 mModel.build(); 1806 mInTransition = true; 1807 SparseArray<MotionController> controllers = new SparseArray<>(); 1808 for (int i = 0; i < n; i++) { 1809 View child = getChildAt(i); 1810 controllers.put(child.getId(), mFrameArrayList.get(child)); 1811 } 1812 int layoutWidth = getWidth(); 1813 int layoutHeight = getHeight(); 1814 int arc = mScene.gatPathMotionArc(); 1815 if (arc != UNSET) { 1816 for (int i = 0; i < n; i++) { 1817 MotionController motionController = mFrameArrayList.get(getChildAt(i)); 1818 if (motionController != null) { 1819 motionController.setPathMotionArc(arc); 1820 } 1821 } 1822 } 1823 1824 SparseBooleanArray sparseBooleanArray = new SparseBooleanArray(); 1825 int[] depends = new int[mFrameArrayList.size()]; 1826 int count = 0; 1827 for (int i = 0; i < n; i++) { 1828 View view = getChildAt(i); 1829 MotionController motionController = mFrameArrayList.get(view); 1830 if (motionController.getAnimateRelativeTo() != UNSET) { 1831 sparseBooleanArray.put(motionController.getAnimateRelativeTo(), true); 1832 depends[count++] = motionController.getAnimateRelativeTo(); 1833 } 1834 } 1835 if (mDecoratorsHelpers != null) { 1836 for (int i = 0; i < count; i++) { 1837 MotionController motionController = mFrameArrayList.get(findViewById(depends[i])); 1838 if (motionController == null) { 1839 continue; 1840 } 1841 mScene.getKeyFrames(motionController); 1842 } 1843 // Allow helpers to access all the motionControllers after 1844 for (MotionHelper mDecoratorsHelper : mDecoratorsHelpers) { 1845 mDecoratorsHelper.onPreSetup(this, mFrameArrayList); 1846 } 1847 for (int i = 0; i < count; i++) { 1848 MotionController motionController = mFrameArrayList.get(findViewById(depends[i])); 1849 if (motionController == null) { 1850 continue; 1851 } 1852 motionController.setup(layoutWidth, layoutHeight, 1853 mTransitionDuration, getNanoTime()); 1854 } 1855 } else { 1856 for (int i = 0; i < count; i++) { 1857 MotionController motionController = mFrameArrayList.get(findViewById(depends[i])); 1858 if (motionController == null) { 1859 continue; 1860 } 1861 mScene.getKeyFrames(motionController); 1862 motionController.setup(layoutWidth, layoutHeight, 1863 mTransitionDuration, getNanoTime()); 1864 } 1865 } 1866 // getMap the KeyFrames for each view 1867 for (int i = 0; i < n; i++) { 1868 View v = getChildAt(i); 1869 MotionController motionController = mFrameArrayList.get(v); 1870 if (sparseBooleanArray.get(v.getId())) { 1871 continue; 1872 } 1873 1874 if (motionController != null) { 1875 mScene.getKeyFrames(motionController); 1876 motionController.setup(layoutWidth, layoutHeight, 1877 mTransitionDuration, getNanoTime()); 1878 } 1879 } 1880 1881 float stagger = mScene.getStaggered(); 1882 if (stagger != 0.0f) { 1883 boolean flip = stagger < 0.0; 1884 boolean useMotionStagger = false; 1885 stagger = Math.abs(stagger); 1886 float min = Float.MAX_VALUE, max = -Float.MAX_VALUE; 1887 for (int i = 0; i < n; i++) { 1888 MotionController f = mFrameArrayList.get(getChildAt(i)); 1889 if (!Float.isNaN(f.mMotionStagger)) { 1890 useMotionStagger = true; 1891 break; 1892 } 1893 float x = f.getFinalX(); 1894 float y = f.getFinalY(); 1895 float mdist = flip ? (y - x) : (y + x); 1896 min = Math.min(min, mdist); 1897 max = Math.max(max, mdist); 1898 } 1899 if (useMotionStagger) { 1900 min = Float.MAX_VALUE; 1901 max = -Float.MAX_VALUE; 1902 1903 for (int i = 0; i < n; i++) { 1904 MotionController f = mFrameArrayList.get(getChildAt(i)); 1905 if (!Float.isNaN(f.mMotionStagger)) { 1906 min = Math.min(min, f.mMotionStagger); 1907 max = Math.max(max, f.mMotionStagger); 1908 } 1909 } 1910 for (int i = 0; i < n; i++) { 1911 MotionController f = mFrameArrayList.get(getChildAt(i)); 1912 if (!Float.isNaN(f.mMotionStagger)) { 1913 1914 f.mStaggerScale = 1 / (1 - stagger); 1915 if (flip) { 1916 f.mStaggerOffset = stagger - stagger 1917 * ((max - f.mMotionStagger) / (max - min)); 1918 } else { 1919 f.mStaggerOffset = stagger - stagger 1920 * (f.mMotionStagger - min) / (max - min); 1921 } 1922 } 1923 } 1924 } else { 1925 for (int i = 0; i < n; i++) { 1926 MotionController f = mFrameArrayList.get(getChildAt(i)); 1927 float x = f.getFinalX(); 1928 float y = f.getFinalY(); 1929 float mdist = flip ? (y - x) : (y + x); 1930 f.mStaggerScale = 1 / (1 - stagger); 1931 f.mStaggerOffset = stagger - stagger * (mdist - min) / (max - min); 1932 } 1933 } 1934 } 1935 } 1936 1937 /** 1938 * @param touchUpMode behavior on touch up, can be either: 1939 * <ul> 1940 * <li>TOUCH_UP_COMPLETE (default) : will complete the transition, 1941 * picking up 1942 * automatically a correct velocity to do so</li> 1943 * <li>TOUCH_UP_STOP : will allow stopping mid-transition</li> 1944 * <li>TOUCH_UP_DECELERATE : will slowly decay, 1945 * possibly past the transition (i.e. 1946 * it will do a hard stop if unmanaged)</li> 1947 * <li>TOUCH_UP_DECELERATE_AND_COMPLETE : 1948 * will automatically pick between 1949 * TOUCH_UP_COMPLETE and TOUCH_UP_DECELERATE</li> 1950 * </ul>, 1951 * TOUCH_UP_STOP (will allow stopping 1952 * @param position animate to given position 1953 * @param currentVelocity 1954 */ 1955 public void touchAnimateTo(int touchUpMode, float position, float currentVelocity) { 1956 if (DEBUG) { 1957 Log.v(TAG, " " + Debug.getLocation() + " touchAnimateTo " 1958 + position + " " + currentVelocity); 1959 } 1960 if (mScene == null) { 1961 return; 1962 } 1963 if (mTransitionLastPosition == position) { 1964 return; 1965 } 1966 1967 mTemporalInterpolator = true; 1968 mAnimationStartTime = getNanoTime(); 1969 mTransitionDuration = mScene.getDuration() / 1000f; 1970 1971 mTransitionGoalPosition = position; 1972 mInTransition = true; 1973 1974 switch (touchUpMode) { 1975 case TOUCH_UP_COMPLETE: 1976 case TOUCH_UP_NEVER_TO_START: 1977 case TOUCH_UP_NEVER_TO_END: 1978 case TOUCH_UP_COMPLETE_TO_START: 1979 case TOUCH_UP_COMPLETE_TO_END: { 1980 if (touchUpMode == TOUCH_UP_COMPLETE_TO_START 1981 || touchUpMode == TOUCH_UP_NEVER_TO_END) { 1982 position = 0; 1983 } else if (touchUpMode == TOUCH_UP_COMPLETE_TO_END 1984 || touchUpMode == TOUCH_UP_NEVER_TO_START) { 1985 position = 1; 1986 } 1987 1988 if (mScene.getAutoCompleteMode() 1989 == TouchResponse.COMPLETE_MODE_CONTINUOUS_VELOCITY) { 1990 mStopLogic.config(mTransitionLastPosition, position, currentVelocity, 1991 mTransitionDuration, mScene.getMaxAcceleration(), 1992 mScene.getMaxVelocity()); 1993 } else { 1994 mStopLogic.springConfig(mTransitionLastPosition, position, currentVelocity, 1995 mScene.getSpringMass(), 1996 mScene.getSpringStiffiness(), 1997 mScene.getSpringDamping(), 1998 mScene.getSpringStopThreshold(), mScene.getSpringBoundary()); 1999 } 2000 2001 int currentState = mCurrentState; // TODO: remove setProgress(), temporary fix 2002 mTransitionGoalPosition = position; 2003 mCurrentState = currentState; 2004 mInterpolator = mStopLogic; 2005 } 2006 break; 2007 case TOUCH_UP_STOP: { 2008 // nothing to do 2009 } 2010 break; 2011 case TOUCH_UP_DECELERATE: { 2012 mDecelerateLogic.config(currentVelocity, mTransitionLastPosition, 2013 mScene.getMaxAcceleration()); 2014 mInterpolator = mDecelerateLogic; 2015 } 2016 break; 2017 case TOUCH_UP_DECELERATE_AND_COMPLETE: { 2018 if (willJump(currentVelocity, mTransitionLastPosition, 2019 mScene.getMaxAcceleration())) { 2020 mDecelerateLogic.config(currentVelocity, 2021 mTransitionLastPosition, mScene.getMaxAcceleration()); 2022 mInterpolator = mDecelerateLogic; 2023 } else { 2024 mStopLogic.config(mTransitionLastPosition, position, currentVelocity, 2025 mTransitionDuration, 2026 mScene.getMaxAcceleration(), mScene.getMaxVelocity()); 2027 mLastVelocity = 0; 2028 int currentState = mCurrentState; // TODO: remove setProgress(), (temporary fix) 2029 mTransitionGoalPosition = position; 2030 mCurrentState = currentState; 2031 mInterpolator = mStopLogic; 2032 } 2033 } 2034 break; 2035 } 2036 2037 mTransitionInstantly = false; 2038 mAnimationStartTime = getNanoTime(); 2039 invalidate(); 2040 } 2041 2042 /** 2043 * Allows you to use trigger spring motion touch behaviour. 2044 * You must have configured all the spring parameters in the Transition's OnSwipe 2045 * 2046 * @param position the position 0 - 1 2047 * @param currentVelocity the current velocity rate of change in position per second 2048 */ 2049 public void touchSpringTo(float position, float currentVelocity) { 2050 if (DEBUG) { 2051 Log.v(TAG, " " + Debug.getLocation() 2052 + " touchAnimateTo " + position + " " + currentVelocity); 2053 } 2054 if (mScene == null) { 2055 return; 2056 } 2057 if (mTransitionLastPosition == position) { 2058 return; 2059 } 2060 2061 mTemporalInterpolator = true; 2062 mAnimationStartTime = getNanoTime(); 2063 mTransitionDuration = mScene.getDuration() / 1000f; 2064 2065 mTransitionGoalPosition = position; 2066 mInTransition = true; 2067 2068 mStopLogic.springConfig(mTransitionLastPosition, position, currentVelocity, 2069 mScene.getSpringMass(), mScene.getSpringStiffiness(), mScene.getSpringDamping(), 2070 mScene.getSpringStopThreshold(), mScene.getSpringBoundary()); 2071 2072 int currentState = mCurrentState; // TODO: remove setProgress(), this is a temporary fix 2073 mTransitionGoalPosition = position; 2074 mCurrentState = currentState; 2075 mInterpolator = mStopLogic; 2076 2077 2078 mTransitionInstantly = false; 2079 mAnimationStartTime = getNanoTime(); 2080 invalidate(); 2081 } 2082 2083 private static boolean willJump(float velocity, 2084 float position, 2085 float maxAcceleration) { 2086 if (velocity > 0) { 2087 float time = velocity / maxAcceleration; 2088 float pos = velocity * time - (maxAcceleration * time * time) / 2; 2089 return (position + pos > 1); 2090 } else { 2091 float time = -velocity / maxAcceleration; 2092 float pos = velocity * time + (maxAcceleration * time * time) / 2; 2093 return (position + pos < 0); 2094 } 2095 } 2096 2097 /** 2098 * Basic deceleration interpolator 2099 */ 2100 class DecelerateInterpolator extends MotionInterpolator { 2101 float mInitialV = 0; 2102 float mCurrentP = 0; 2103 float mMaxA; 2104 2105 public void config(float velocity, float position, float maxAcceleration) { 2106 mInitialV = velocity; 2107 mCurrentP = position; 2108 mMaxA = maxAcceleration; 2109 } 2110 2111 @Override 2112 public float getInterpolation(float time) { 2113 if (mInitialV > 0) { 2114 if (mInitialV / mMaxA < time) { 2115 time = mInitialV / mMaxA; 2116 } 2117 mLastVelocity = mInitialV - mMaxA * time; 2118 float pos = mInitialV * time - (mMaxA * time * time) / 2; 2119 return pos + mCurrentP; 2120 } else { 2121 2122 if (-mInitialV / mMaxA < time) { 2123 time = -mInitialV / mMaxA; 2124 } 2125 mLastVelocity = mInitialV + mMaxA * time; 2126 float pos = mInitialV * time + (mMaxA * time * time) / 2; 2127 return pos + mCurrentP; 2128 } 2129 } 2130 2131 @Override 2132 public float getVelocity() { 2133 return mLastVelocity; 2134 } 2135 } 2136 2137 /** 2138 * @param position animate to given position 2139 */ 2140 void animateTo(float position) { 2141 if (DEBUG) { 2142 Log.v(TAG, " " + Debug.getLocation() + " ... animateTo(" + position 2143 + ") last:" + mTransitionLastPosition); 2144 } 2145 if (mScene == null) { 2146 return; 2147 } 2148 2149 if (mTransitionLastPosition != mTransitionPosition && mTransitionInstantly) { 2150 // if we had a call from setProgress() but evaluate() didn't run, 2151 // the mTransitionLastPosition might not have been updated 2152 mTransitionLastPosition = mTransitionPosition; 2153 } 2154 2155 if (mTransitionLastPosition == position) { 2156 return; 2157 } 2158 mTemporalInterpolator = false; 2159 float currentPosition = mTransitionLastPosition; 2160 mTransitionGoalPosition = position; 2161 mTransitionDuration = mScene.getDuration() / 1000f; 2162 setProgress(mTransitionGoalPosition); 2163 mInterpolator = null; 2164 mProgressInterpolator = mScene.getInterpolator(); 2165 mTransitionInstantly = false; 2166 mAnimationStartTime = getNanoTime(); 2167 mInTransition = true; 2168 mTransitionPosition = currentPosition; 2169 if (DEBUG) { 2170 Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = " 2171 + mTransitionLastPosition + " currentPosition =" + currentPosition); 2172 } 2173 mTransitionLastPosition = currentPosition; 2174 invalidate(); 2175 } 2176 2177 private void computeCurrentPositions() { 2178 final int n = getChildCount(); 2179 for (int i = 0; i < n; i++) { 2180 View v = getChildAt(i); 2181 MotionController frame = mFrameArrayList.get(v); 2182 if (frame == null) { 2183 continue; 2184 } 2185 frame.setStartCurrentState(v); 2186 } 2187 } 2188 2189 /** 2190 * Animate to the starting position of the current transition. 2191 * This will not work during on create as there is no transition 2192 * Transitions are only set up during onAttach 2193 */ 2194 public void transitionToStart() { 2195 animateTo(0.0f); 2196 } 2197 2198 /** 2199 * Animate to the starting position of the current transition. 2200 * This will not work during on create as there is no transition 2201 * Transitions are only set up during onAttach 2202 * 2203 * @param onComplete callback when task is done 2204 */ 2205 public void transitionToStart(Runnable onComplete) { 2206 animateTo(0.0f); 2207 mOnComplete = onComplete; 2208 } 2209 2210 /** 2211 * Animate to the ending position of the current transition. 2212 * This will not work during on create as there is no transition 2213 * Transitions are only set up during onAttach 2214 */ 2215 public void transitionToEnd() { 2216 animateTo(1.0f); 2217 mOnComplete = null; 2218 } 2219 2220 /** 2221 * Animate to the ending position of the current transition. 2222 * This will not work during on create as there is no transition 2223 * Transitions are only set up during onAttach 2224 * 2225 * @param onComplete callback when task is done 2226 */ 2227 public void transitionToEnd(Runnable onComplete) { 2228 animateTo(1.0f); 2229 mOnComplete = onComplete; 2230 } 2231 2232 /** 2233 * Animate to the state defined by the id. 2234 * The id is the id of the ConstraintSet or the id of the State. 2235 * 2236 * @param id the state to transition to 2237 */ 2238 public void transitionToState(int id) { 2239 if (!isAttachedToWindow()) { 2240 if (mStateCache == null) { 2241 mStateCache = new StateCache(); 2242 } 2243 mStateCache.setEndState(id); 2244 return; 2245 } 2246 transitionToState(id, -1, -1); 2247 } 2248 2249 /** 2250 * Animate to the state defined by the id. 2251 * The id is the id of the ConstraintSet or the id of the State. 2252 * 2253 * @param id the state to transition to 2254 * @param duration time in ms. if 0 set by default or transition -1 by current 2255 */ 2256 2257 public void transitionToState(int id, int duration) { 2258 if (!isAttachedToWindow()) { 2259 if (mStateCache == null) { 2260 mStateCache = new StateCache(); 2261 } 2262 mStateCache.setEndState(id); 2263 return; 2264 } 2265 transitionToState(id, -1, -1, duration); 2266 } 2267 2268 /** 2269 * Animate to the state defined by the id. 2270 * Width and height may be used in the picking of the id using this StateSet. 2271 * 2272 * @param id the state to transition 2273 * @param screenWidth the with of the motionLayout used to select the variant 2274 * @param screenHeight the height of the motionLayout used to select the variant 2275 */ 2276 public void transitionToState(int id, int screenWidth, int screenHeight) { 2277 transitionToState(id, screenWidth, screenHeight, -1); 2278 } 2279 2280 /** 2281 * Rotate the layout based on the angle to a ConstraintSet 2282 * @param id constraintSet 2283 * @param duration time to take to rotate 2284 */ 2285 public void rotateTo(int id, int duration) { 2286 mInRotation = true; 2287 mPreRotateWidth = getWidth(); 2288 mPreRotateHeight = getHeight(); 2289 2290 int currentRotation = getDisplay().getRotation(); 2291 mRotatMode = (((currentRotation + 1) % 4) > ((mPreviouseRotation + 1) % 4)) ? 1 : 2; 2292 2293 mPreviouseRotation = currentRotation; 2294 final int n = getChildCount(); 2295 for (int i = 0; i < n; i++) { 2296 View v = getChildAt(i); 2297 ViewState bounds = mPreRotate.get(v); 2298 if (bounds == null) { 2299 bounds = new ViewState(); 2300 mPreRotate.put(v, bounds); 2301 } 2302 bounds.getState(v); 2303 } 2304 2305 mBeginState = -1; 2306 mEndState = id; 2307 mScene.setTransition(-1, mEndState); 2308 mModel.initFrom(mLayoutWidget, null, mScene.getConstraintSet(mEndState)); 2309 mTransitionPosition = 0; 2310 2311 mTransitionLastPosition = 0; 2312 invalidate(); 2313 transitionToEnd(new Runnable() { 2314 @Override 2315 public void run() { 2316 mInRotation = false; 2317 } 2318 }); 2319 if (duration > 0) { 2320 mTransitionDuration = duration / 1000f; 2321 } 2322 2323 } 2324 2325 public boolean isInRotation() { 2326 return mInRotation; 2327 } 2328 2329 /** 2330 * This jumps to a state 2331 * It will be at that state after one repaint cycle 2332 * If the current transition contains that state. 2333 * It setsProgress 0 or 1 to that state. 2334 * If not in the current transition itsl 2335 * 2336 * @param id state to set 2337 */ 2338 public void jumpToState(int id) { 2339 if (!isAttachedToWindow()) { 2340 mCurrentState = id; 2341 } 2342 if (mBeginState == id) { 2343 setProgress(0); 2344 } else if (mEndState == id) { 2345 setProgress(1); 2346 } else { 2347 setTransition(id, id); 2348 } 2349 } 2350 2351 /** 2352 * Animate to the state defined by the id. 2353 * Width and height may be used in the picking of the id using this StateSet. 2354 * 2355 * @param id the state to transition 2356 * @param screenWidth the with of the motionLayout used to select the variant 2357 * @param screenHeight the height of the motionLayout used to select the variant 2358 * @param duration time in ms. if 0 set by default or transition -1 by current 2359 */ 2360 2361 public void transitionToState(int id, int screenWidth, int screenHeight, int duration) { 2362 // if id is either end or start state, transition using current setup. 2363 // if id is not part of end/start, need to setup 2364 2365 // if id == end state, just animate 2366 // ... but check if currentState is unknown. if unknown, call computeCurrentPosition 2367 // if id != end state 2368 if (DEBUG && mScene.mStateSet == null) { 2369 Log.v(TAG, Debug.getLocation() + " mStateSet = null"); 2370 } 2371 if (mScene != null && mScene.mStateSet != null) { 2372 int tmp_id = mScene.mStateSet.convertToConstraintSet(mCurrentState, 2373 id, screenWidth, screenHeight); 2374 2375 if (tmp_id != -1) { 2376 if (DEBUG) { 2377 Log.v(TAG, " got state " + Debug.getLocation() + " lookup(" 2378 + Debug.getName(getContext(), id) 2379 + screenWidth + " , " + screenHeight + " ) = " 2380 + Debug.getName(getContext(), tmp_id)); 2381 } 2382 id = tmp_id; 2383 } 2384 } 2385 if (mCurrentState == id) { 2386 return; 2387 } 2388 if (mBeginState == id) { 2389 animateTo(0.0f); 2390 if (duration > 0) { 2391 mTransitionDuration = duration / 1000f; 2392 } 2393 return; 2394 } 2395 if (mEndState == id) { 2396 animateTo(1.0f); 2397 if (duration > 0) { 2398 mTransitionDuration = duration / 1000f; 2399 } 2400 return; 2401 } 2402 mEndState = id; 2403 if (mCurrentState != UNSET) { 2404 if (DEBUG) { 2405 Log.v(TAG, " transitionToState " + Debug.getLocation() + " current = " 2406 + Debug.getName(getContext(), mCurrentState) 2407 + " to " + Debug.getName(getContext(), mEndState)); 2408 Debug.logStack(TAG, " transitionToState ", 4); 2409 Log.v(TAG, "-------------------------------------------"); 2410 } 2411 setTransition(mCurrentState, id); 2412 2413 animateTo(1.0f); 2414 2415 mTransitionLastPosition = 0; 2416 transitionToEnd(); 2417 if (duration > 0) { 2418 mTransitionDuration = duration / 1000f; 2419 } 2420 return; 2421 } 2422 if (DEBUG) { 2423 Log.v(TAG, "setTransition unknown -> " 2424 + Debug.getName(getContext(), id)); 2425 } 2426 2427 // TODO correctly use width & height 2428 mTemporalInterpolator = false; 2429 mTransitionGoalPosition = 1; 2430 mTransitionPosition = 0; 2431 mTransitionLastPosition = 0; 2432 mTransitionLastTime = getNanoTime(); 2433 mAnimationStartTime = getNanoTime(); 2434 mTransitionInstantly = false; 2435 mInterpolator = null; 2436 if (duration == -1) { 2437 mTransitionDuration = mScene.getDuration() / 1000f; 2438 } 2439 mBeginState = UNSET; 2440 mScene.setTransition(mBeginState, mEndState); 2441 SparseArray<MotionController> controllers = new SparseArray<>(); 2442 if (duration == 0) { 2443 mTransitionDuration = mScene.getDuration() / 1000f; 2444 } else if (duration > 0) { 2445 mTransitionDuration = duration / 1000f; 2446 } 2447 2448 int n = getChildCount(); 2449 2450 mFrameArrayList.clear(); 2451 for (int i = 0; i < n; i++) { 2452 View v = getChildAt(i); 2453 MotionController f = new MotionController(v); 2454 mFrameArrayList.put(v, f); 2455 controllers.put(v.getId(), mFrameArrayList.get(v)); 2456 } 2457 mInTransition = true; 2458 2459 mModel.initFrom(mLayoutWidget, null, mScene.getConstraintSet(id)); 2460 rebuildScene(); 2461 mModel.build(); 2462 computeCurrentPositions(); 2463 int layoutWidth = getWidth(); 2464 int layoutHeight = getHeight(); 2465 // getMap the KeyFrames for each view 2466 2467 if (mDecoratorsHelpers != null) { 2468 for (int i = 0; i < n; i++) { 2469 MotionController motionController = mFrameArrayList.get(getChildAt(i)); 2470 if (motionController == null) { 2471 continue; 2472 } 2473 mScene.getKeyFrames(motionController); 2474 } 2475 // Allow helpers to access all the motionControllers after 2476 for (MotionHelper mDecoratorsHelper : mDecoratorsHelpers) { 2477 mDecoratorsHelper.onPreSetup(this, mFrameArrayList); 2478 } 2479 for (int i = 0; i < n; i++) { 2480 MotionController motionController = mFrameArrayList.get(getChildAt(i)); 2481 if (motionController == null) { 2482 continue; 2483 } 2484 motionController.setup(layoutWidth, layoutHeight, 2485 mTransitionDuration, getNanoTime()); 2486 } 2487 } else { 2488 for (int i = 0; i < n; i++) { 2489 MotionController motionController = mFrameArrayList.get(getChildAt(i)); 2490 if (motionController == null) { 2491 continue; 2492 } 2493 mScene.getKeyFrames(motionController); 2494 motionController.setup(layoutWidth, layoutHeight, 2495 mTransitionDuration, getNanoTime()); 2496 } 2497 } 2498 2499 float stagger = mScene.getStaggered(); 2500 if (stagger != 0.0f) { 2501 float min = Float.MAX_VALUE, max = -Float.MAX_VALUE; 2502 for (int i = 0; i < n; i++) { 2503 MotionController f = mFrameArrayList.get(getChildAt(i)); 2504 float x = f.getFinalX(); 2505 float y = f.getFinalY(); 2506 min = Math.min(min, y + x); 2507 max = Math.max(max, y + x); 2508 } 2509 2510 for (int i = 0; i < n; i++) { 2511 MotionController f = mFrameArrayList.get(getChildAt(i)); 2512 float x = f.getFinalX(); 2513 float y = f.getFinalY(); 2514 f.mStaggerScale = 1 / (1 - stagger); 2515 f.mStaggerOffset = stagger - stagger * (x + y - min) / (max - min); 2516 } 2517 } 2518 2519 mTransitionPosition = 0; 2520 mTransitionLastPosition = 0; 2521 mInTransition = true; 2522 2523 invalidate(); 2524 } 2525 2526 /** 2527 * Returns the last velocity used in the transition 2528 * 2529 * @return 2530 */ 2531 public float getVelocity() { 2532 return mLastVelocity; 2533 } 2534 2535 /** 2536 * Returns the last layout velocity used in the transition 2537 * 2538 * @param view The view 2539 * @param posOnViewX The x position on the view 2540 * @param posOnViewY The y position on the view 2541 * @param returnVelocity The velocity 2542 * @param type Velocity returned 0 = post layout, 1 = layout, 2 = static postlayout 2543 */ 2544 public void getViewVelocity(View view, 2545 float posOnViewX, 2546 float posOnViewY, 2547 float[] returnVelocity, 2548 int type) { 2549 float v = mLastVelocity; 2550 float position = mTransitionLastPosition; 2551 if (mInterpolator != null) { 2552 float deltaT = EPSILON; 2553 float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition); 2554 float interpolatedPosition = 2555 mInterpolator.getInterpolation(mTransitionLastPosition + deltaT); 2556 position = mInterpolator.getInterpolation(mTransitionLastPosition); 2557 interpolatedPosition -= position; 2558 interpolatedPosition /= deltaT; 2559 v = dir * interpolatedPosition / mTransitionDuration; 2560 } 2561 2562 if (mInterpolator instanceof MotionInterpolator) { 2563 v = ((MotionInterpolator) mInterpolator).getVelocity(); 2564 2565 } 2566 2567 MotionController f = mFrameArrayList.get(view); 2568 if ((type & 1) == 0) { 2569 f.getPostLayoutDvDp(position, 2570 view.getWidth(), view.getHeight(), 2571 posOnViewX, posOnViewY, returnVelocity); 2572 } else { 2573 f.getDpDt(position, posOnViewX, posOnViewY, returnVelocity); 2574 } 2575 if (type < VELOCITY_STATIC_POST_LAYOUT) { 2576 returnVelocity[0] *= v; 2577 returnVelocity[1] *= v; 2578 } 2579 2580 } 2581 2582 //////////////////////////////////////////////////////////////////////////////// 2583 // This contains the logic for interacting with the ConstraintLayout Solver 2584 class Model { 2585 ConstraintWidgetContainer mLayoutStart = new ConstraintWidgetContainer(); 2586 ConstraintWidgetContainer mLayoutEnd = new ConstraintWidgetContainer(); 2587 ConstraintSet mStart = null; 2588 ConstraintSet mEnd = null; 2589 int mStartId; 2590 int mEndId; 2591 2592 void copy(ConstraintWidgetContainer src, ConstraintWidgetContainer dest) { 2593 ArrayList<ConstraintWidget> children = src.getChildren(); 2594 HashMap<ConstraintWidget, ConstraintWidget> map = new HashMap<>(); 2595 map.put(src, dest); 2596 dest.getChildren().clear(); 2597 dest.copy(src, map); 2598 for (ConstraintWidget child_s : children) { 2599 ConstraintWidget child_d; 2600 if (child_s instanceof androidx.constraintlayout.core.widgets.Barrier) { 2601 child_d = new androidx.constraintlayout.core.widgets.Barrier(); 2602 } else if (child_s instanceof androidx.constraintlayout.core.widgets.Guideline) { 2603 child_d = new androidx.constraintlayout.core.widgets.Guideline(); 2604 } else if (child_s instanceof Flow) { 2605 child_d = new Flow(); 2606 } else if (child_s instanceof Placeholder) { 2607 child_d = new Placeholder(); 2608 } else if (child_s instanceof androidx.constraintlayout.core.widgets.Helper) { 2609 child_d = new androidx.constraintlayout.core.widgets.HelperWidget(); 2610 } else { 2611 child_d = new ConstraintWidget(); 2612 } 2613 dest.add(child_d); 2614 map.put(child_s, child_d); 2615 } 2616 for (ConstraintWidget child_s : children) { 2617 map.get(child_s).copy(child_s, map); 2618 } 2619 } 2620 2621 void initFrom(ConstraintWidgetContainer baseLayout, 2622 ConstraintSet start, 2623 ConstraintSet end) { 2624 mStart = start; 2625 mEnd = end; 2626 mLayoutStart = new ConstraintWidgetContainer(); 2627 mLayoutEnd = new ConstraintWidgetContainer(); 2628 mLayoutStart.setMeasurer(mLayoutWidget.getMeasurer()); 2629 mLayoutEnd.setMeasurer(mLayoutWidget.getMeasurer()); 2630 mLayoutStart.removeAllChildren(); 2631 mLayoutEnd.removeAllChildren(); 2632 copy(mLayoutWidget, mLayoutStart); 2633 copy(mLayoutWidget, mLayoutEnd); 2634 if (mTransitionLastPosition > 0.5) { 2635 if (start != null) { 2636 setupConstraintWidget(mLayoutStart, start); 2637 } 2638 setupConstraintWidget(mLayoutEnd, end); 2639 } else { 2640 setupConstraintWidget(mLayoutEnd, end); 2641 if (start != null) { 2642 setupConstraintWidget(mLayoutStart, start); 2643 } 2644 } 2645 // then init the engine... 2646 if (DEBUG) { 2647 Log.v(TAG, "> mLayoutStart.updateHierarchy " + Debug.getLocation()); 2648 } 2649 mLayoutStart.setRtl(isRtl()); 2650 mLayoutStart.updateHierarchy(); 2651 2652 if (DEBUG) { 2653 for (ConstraintWidget child : mLayoutStart.getChildren()) { 2654 View view = (View) child.getCompanionWidget(); 2655 debugWidget(">>>>>>> " + Debug.getName(view), child); 2656 } 2657 Log.v(TAG, "> mLayoutEnd.updateHierarchy " + Debug.getLocation()); 2658 Log.v(TAG, "> mLayoutEnd.updateHierarchy " 2659 + Debug.getLocation() + " == isRtl()=" + isRtl()); 2660 } 2661 mLayoutEnd.setRtl(isRtl()); 2662 mLayoutEnd.updateHierarchy(); 2663 2664 if (DEBUG) { 2665 for (ConstraintWidget child : mLayoutEnd.getChildren()) { 2666 View view = (View) child.getCompanionWidget(); 2667 debugWidget(">>>>>>> " + Debug.getName(view), child); 2668 } 2669 } 2670 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 2671 if (layoutParams != null) { 2672 if (layoutParams.width == WRAP_CONTENT) { 2673 mLayoutStart.setHorizontalDimensionBehaviour( 2674 ConstraintWidget.DimensionBehaviour.WRAP_CONTENT); 2675 mLayoutEnd.setHorizontalDimensionBehaviour( 2676 ConstraintWidget.DimensionBehaviour.WRAP_CONTENT); 2677 } 2678 if (layoutParams.height == WRAP_CONTENT) { 2679 mLayoutStart.setVerticalDimensionBehaviour( 2680 ConstraintWidget.DimensionBehaviour.WRAP_CONTENT); 2681 mLayoutEnd.setVerticalDimensionBehaviour( 2682 ConstraintWidget.DimensionBehaviour.WRAP_CONTENT); 2683 } 2684 } 2685 } 2686 2687 private void setupConstraintWidget(ConstraintWidgetContainer base, ConstraintSet cSet) { 2688 SparseArray<ConstraintWidget> mapIdToWidget = new SparseArray<>(); 2689 Constraints.LayoutParams layoutParams = 2690 new Constraints.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 2691 ViewGroup.LayoutParams.WRAP_CONTENT); 2692 2693 mapIdToWidget.clear(); 2694 mapIdToWidget.put(PARENT_ID, base); 2695 mapIdToWidget.put(getId(), base); 2696 if (cSet != null && cSet.mRotate != 0) { 2697 resolveSystem(mLayoutEnd, getOptimizationLevel(), 2698 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY), 2699 MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY)); 2700 } 2701 // build id widget map 2702 for (ConstraintWidget child : base.getChildren()) { 2703 child.setAnimated(true); 2704 View view = (View) child.getCompanionWidget(); 2705 mapIdToWidget.put(view.getId(), child); 2706 } 2707 2708 for (ConstraintWidget child : base.getChildren()) { 2709 View view = (View) child.getCompanionWidget(); 2710 cSet.applyToLayoutParams(view.getId(), layoutParams); 2711 2712 child.setWidth(cSet.getWidth(view.getId())); 2713 child.setHeight(cSet.getHeight(view.getId())); 2714 if (view instanceof ConstraintHelper) { 2715 cSet.applyToHelper((ConstraintHelper) view, child, layoutParams, mapIdToWidget); 2716 if (view instanceof Barrier) { 2717 ((Barrier) view).validateParams(); 2718 if (DEBUG) { 2719 Log.v(TAG, ">>>>>>>>>> Barrier " + Debug.getName(getContext(), 2720 ((Barrier) view).getReferencedIds())); 2721 } 2722 } 2723 } 2724 if (DEBUG) { 2725 debugLayoutParam(">>>>>>> " + Debug.getName(view), layoutParams); 2726 } 2727 layoutParams.resolveLayoutDirection(getLayoutDirection()); 2728 applyConstraintsFromLayoutParams(false, view, child, layoutParams, mapIdToWidget); 2729 if (cSet.getVisibilityMode(view.getId()) == ConstraintSet.VISIBILITY_MODE_IGNORE) { 2730 child.setVisibility(view.getVisibility()); 2731 } else { 2732 child.setVisibility(cSet.getVisibility(view.getId())); 2733 } 2734 } 2735 for (ConstraintWidget child : base.getChildren()) { 2736 if (child instanceof androidx.constraintlayout.core.widgets.VirtualLayout) { 2737 ConstraintHelper view = (ConstraintHelper) child.getCompanionWidget(); 2738 Helper helper = (Helper) child; 2739 view.updatePreLayout(base, helper, mapIdToWidget); 2740 androidx.constraintlayout.core.widgets.VirtualLayout virtualLayout = 2741 (androidx.constraintlayout.core.widgets.VirtualLayout) helper; 2742 virtualLayout.captureWidgets(); 2743 } 2744 } 2745 } 2746 2747 ConstraintWidget getWidget(ConstraintWidgetContainer container, View view) { 2748 if (container.getCompanionWidget() == view) { 2749 return container; 2750 } 2751 ArrayList<ConstraintWidget> children = container.getChildren(); 2752 final int count = children.size(); 2753 for (int i = 0; i < count; i++) { 2754 ConstraintWidget widget = children.get(i); 2755 if (widget.getCompanionWidget() == view) { 2756 return widget; 2757 } 2758 2759 } 2760 return null; 2761 } 2762 2763 @SuppressLint("LogConditional") 2764 private void debugLayoutParam(String str, LayoutParams params) { 2765 String a = " "; 2766 a += params.startToStart != UNSET ? "SS" : "__"; 2767 a += params.startToEnd != UNSET ? "|SE" : "|__"; 2768 a += params.endToStart != UNSET ? "|ES" : "|__"; 2769 a += params.endToEnd != UNSET ? "|EE" : "|__"; 2770 a += params.leftToLeft != UNSET ? "|LL" : "|__"; 2771 a += params.leftToRight != UNSET ? "|LR" : "|__"; 2772 a += params.rightToLeft != UNSET ? "|RL" : "|__"; 2773 a += params.rightToRight != UNSET ? "|RR" : "|__"; 2774 a += params.topToTop != UNSET ? "|TT" : "|__"; 2775 a += params.topToBottom != UNSET ? "|TB" : "|__"; 2776 a += params.bottomToTop != UNSET ? "|BT" : "|__"; 2777 a += params.bottomToBottom != UNSET ? "|BB" : "|__"; 2778 Log.v(TAG, str + a); 2779 } 2780 2781 @SuppressLint("LogConditional") 2782 private void debugWidget(String str, ConstraintWidget child) { 2783 String a = " "; 2784 a += child.mTop.mTarget != null 2785 ? ("T" + (child.mTop.mTarget.mType == ConstraintAnchor.Type.TOP ? "T" : "B")) 2786 : "__"; 2787 a += child.mBottom.mTarget != null 2788 ? ("B" + (child.mBottom.mTarget.mType == ConstraintAnchor.Type.TOP ? "T" : "B")) 2789 : "__"; 2790 a += child.mLeft.mTarget != null 2791 ? ("L" + (child.mLeft.mTarget.mType == ConstraintAnchor.Type.LEFT ? "L" : "R")) 2792 : "__"; 2793 a += child.mRight.mTarget != null 2794 ? ("R" + (child.mRight.mTarget.mType == ConstraintAnchor.Type.LEFT ? "L" : "R")) 2795 : "__"; 2796 Log.v(TAG, str + a + " --- " + child); 2797 } 2798 2799 @SuppressLint("LogConditional") 2800 private void debugLayout(String title, ConstraintWidgetContainer c) { 2801 View v = (View) c.getCompanionWidget(); 2802 String cName = title + " " + Debug.getName(v); 2803 Log.v(TAG, cName + " ========= " + c); 2804 int count = c.getChildren().size(); 2805 for (int i = 0; i < count; i++) { 2806 String str = cName + "[" + i + "] "; 2807 ConstraintWidget child = c.getChildren().get(i); 2808 String a = ""; 2809 a += child.mTop.mTarget != null ? "T" : "_"; 2810 a += child.mBottom.mTarget != null ? "B" : "_"; 2811 a += child.mLeft.mTarget != null ? "L" : "_"; 2812 a += child.mRight.mTarget != null ? "R" : "_"; 2813 v = (View) child.getCompanionWidget(); 2814 String name = Debug.getName(v); 2815 if (v instanceof TextView) { 2816 name += "(" + ((TextView) v).getText() + ")"; 2817 } 2818 Log.v(TAG, str + " " + name + " " + child + " " + a); 2819 } 2820 Log.v(TAG, cName + " done. "); 2821 } 2822 2823 public void reEvaluateState() { 2824 measure(mLastWidthMeasureSpec, mLastHeightMeasureSpec); 2825 setupMotionViews(); 2826 } 2827 2828 public void measure(int widthMeasureSpec, int heightMeasureSpec) { 2829 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 2830 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 2831 2832 mWidthMeasureMode = widthMode; 2833 mHeightMeasureMode = heightMode; 2834 2835 computeStartEndSize(widthMeasureSpec, heightMeasureSpec); 2836 2837 // This works around the problem that MotionLayout calls its children 2838 // Wrap content children 2839 // with measure(AT_MOST,AT_MOST) then measure(EXACTLY, EXACTLY) 2840 // if a child of MotionLayout is a motionLayout 2841 // it would not know it could resize during animation 2842 // other Layouts may have this behaviour but for now this is the only one we support 2843 2844 boolean recompute_start_end_size = true; 2845 if (getParent() instanceof MotionLayout 2846 && widthMode == MeasureSpec.EXACTLY 2847 && heightMode == MeasureSpec.EXACTLY) { 2848 recompute_start_end_size = false; 2849 } 2850 if (recompute_start_end_size) { 2851 computeStartEndSize(widthMeasureSpec, heightMeasureSpec); 2852 2853 mStartWrapWidth = mLayoutStart.getWidth(); 2854 mStartWrapHeight = mLayoutStart.getHeight(); 2855 mEndWrapWidth = mLayoutEnd.getWidth(); 2856 mEndWrapHeight = mLayoutEnd.getHeight(); 2857 mMeasureDuringTransition = ((mStartWrapWidth != mEndWrapWidth) 2858 || (mStartWrapHeight != mEndWrapHeight)); 2859 } 2860 2861 int width = mStartWrapWidth; 2862 int height = mStartWrapHeight; 2863 if (mWidthMeasureMode == MeasureSpec.AT_MOST 2864 || mWidthMeasureMode == MeasureSpec.UNSPECIFIED) { 2865 width = (int) (mStartWrapWidth + mPostInterpolationPosition 2866 * (mEndWrapWidth - mStartWrapWidth)); 2867 } 2868 if (mHeightMeasureMode == MeasureSpec.AT_MOST 2869 || mHeightMeasureMode == MeasureSpec.UNSPECIFIED) { 2870 height = (int) (mStartWrapHeight + mPostInterpolationPosition 2871 * (mEndWrapHeight - mStartWrapHeight)); 2872 } 2873 2874 boolean isWidthMeasuredTooSmall = mLayoutStart.isWidthMeasuredTooSmall() 2875 || mLayoutEnd.isWidthMeasuredTooSmall(); 2876 boolean isHeightMeasuredTooSmall = mLayoutStart.isHeightMeasuredTooSmall() 2877 || mLayoutEnd.isHeightMeasuredTooSmall(); 2878 resolveMeasuredDimension(widthMeasureSpec, heightMeasureSpec, 2879 width, height, isWidthMeasuredTooSmall, isHeightMeasuredTooSmall); 2880 2881 if (DEBUG) { 2882 Debug.logStack(TAG, ">>>>>>>>", 3); 2883 debugLayout(">>>>>>> measure str ", mLayoutStart); 2884 debugLayout(">>>>>>> measure end ", mLayoutEnd); 2885 } 2886 } 2887 2888 private void computeStartEndSize(int widthMeasureSpec, int heightMeasureSpec) { 2889 int optimisationLevel = getOptimizationLevel(); 2890 2891 if (mCurrentState == getStartState()) { 2892 resolveSystem(mLayoutEnd, optimisationLevel, 2893 (mEnd == null || mEnd.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec, 2894 (mEnd == null || mEnd.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec); 2895 if (mStart != null) { 2896 resolveSystem(mLayoutStart, optimisationLevel, 2897 (mStart.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec, 2898 (mStart.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec); 2899 } 2900 } else { 2901 if (mStart != null) { 2902 resolveSystem(mLayoutStart, optimisationLevel, 2903 (mStart.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec, 2904 (mStart.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec); 2905 } 2906 resolveSystem(mLayoutEnd, optimisationLevel, 2907 (mEnd == null || mEnd.mRotate == 0) ? widthMeasureSpec : heightMeasureSpec, 2908 (mEnd == null || mEnd.mRotate == 0) ? heightMeasureSpec : widthMeasureSpec); 2909 } 2910 } 2911 2912 public void build() { 2913 final int n = getChildCount(); 2914 mFrameArrayList.clear(); 2915 SparseArray<MotionController> controllers = new SparseArray<>(); 2916 int[] ids = new int[n]; 2917 for (int i = 0; i < n; i++) { 2918 View v = getChildAt(i); 2919 MotionController motionController = new MotionController(v); 2920 controllers.put(ids[i] = v.getId(), motionController); 2921 mFrameArrayList.put(v, motionController); 2922 } 2923 for (int i = 0; i < n; i++) { 2924 View v = getChildAt(i); 2925 MotionController motionController = mFrameArrayList.get(v); 2926 if (motionController == null) { 2927 continue; 2928 } 2929 if (mStart != null) { 2930 ConstraintWidget startWidget = getWidget(mLayoutStart, v); 2931 if (startWidget != null) { 2932 motionController.setStartState(toRect(startWidget), mStart, 2933 getWidth(), getHeight()); 2934 } else { 2935 if (mDebugPath != 0) { 2936 Log.e(TAG, Debug.getLocation() + "no widget for " 2937 + Debug.getName(v) + " (" + v.getClass().getName() + ")"); 2938 } 2939 } 2940 } else { 2941 if (mInRotation) { 2942 motionController.setStartState(mPreRotate.get(v), v, mRotatMode, 2943 mPreRotateWidth, mPreRotateHeight); 2944 } 2945 } 2946 if (mEnd != null) { 2947 ConstraintWidget endWidget = getWidget(mLayoutEnd, v); 2948 if (endWidget != null) { 2949 motionController.setEndState(toRect(endWidget), mEnd, 2950 getWidth(), getHeight()); 2951 } else { 2952 if (mDebugPath != 0) { 2953 Log.e(TAG, Debug.getLocation() + "no widget for " 2954 + Debug.getName(v) 2955 + " (" + v.getClass().getName() + ")"); 2956 } 2957 } 2958 } 2959 } 2960 2961 for (int i = 0; i < n; i++) { 2962 MotionController controller = controllers.get(ids[i]); 2963 int relativeToId = controller.getAnimateRelativeTo(); 2964 if (relativeToId != UNSET) { 2965 controller.setupRelative(controllers.get(relativeToId)); 2966 } 2967 } 2968 } 2969 2970 public void setMeasuredId(int startId, int endId) { 2971 mStartId = startId; 2972 mEndId = endId; 2973 } 2974 2975 public boolean isNotConfiguredWith(int startId, int endId) { 2976 return startId != mStartId || endId != mEndId; 2977 } 2978 } 2979 2980 Model mModel = new Model(); 2981 2982 private Rect toRect(ConstraintWidget cw) { 2983 mTempRect.top = cw.getY(); 2984 mTempRect.left = cw.getX(); 2985 mTempRect.right = cw.getWidth() + mTempRect.left; 2986 mTempRect.bottom = cw.getHeight() + mTempRect.top; 2987 return mTempRect; 2988 } 2989 2990 @Override 2991 public void requestLayout() { 2992 if (!mMeasureDuringTransition) { 2993 if (mCurrentState == UNSET && mScene != null 2994 && mScene.mCurrentTransition != null) { 2995 int mode = mScene.mCurrentTransition.getLayoutDuringTransition(); 2996 if (mode == MotionScene.LAYOUT_IGNORE_REQUEST) { 2997 return; 2998 } else if (mode == MotionScene.LAYOUT_CALL_MEASURE) { 2999 final int n = getChildCount(); 3000 for (int i = 0; i < n; i++) { 3001 View v = getChildAt(i); 3002 MotionController motionController = mFrameArrayList.get(v); 3003 motionController.remeasure(); 3004 } 3005 return; 3006 } 3007 } 3008 } 3009 super.requestLayout(); 3010 } 3011 3012 @Override 3013 public String toString() { 3014 Context context = getContext(); 3015 return Debug.getName(context, mBeginState) + "->" 3016 + Debug.getName(context, mEndState) 3017 + " (pos:" + mTransitionLastPosition + " Dpos/Dt:" + mLastVelocity; 3018 } 3019 3020 @Override 3021 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3022 if (DEBUG) { 3023 Log.v(TAG, "onMeasure " + Debug.getLocation()); 3024 } 3025 if (mScene == null) { 3026 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 3027 return; 3028 } 3029 boolean recalc = (mLastWidthMeasureSpec != widthMeasureSpec 3030 || mLastHeightMeasureSpec != heightMeasureSpec); 3031 if (mNeedsFireTransitionCompleted) { 3032 mNeedsFireTransitionCompleted = false; 3033 onNewStateAttachHandlers(); 3034 processTransitionCompleted(); 3035 recalc = true; 3036 } 3037 3038 if (mDirtyHierarchy) { 3039 recalc = true; 3040 } 3041 3042 mLastWidthMeasureSpec = widthMeasureSpec; 3043 mLastHeightMeasureSpec = heightMeasureSpec; 3044 3045 int startId = mScene.getStartId(); 3046 int endId = mScene.getEndId(); 3047 boolean setMeasure = true; 3048 if ((recalc || mModel.isNotConfiguredWith(startId, endId)) && mBeginState != UNSET) { 3049 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 3050 mModel.initFrom(mLayoutWidget, mScene.getConstraintSet(startId), 3051 mScene.getConstraintSet(endId)); 3052 mModel.reEvaluateState(); 3053 mModel.setMeasuredId(startId, endId); 3054 setMeasure = false; 3055 } else if (recalc) { 3056 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 3057 } 3058 3059 if (mMeasureDuringTransition || setMeasure) { 3060 int heightPadding = getPaddingTop() + getPaddingBottom(); 3061 int widthPadding = getPaddingLeft() + getPaddingRight(); 3062 int androidLayoutWidth = mLayoutWidget.getWidth() + widthPadding; 3063 int androidLayoutHeight = mLayoutWidget.getHeight() + heightPadding; 3064 if (mWidthMeasureMode == MeasureSpec.AT_MOST 3065 || mWidthMeasureMode == MeasureSpec.UNSPECIFIED) { 3066 androidLayoutWidth = (int) (mStartWrapWidth + mPostInterpolationPosition 3067 * (mEndWrapWidth - mStartWrapWidth)); 3068 requestLayout(); 3069 } 3070 if (mHeightMeasureMode == MeasureSpec.AT_MOST 3071 || mHeightMeasureMode == MeasureSpec.UNSPECIFIED) { 3072 androidLayoutHeight = (int) (mStartWrapHeight + mPostInterpolationPosition 3073 * (mEndWrapHeight - mStartWrapHeight)); 3074 requestLayout(); 3075 } 3076 setMeasuredDimension(androidLayoutWidth, androidLayoutHeight); 3077 } 3078 evaluateLayout(); 3079 } 3080 3081 @Override 3082 public boolean onStartNestedScroll(@NonNull View child, 3083 @NonNull View target, 3084 int axes, int type) { 3085 if (DEBUG) { 3086 Log.v(TAG, "********** onStartNestedScroll( child:" + Debug.getName(child) 3087 + ", target:" + Debug.getName(target) + ", axis:" + axes + ", type:" + type); 3088 } 3089 if (mScene == null 3090 || mScene.mCurrentTransition == null 3091 || mScene.mCurrentTransition.getTouchResponse() == null 3092 || (mScene.mCurrentTransition.getTouchResponse().getFlags() 3093 & TouchResponse.FLAG_DISABLE_SCROLL) != 0) { 3094 return false; 3095 } 3096 return true; 3097 } 3098 3099 @Override 3100 public void onNestedScrollAccepted(@NonNull View child, 3101 @NonNull View target, 3102 int axes, 3103 int type) { 3104 if (DEBUG) { 3105 Log.v(TAG, "********** onNestedScrollAccepted( child:" + Debug.getName(child) 3106 + ", target:" + Debug.getName(target) + ", axis:" + axes + ", type:" + type); 3107 } 3108 mScrollTargetTime = getNanoTime(); 3109 mScrollTargetDT = 0; 3110 mScrollTargetDX = 0; 3111 mScrollTargetDY = 0; 3112 } 3113 3114 @Override 3115 public void onStopNestedScroll(@NonNull View target, int type) { 3116 if (DEBUG) { 3117 Log.v(TAG, "********** onStopNestedScroll( target:" 3118 + Debug.getName(target) + " , type:" + type + " " 3119 + mScrollTargetDX + ", " + mScrollTargetDY); 3120 Debug.logStack(TAG, "onStopNestedScroll ", 8); 3121 3122 } 3123 if (mScene == null || mScrollTargetDT == 0) { 3124 return; 3125 } 3126 mScene.processScrollUp(mScrollTargetDX / mScrollTargetDT, 3127 mScrollTargetDY / mScrollTargetDT); 3128 } 3129 3130 @Override 3131 public void onNestedScroll(@NonNull View target, 3132 int dxConsumed, 3133 int dyConsumed, 3134 int dxUnconsumed, 3135 int dyUnconsumed, 3136 int type, int[] consumed) { 3137 if (mUndergoingMotion || dxConsumed != 0 || dyConsumed != 0) { 3138 consumed[0] += dxUnconsumed; 3139 consumed[1] += dyUnconsumed; 3140 } 3141 mUndergoingMotion = false; 3142 } 3143 3144 @Override 3145 public void onNestedScroll(@NonNull View target, 3146 int dxConsumed, 3147 int dyConsumed, 3148 int dxUnconsumed, 3149 int dyUnconsumed, 3150 int type) { 3151 if (DEBUG) { 3152 Log.v(TAG, "********** onNestedScroll( target:" + Debug.getName(target) 3153 + ", dxConsumed:" + dxConsumed 3154 + ", dyConsumed:" + dyConsumed 3155 + ", dyConsumed:" + dxUnconsumed 3156 + ", dyConsumed:" + dyUnconsumed + ", type:" + type); 3157 } 3158 } 3159 3160 @Override 3161 public void onNestedPreScroll(@NonNull View target, 3162 int dx, 3163 int dy, 3164 int @NonNull [] consumed, 3165 int type) { 3166 3167 MotionScene scene = mScene; 3168 if (scene == null) { 3169 return; 3170 } 3171 3172 MotionScene.Transition currentTransition = scene.mCurrentTransition; 3173 if (currentTransition == null || !currentTransition.isEnabled()) { 3174 return; 3175 } 3176 3177 if (currentTransition.isEnabled()) { 3178 TouchResponse touchResponse = currentTransition.getTouchResponse(); 3179 if (touchResponse != null) { 3180 int regionId = touchResponse.getTouchRegionId(); 3181 if (regionId != MotionScene.UNSET && target.getId() != regionId) { 3182 return; 3183 } 3184 } 3185 } 3186 3187 if (scene.getMoveWhenScrollAtTop()) { 3188 // This blocks transition during scrolling 3189 TouchResponse touchResponse = currentTransition.getTouchResponse(); 3190 int vert = -1; 3191 if (touchResponse != null) { 3192 if ((touchResponse.getFlags() & TouchResponse.FLAG_SUPPORT_SCROLL_UP) != 0) { 3193 vert = dy; 3194 } 3195 } 3196 if ((mTransitionPosition == 1 || mTransitionPosition == 0) 3197 && target.canScrollVertically(vert)) { 3198 return; 3199 } 3200 } 3201 3202 // This should be disabled in androidx 3203 if (currentTransition.getTouchResponse() != null 3204 && (currentTransition.getTouchResponse().getFlags() 3205 & TouchResponse.FLAG_DISABLE_POST_SCROLL) != 0) { 3206 float dir = scene.getProgressDirection(dx, dy); 3207 if ((mTransitionLastPosition <= 0.0f && (dir < 0)) 3208 || (mTransitionLastPosition >= 1.0f && (dir > 0))) { 3209 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 3210 target.setNestedScrollingEnabled(false); 3211 // TODO find a better hack 3212 target.post(new Runnable() { 3213 @Override 3214 public void run() { 3215 target.setNestedScrollingEnabled(true); 3216 } 3217 }); 3218 } 3219 return; 3220 } 3221 } 3222 3223 if (DEBUG) { 3224 Log.v(TAG, "********** onNestedPreScroll(target:" 3225 + Debug.getName(target) + ", dx:" + dx + ", dy:" + dy + ", type:" + type); 3226 } 3227 float progress = mTransitionPosition; 3228 long time = getNanoTime(); 3229 mScrollTargetDX = dx; 3230 mScrollTargetDY = dy; 3231 mScrollTargetDT = (float) ((time - mScrollTargetTime) * 1E-9); 3232 mScrollTargetTime = time; 3233 if (DEBUG) { 3234 Log.v(TAG, "********** dy = " + dx + " dy = " + dy + " dt = " + mScrollTargetDT); 3235 } 3236 scene.processScrollMove(dx, dy); 3237 if (progress != mTransitionPosition) { 3238 consumed[0] = dx; 3239 consumed[1] = dy; 3240 } 3241 evaluate(false); 3242 if (consumed[0] != 0 || consumed[1] != 0) { 3243 mUndergoingMotion = true; 3244 } 3245 3246 } 3247 3248 @Override 3249 public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { 3250 return false; 3251 } 3252 3253 @Override 3254 public boolean onNestedFling(@NonNull View target, 3255 float velocityX, 3256 float velocityY, 3257 boolean consumed) { 3258 return false; 3259 } 3260 3261 //////////////////////////////////////////////////////////////////////////////////////// 3262 // Used to draw debugging lines 3263 //////////////////////////////////////////////////////////////////////////////////////// 3264 private class DevModeDraw { 3265 private static final int DEBUG_PATH_TICKS_PER_MS = 16; 3266 float[] mPoints; 3267 int[] mPathMode; 3268 float[] mKeyFramePoints; 3269 Path mPath; 3270 Paint mPaint; 3271 Paint mPaintKeyframes; 3272 Paint mPaintGraph; 3273 Paint mTextPaint; 3274 Paint mFillPaint; 3275 private float[] mRectangle; 3276 final int mRedColor = 0xFFFFAA33; 3277 final int mKeyframeColor = 0xffe0759a; 3278 final int mGraphColor = 0xFF33AA00; 3279 final int mShadowColor = 0x77000000; 3280 final int mDiamondSize = 10; 3281 DashPathEffect mDashPathEffect; 3282 int mKeyFrameCount; 3283 Rect mBounds = new Rect(); 3284 boolean mPresentationMode = false; 3285 int mShadowTranslate = 1; 3286 3287 DevModeDraw() { 3288 mPaint = new Paint(); 3289 mPaint.setAntiAlias(true); 3290 mPaint.setColor(mRedColor); 3291 mPaint.setStrokeWidth(2); 3292 mPaint.setStyle(Paint.Style.STROKE); 3293 3294 mPaintKeyframes = new Paint(); 3295 mPaintKeyframes.setAntiAlias(true); 3296 mPaintKeyframes.setColor(mKeyframeColor); 3297 mPaintKeyframes.setStrokeWidth(2); 3298 mPaintKeyframes.setStyle(Paint.Style.STROKE); 3299 3300 mPaintGraph = new Paint(); 3301 mPaintGraph.setAntiAlias(true); 3302 mPaintGraph.setColor(mGraphColor); 3303 mPaintGraph.setStrokeWidth(2); 3304 mPaintGraph.setStyle(Paint.Style.STROKE); 3305 3306 mTextPaint = new Paint(); 3307 mTextPaint.setAntiAlias(true); 3308 mTextPaint.setColor(mGraphColor); 3309 mTextPaint.setTextSize(12 * getContext().getResources().getDisplayMetrics().density); 3310 mRectangle = new float[8]; 3311 mFillPaint = new Paint(); 3312 mFillPaint.setAntiAlias(true); 3313 mDashPathEffect = new DashPathEffect(new float[]{4, 8}, 0); 3314 mPaintGraph.setPathEffect(mDashPathEffect); 3315 mKeyFramePoints = new float[MAX_KEY_FRAMES * 2]; 3316 mPathMode = new int[MAX_KEY_FRAMES]; 3317 3318 if (mPresentationMode) { 3319 mPaint.setStrokeWidth(8); 3320 mFillPaint.setStrokeWidth(8); 3321 mPaintKeyframes.setStrokeWidth(8); 3322 mShadowTranslate = 4; 3323 } 3324 } 3325 3326 public void draw(Canvas canvas, 3327 HashMap<View, MotionController> frameArrayList, 3328 int duration, int debugPath) { 3329 if (frameArrayList == null || frameArrayList.size() == 0) { 3330 return; 3331 } 3332 canvas.save(); 3333 if (!isInEditMode() && (DEBUG_SHOW_PROGRESS & debugPath) == DEBUG_SHOW_PATH) { 3334 String str = getContext().getResources().getResourceName(mEndState) 3335 + ":" + getProgress(); 3336 canvas.drawText(str, 10, getHeight() - 30, mTextPaint); 3337 canvas.drawText(str, 11, getHeight() - 29, mPaint); 3338 } 3339 for (MotionController motionController : frameArrayList.values()) { 3340 int mode = motionController.getDrawPath(); 3341 if (debugPath > 0 && mode == MotionController.DRAW_PATH_NONE) { 3342 mode = MotionController.DRAW_PATH_BASIC; 3343 } 3344 if (mode == MotionController.DRAW_PATH_NONE) { // do not draw path 3345 continue; 3346 } 3347 3348 mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode); 3349 3350 if (mode >= MotionController.DRAW_PATH_BASIC) { 3351 3352 int frames = duration / DEBUG_PATH_TICKS_PER_MS; 3353 if (mPoints == null || mPoints.length != frames * 2) { 3354 mPoints = new float[frames * 2]; 3355 mPath = new Path(); 3356 } 3357 3358 canvas.translate(mShadowTranslate, mShadowTranslate); 3359 3360 mPaint.setColor(mShadowColor); 3361 mFillPaint.setColor(mShadowColor); 3362 mPaintKeyframes.setColor(mShadowColor); 3363 mPaintGraph.setColor(mShadowColor); 3364 motionController.buildPath(mPoints, frames); 3365 drawAll(canvas, mode, mKeyFrameCount, motionController); 3366 mPaint.setColor(mRedColor); 3367 mPaintKeyframes.setColor(mKeyframeColor); 3368 mFillPaint.setColor(mKeyframeColor); 3369 mPaintGraph.setColor(mGraphColor); 3370 3371 canvas.translate(-mShadowTranslate, -mShadowTranslate); 3372 drawAll(canvas, mode, mKeyFrameCount, motionController); 3373 if (mode == MotionController.DRAW_PATH_RECTANGLE) { 3374 drawRectangle(canvas, motionController); 3375 } 3376 } 3377 3378 } 3379 canvas.restore(); 3380 } 3381 3382 public void drawAll(Canvas canvas, 3383 int mode, 3384 int keyFrames, 3385 MotionController motionController) { 3386 if (mode == MotionController.DRAW_PATH_AS_CONFIGURED) { 3387 drawPathAsConfigured(canvas); 3388 } 3389 if (mode == MotionController.DRAW_PATH_RELATIVE) { 3390 drawPathRelative(canvas); 3391 } 3392 if (mode == MotionController.DRAW_PATH_CARTESIAN) { 3393 drawPathCartesian(canvas); 3394 } 3395 drawBasicPath(canvas); 3396 drawTicks(canvas, mode, keyFrames, motionController); 3397 } 3398 3399 private void drawBasicPath(Canvas canvas) { 3400 canvas.drawLines(mPoints, mPaint); 3401 } 3402 3403 private void drawTicks(Canvas canvas, 3404 int mode, 3405 int keyFrames, 3406 MotionController motionController) { 3407 int viewWidth = 0; 3408 int viewHeight = 0; 3409 if (motionController.mView != null) { 3410 viewWidth = motionController.mView.getWidth(); 3411 viewHeight = motionController.mView.getHeight(); 3412 } 3413 for (int i = 1; i < keyFrames - 1; i++) { 3414 if (mode == MotionController.DRAW_PATH_AS_CONFIGURED 3415 && mPathMode[i - 1] == MotionController.DRAW_PATH_NONE) { 3416 continue; 3417 3418 } 3419 float x = mKeyFramePoints[i * 2]; 3420 float y = mKeyFramePoints[i * 2 + 1]; 3421 mPath.reset(); 3422 mPath.moveTo(x, y + mDiamondSize); 3423 mPath.lineTo(x + mDiamondSize, y); 3424 mPath.lineTo(x, y - mDiamondSize); 3425 mPath.lineTo(x - mDiamondSize, y); 3426 mPath.close(); 3427 3428 @SuppressWarnings("unused") 3429 MotionPaths framePoint = motionController.getKeyFrame(i - 1); 3430 float dx = 0; //framePoint.translationX; 3431 float dy = 0; //framePoint.translationY; 3432 if (mode == MotionController.DRAW_PATH_AS_CONFIGURED) { 3433 3434 if (mPathMode[i - 1] == MotionPaths.PERPENDICULAR) { 3435 drawPathRelativeTicks(canvas, x - dx, y - dy); 3436 } else if (mPathMode[i - 1] == MotionPaths.CARTESIAN) { 3437 drawPathCartesianTicks(canvas, x - dx, y - dy); 3438 } else if (mPathMode[i - 1] == MotionPaths.SCREEN) { 3439 drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight); 3440 } 3441 3442 canvas.drawPath(mPath, mFillPaint); 3443 } 3444 if (mode == MotionController.DRAW_PATH_RELATIVE) { 3445 drawPathRelativeTicks(canvas, x - dx, y - dy); 3446 } 3447 if (mode == MotionController.DRAW_PATH_CARTESIAN) { 3448 drawPathCartesianTicks(canvas, x - dx, y - dy); 3449 } 3450 if (mode == MotionController.DRAW_PATH_SCREEN) { 3451 drawPathScreenTicks(canvas, x - dx, y - dy, viewWidth, viewHeight); 3452 } 3453 if (dx != 0 || dy != 0) { 3454 drawTranslation(canvas, x - dx, y - dy, x, y); 3455 } else { 3456 canvas.drawPath(mPath, mFillPaint); 3457 } 3458 } 3459 if (mPoints.length > 1) { 3460 // Draw the starting and ending circle 3461 canvas.drawCircle(mPoints[0], mPoints[1], 8, mPaintKeyframes); 3462 canvas.drawCircle(mPoints[mPoints.length - 2], 3463 mPoints[mPoints.length - 1], 8, mPaintKeyframes); 3464 } 3465 } 3466 3467 private void drawTranslation(Canvas canvas, float x1, float y1, float x2, float y2) { 3468 canvas.drawRect(x1, y1, x2, y2, mPaintGraph); 3469 canvas.drawLine(x1, y1, x2, y2, mPaintGraph); 3470 } 3471 3472 private void drawPathRelative(Canvas canvas) { 3473 canvas.drawLine(mPoints[0], mPoints[1], 3474 mPoints[mPoints.length - 2], mPoints[mPoints.length - 1], mPaintGraph); 3475 } 3476 3477 private void drawPathAsConfigured(Canvas canvas) { 3478 boolean path = false; 3479 boolean cart = false; 3480 for (int i = 0; i < mKeyFrameCount; i++) { 3481 if (mPathMode[i] == MotionPaths.PERPENDICULAR) { 3482 path = true; 3483 } 3484 if (mPathMode[i] == MotionPaths.CARTESIAN) { 3485 cart = true; 3486 } 3487 } 3488 if (path) { 3489 drawPathRelative(canvas); 3490 } 3491 if (cart) { 3492 drawPathCartesian(canvas); 3493 } 3494 } 3495 3496 private void drawPathRelativeTicks(Canvas canvas, float x, float y) { 3497 float x1 = mPoints[0]; 3498 float y1 = mPoints[1]; 3499 float x2 = mPoints[mPoints.length - 2]; 3500 float y2 = mPoints[mPoints.length - 1]; 3501 float dist = (float) Math.hypot(x1 - x2, y1 - y2); 3502 float t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / (dist * dist); 3503 float xp = x1 + t * (x2 - x1); 3504 float yp = y1 + t * (y2 - y1); 3505 3506 Path path = new Path(); 3507 path.moveTo(x, y); 3508 path.lineTo(xp, yp); 3509 float len = (float) Math.hypot(xp - x, yp - y); 3510 String text = "" + ((int) (100 * len / dist)) / 100.0f; 3511 getTextBounds(text, mTextPaint); 3512 float off = len / 2 - mBounds.width() / 2; 3513 canvas.drawTextOnPath(text, path, off, -20, mTextPaint); 3514 canvas.drawLine(x, y, xp, yp, mPaintGraph); 3515 } 3516 3517 void getTextBounds(String text, Paint paint) { 3518 paint.getTextBounds(text, 0, text.length(), mBounds); 3519 } 3520 3521 private void drawPathCartesian(Canvas canvas) { 3522 float x1 = mPoints[0]; 3523 float y1 = mPoints[1]; 3524 float x2 = mPoints[mPoints.length - 2]; 3525 float y2 = mPoints[mPoints.length - 1]; 3526 3527 canvas.drawLine(Math.min(x1, x2), Math.max(y1, y2), 3528 Math.max(x1, x2), Math.max(y1, y2), mPaintGraph); 3529 canvas.drawLine(Math.min(x1, x2), Math.min(y1, y2), 3530 Math.min(x1, x2), Math.max(y1, y2), mPaintGraph); 3531 } 3532 3533 private void drawPathCartesianTicks(Canvas canvas, float x, float y) { 3534 float x1 = mPoints[0]; 3535 float y1 = mPoints[1]; 3536 float x2 = mPoints[mPoints.length - 2]; 3537 float y2 = mPoints[mPoints.length - 1]; 3538 float minx = Math.min(x1, x2); 3539 float maxy = Math.max(y1, y2); 3540 float xgap = x - Math.min(x1, x2); 3541 float ygap = Math.max(y1, y2) - y; 3542 // Horizontal line 3543 String text = "" + ((int) (0.5 + 100 * xgap / Math.abs(x2 - x1))) / 100.0f; 3544 getTextBounds(text, mTextPaint); 3545 float off = xgap / 2 - mBounds.width() / 2; 3546 canvas.drawText(text, off + minx, y - 20, mTextPaint); 3547 canvas.drawLine(x, y, 3548 Math.min(x1, x2), y, mPaintGraph); 3549 3550 // Vertical line 3551 text = "" + ((int) (0.5 + 100 * ygap / Math.abs(y2 - y1))) / 100.0f; 3552 getTextBounds(text, mTextPaint); 3553 off = ygap / 2 - mBounds.height() / 2; 3554 canvas.drawText(text, x + 5, maxy - off, mTextPaint); 3555 canvas.drawLine(x, y, 3556 x, Math.max(y1, y2), mPaintGraph); 3557 } 3558 3559 private void drawPathScreenTicks(Canvas canvas, 3560 float x, 3561 float y, 3562 int viewWidth, 3563 int viewHeight) { 3564 float x1 = 0; 3565 float y1 = 0; 3566 float x2 = 1; 3567 float y2 = 1; 3568 float minx = 0; 3569 float maxy = 0; 3570 float xgap = x; 3571 float ygap = y; 3572 // Horizontal line 3573 String text = "" + ((int) (0.5 + 100 * (xgap - viewWidth / 2) 3574 / (getWidth() - viewWidth))) / 100.0f; 3575 getTextBounds(text, mTextPaint); 3576 float off = xgap / 2 - mBounds.width() / 2; 3577 canvas.drawText(text, off + minx, y - 20, mTextPaint); 3578 canvas.drawLine(x, y, 3579 Math.min(x1, x2), y, mPaintGraph); 3580 3581 // Vertical line 3582 text = "" + ((int) (0.5 + 100 * (ygap - viewHeight / 2) 3583 / (getHeight() - viewHeight))) / 100.0f; 3584 getTextBounds(text, mTextPaint); 3585 off = ygap / 2 - mBounds.height() / 2; 3586 canvas.drawText(text, x + 5, maxy - off, mTextPaint); 3587 canvas.drawLine(x, y, 3588 x, Math.max(y1, y2), mPaintGraph); 3589 } 3590 3591 private void drawRectangle(Canvas canvas, MotionController motionController) { 3592 mPath.reset(); 3593 int rectFrames = 50; 3594 for (int i = 0; i <= rectFrames; i++) { 3595 float p = i / (float) rectFrames; 3596 motionController.buildRect(p, mRectangle, 0); 3597 mPath.moveTo(mRectangle[0], mRectangle[1]); 3598 mPath.lineTo(mRectangle[2], mRectangle[3]); 3599 mPath.lineTo(mRectangle[4], mRectangle[5]); 3600 mPath.lineTo(mRectangle[6], mRectangle[7]); 3601 mPath.close(); 3602 } 3603 mPaint.setColor(0x44000000); 3604 canvas.translate(2, 2); 3605 canvas.drawPath(mPath, mPaint); 3606 3607 canvas.translate(-2, -2); 3608 mPaint.setColor(0xFFFF0000); 3609 canvas.drawPath(mPath, mPaint); 3610 } 3611 3612 } 3613 3614 @SuppressLint("LogConditional") 3615 private void debugPos() { 3616 for (int i = 0; i < getChildCount(); i++) { 3617 final View child = getChildAt(i); 3618 Log.v(TAG, " " + Debug.getLocation() + " " + Debug.getName(this) 3619 + " " + Debug.getName(getContext(), mCurrentState) + " " + Debug.getName(child) 3620 + child.getLeft() + " " 3621 + child.getTop()); 3622 } 3623 } 3624 3625 /** 3626 * Used to draw debugging graphics and to do post layout changes 3627 * 3628 * @param canvas 3629 */ 3630 @Override 3631 protected void dispatchDraw(Canvas canvas) { 3632 if (DEBUG) { 3633 Log.v(TAG, " dispatchDraw " + getProgress() + Debug.getLocation()); 3634 } 3635 if (mDecoratorsHelpers != null) { 3636 for (MotionHelper decor : mDecoratorsHelpers) { 3637 decor.onPreDraw(canvas); 3638 } 3639 } 3640 evaluate(false); 3641 if (mScene != null && mScene.mViewTransitionController != null) { 3642 mScene.mViewTransitionController.animate(); 3643 } 3644 if (DEBUG) { 3645 Log.v(TAG, " dispatchDraw" + Debug.getLocation() + " " + Debug.getName(this) 3646 + " " + Debug.getName(getContext(), mCurrentState)); 3647 debugPos(); 3648 } 3649 super.dispatchDraw(canvas); 3650 if (mScene == null) { 3651 return; 3652 } 3653 if (DEBUG) { 3654 mDebugPath = 0xFF; 3655 } 3656 if ((mDebugPath & 1) == 1) { 3657 if (!isInEditMode()) { 3658 mFrames++; 3659 long currentDrawTime = getNanoTime(); 3660 if (mLastDrawTime != -1) { 3661 long delay = currentDrawTime - mLastDrawTime; 3662 if (delay > 200000000) { 3663 float fps = mFrames / (delay * 1E-9f); 3664 mLastFps = ((int) (fps * 100)) / 100.0f; 3665 mFrames = 0; 3666 mLastDrawTime = currentDrawTime; 3667 } 3668 } else { 3669 mLastDrawTime = currentDrawTime; 3670 } 3671 Paint paint = new Paint(); 3672 paint.setTextSize(42); 3673 float p = ((int) (getProgress() * 1000)) / 10f; 3674 String str = mLastFps + " fps " + Debug.getState(this, mBeginState) + " -> "; 3675 str += Debug.getState(this, mEndState) + " (progress: " + p + " ) state=" 3676 + ((mCurrentState == UNSET) ? "undefined" 3677 : Debug.getState(this, mCurrentState)); 3678 paint.setColor(0xFF000000); 3679 canvas.drawText(str, 11, getHeight() - 29, paint); 3680 paint.setColor(0xFF880088); 3681 canvas.drawText(str, 10, getHeight() - 30, paint); 3682 3683 } 3684 } 3685 if (mDebugPath > 1) { 3686 if (mDevModeDraw == null) { 3687 mDevModeDraw = new DevModeDraw(); 3688 } 3689 mDevModeDraw.draw(canvas, mFrameArrayList, mScene.getDuration(), mDebugPath); 3690 } 3691 if (mDecoratorsHelpers != null) { 3692 for (MotionHelper decor : mDecoratorsHelpers) { 3693 decor.onPostDraw(canvas); 3694 } 3695 } 3696 } 3697 3698 /** 3699 * Direct layout evaluation 3700 */ 3701 private void evaluateLayout() { 3702 float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition); 3703 long currentTime = getNanoTime(); 3704 3705 float deltaPos = 0f; 3706 if (!(mInterpolator instanceof StopLogic)) { // if we are not in a drag 3707 deltaPos = dir * (currentTime - mTransitionLastTime) * 1E-9f / mTransitionDuration; 3708 } 3709 float position = mTransitionLastPosition + deltaPos; 3710 3711 boolean done = false; 3712 if (mTransitionInstantly) { 3713 position = mTransitionGoalPosition; 3714 } 3715 3716 if ((dir > 0 && position >= mTransitionGoalPosition) 3717 || (dir <= 0 && position <= mTransitionGoalPosition)) { 3718 position = mTransitionGoalPosition; 3719 done = true; 3720 } 3721 if (mInterpolator != null && !done) { 3722 if (mTemporalInterpolator) { 3723 float time = (currentTime - mAnimationStartTime) * 1E-9f; 3724 position = mInterpolator.getInterpolation(time); 3725 } else { 3726 position = mInterpolator.getInterpolation(position); 3727 } 3728 } 3729 if ((dir > 0 && position >= mTransitionGoalPosition) 3730 || (dir <= 0 && position <= mTransitionGoalPosition)) { 3731 position = mTransitionGoalPosition; 3732 } 3733 mPostInterpolationPosition = position; 3734 int n = getChildCount(); 3735 long time = getNanoTime(); 3736 float interPos = mProgressInterpolator == null ? position 3737 : mProgressInterpolator.getInterpolation(position); 3738 for (int i = 0; i < n; i++) { 3739 final View child = getChildAt(i); 3740 final MotionController frame = mFrameArrayList.get(child); 3741 if (frame != null) { 3742 frame.interpolate(child, interPos, time, mKeyCache); 3743 } 3744 } 3745 if (mMeasureDuringTransition) { 3746 requestLayout(); 3747 } 3748 } 3749 3750 void endTrigger(boolean start) { 3751 int n = getChildCount(); 3752 for (int i = 0; i < n; i++) { 3753 final View child = getChildAt(i); 3754 final MotionController frame = mFrameArrayList.get(child); 3755 if (frame != null) { 3756 frame.endTrigger(start); 3757 } 3758 } 3759 } 3760 3761 void evaluate(boolean force) { 3762 3763 if (mTransitionLastTime == -1) { 3764 mTransitionLastTime = getNanoTime(); 3765 } 3766 if (mTransitionLastPosition > 0.0f && mTransitionLastPosition < 1.0f) { 3767 mCurrentState = UNSET; 3768 } 3769 3770 boolean newState = false; 3771 if (mKeepAnimating || (mInTransition 3772 && (force || mTransitionGoalPosition != mTransitionLastPosition))) { 3773 float dir = Math.signum(mTransitionGoalPosition - mTransitionLastPosition); 3774 long currentTime = getNanoTime(); 3775 3776 float deltaPos = 0f; 3777 if (!(mInterpolator instanceof MotionInterpolator)) { // if we are not in a drag 3778 deltaPos = dir * (currentTime - mTransitionLastTime) * 1E-9f / mTransitionDuration; 3779 } 3780 float position = mTransitionLastPosition + deltaPos; 3781 3782 boolean done = false; 3783 if (mTransitionInstantly) { 3784 position = mTransitionGoalPosition; 3785 } 3786 3787 if ((dir > 0 && position >= mTransitionGoalPosition) 3788 || (dir <= 0 && position <= mTransitionGoalPosition)) { 3789 position = mTransitionGoalPosition; 3790 mInTransition = false; 3791 done = true; 3792 } 3793 if (DEBUG) { 3794 Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = " 3795 + mTransitionLastPosition + " position = " + position); 3796 } 3797 mTransitionLastPosition = position; 3798 mTransitionPosition = position; 3799 mTransitionLastTime = currentTime; 3800 int notStopLogic = 0; 3801 int stopLogicContinue = 1; 3802 int stopLogicStop = 2; 3803 int stopLogicDone = notStopLogic; 3804 if (mInterpolator != null && !done) { 3805 if (mTemporalInterpolator) { 3806 float time = (currentTime - mAnimationStartTime) * 1E-9f; 3807 position = mInterpolator.getInterpolation(time); 3808 if (mInterpolator == mStopLogic) { 3809 boolean dp = mStopLogic.isStopped(); 3810 stopLogicDone = dp ? stopLogicStop : stopLogicContinue; 3811 } 3812 3813 if (DEBUG) { 3814 Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = " 3815 + mTransitionLastPosition + " position = " + position); 3816 } 3817 mTransitionLastPosition = position; 3818 3819 mTransitionLastTime = currentTime; 3820 if (mInterpolator instanceof MotionInterpolator) { 3821 float lastVelocity = ((MotionInterpolator) mInterpolator).getVelocity(); 3822 mLastVelocity = lastVelocity; 3823 if (Math.abs(lastVelocity) * mTransitionDuration <= EPSILON 3824 && stopLogicDone == stopLogicStop) { 3825 mInTransition = false; 3826 } 3827 if (lastVelocity > 0 && position >= 1.0f) { 3828 mTransitionLastPosition = position = 1.0f; 3829 mInTransition = false; 3830 } 3831 if (lastVelocity < 0 && position <= 0) { 3832 mTransitionLastPosition = position = 0.0f; 3833 mInTransition = false; 3834 } 3835 } 3836 3837 } else { 3838 3839 float p2 = position; 3840 position = mInterpolator.getInterpolation(position); 3841 if (mInterpolator instanceof MotionInterpolator) { 3842 mLastVelocity = ((MotionInterpolator) mInterpolator).getVelocity(); 3843 } else { 3844 p2 = mInterpolator.getInterpolation(p2 + deltaPos); 3845 mLastVelocity = dir * (p2 - position) / deltaPos; 3846 } 3847 3848 } 3849 } else { 3850 mLastVelocity = deltaPos; 3851 } 3852 if (Math.abs(mLastVelocity) > EPSILON) { 3853 setState(TransitionState.MOVING); 3854 } 3855 3856 if (stopLogicDone != stopLogicContinue) { 3857 if ((dir > 0 && position >= mTransitionGoalPosition) 3858 || (dir <= 0 && position <= mTransitionGoalPosition)) { 3859 position = mTransitionGoalPosition; 3860 mInTransition = false; 3861 } 3862 3863 if (position >= 1.0f || position <= 0.0f) { 3864 mInTransition = false; 3865 setState(TransitionState.FINISHED); 3866 } 3867 } 3868 3869 int n = getChildCount(); 3870 mKeepAnimating = false; 3871 long time = getNanoTime(); 3872 if (DEBUG) { 3873 Log.v(TAG, "LAYOUT frame.interpolate at " + position); 3874 } 3875 mPostInterpolationPosition = position; 3876 float interPos = mProgressInterpolator == null ? position 3877 : mProgressInterpolator.getInterpolation(position); 3878 if (mProgressInterpolator != null) { 3879 mLastVelocity = 3880 mProgressInterpolator 3881 .getInterpolation(position + dir / mTransitionDuration); 3882 mLastVelocity -= mProgressInterpolator.getInterpolation(position); 3883 } 3884 for (int i = 0; i < n; i++) { 3885 final View child = getChildAt(i); 3886 final MotionController frame = mFrameArrayList.get(child); 3887 if (frame != null) { 3888 mKeepAnimating |= frame.interpolate(child, interPos, time, mKeyCache); 3889 } 3890 } 3891 if (DEBUG) { 3892 Log.v(TAG, " interpolate " + Debug.getLocation() + " " + Debug.getName(this) 3893 + " " + Debug.getName(getContext(), mBeginState) + " " + position); 3894 } 3895 3896 boolean end = ((dir > 0 && position >= mTransitionGoalPosition) 3897 || (dir <= 0 && position <= mTransitionGoalPosition)); 3898 if (!mKeepAnimating && !mInTransition && end) { 3899 setState(TransitionState.FINISHED); 3900 } 3901 if (mMeasureDuringTransition) { 3902 requestLayout(); 3903 } 3904 3905 mKeepAnimating |= !end; 3906 3907 // If we have hit the begin state begin state could be unset 3908 if (position <= 0 && mBeginState != UNSET) { 3909 if (mCurrentState != mBeginState) { 3910 newState = true; 3911 mCurrentState = mBeginState; 3912 ConstraintSet set = mScene.getConstraintSet(mBeginState); 3913 set.applyCustomAttributes(this); 3914 setState(TransitionState.FINISHED); 3915 } 3916 } 3917 3918 if (position >= 1.0) { 3919 if (DEBUG) { 3920 Log.v(TAG, Debug.getLoc() + " ============= setting to end " 3921 + Debug.getName(getContext(), mEndState) + " " + position); 3922 } 3923 if (mCurrentState != mEndState) { 3924 newState = true; 3925 mCurrentState = mEndState; 3926 ConstraintSet set = mScene.getConstraintSet(mEndState); 3927 set.applyCustomAttributes(this); 3928 setState(TransitionState.FINISHED); 3929 } 3930 } 3931 3932 if (mKeepAnimating || mInTransition) { 3933 invalidate(); 3934 } else { 3935 if ((dir > 0 && position == 1) || (dir < 0 && position == 0)) { 3936 setState(TransitionState.FINISHED); 3937 } 3938 } 3939 if (!mKeepAnimating && !mInTransition && ((dir > 0 && position == 1) 3940 || (dir < 0 && position == 0))) { 3941 onNewStateAttachHandlers(); 3942 } 3943 } 3944 if (mTransitionLastPosition >= 1.0f) { 3945 if (mCurrentState != mEndState) { 3946 newState = true; 3947 } 3948 mCurrentState = mEndState; 3949 } else if (mTransitionLastPosition <= 0.0f) { 3950 if (mCurrentState != mBeginState) { 3951 newState = true; 3952 } 3953 mCurrentState = mBeginState; 3954 } 3955 3956 mNeedsFireTransitionCompleted |= newState; 3957 3958 if (newState && !mInLayout) { 3959 requestLayout(); 3960 } 3961 3962 mTransitionPosition = mTransitionLastPosition; 3963 } 3964 3965 private boolean mNeedsFireTransitionCompleted = false; 3966 3967 @Override 3968 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 3969 mInLayout = true; 3970 try { 3971 if (DEBUG) { 3972 Log.v(TAG, " onLayout " + getProgress() + " " + Debug.getLocation()); 3973 } 3974 if (mScene == null) { 3975 super.onLayout(changed, left, top, right, bottom); 3976 return; 3977 } 3978 int w = right - left; 3979 int h = bottom - top; 3980 if (mLastLayoutWidth != w || mLastLayoutHeight != h) { 3981 rebuildScene(); 3982 evaluate(true); 3983 if (DEBUG) { 3984 Log.v(TAG, " onLayout rebuildScene " + Debug.getLocation()); 3985 } 3986 } 3987 3988 mLastLayoutWidth = w; 3989 mLastLayoutHeight = h; 3990 mOldWidth = w; 3991 mOldHeight = h; 3992 } finally { 3993 mInLayout = false; 3994 } 3995 } 3996 3997 /** 3998 * block ConstraintLayout from handling layout description 3999 * 4000 * @param id 4001 */ 4002 @Override 4003 protected void parseLayoutDescription(int id) { 4004 mConstraintLayoutSpec = null; 4005 } 4006 4007 private void init(AttributeSet attrs) { 4008 IS_IN_EDIT_MODE = isInEditMode(); 4009 if (attrs != null) { 4010 TypedArray a = getContext() 4011 .obtainStyledAttributes(attrs, R.styleable.MotionLayout); 4012 final int count = a.getIndexCount(); 4013 4014 boolean apply = true; 4015 for (int i = 0; i < count; i++) { 4016 int attr = a.getIndex(i); 4017 if (attr == R.styleable.MotionLayout_layoutDescription) { 4018 int n = a.getResourceId(attr, UNSET); 4019 mScene = new MotionScene(getContext(), this, n); 4020 } else if (attr == R.styleable.MotionLayout_currentState) { 4021 mCurrentState = a.getResourceId(attr, UNSET); 4022 } else if (attr == R.styleable.MotionLayout_motionProgress) { 4023 mTransitionGoalPosition = a.getFloat(attr, 0.0f); 4024 mInTransition = true; 4025 } else if (attr == R.styleable.MotionLayout_applyMotionScene) { 4026 apply = a.getBoolean(attr, apply); 4027 } else if (attr == R.styleable.MotionLayout_showPaths) { 4028 if (mDebugPath == 0) { // favor motionDebug 4029 mDebugPath = a.getBoolean(attr, false) ? DEBUG_SHOW_PATH : 0; 4030 } 4031 } else if (attr == R.styleable.MotionLayout_motionDebug) { 4032 mDebugPath = a.getInt(attr, 0); 4033 } 4034 } 4035 a.recycle(); 4036 if (mScene == null) { 4037 Log.e(TAG, "WARNING NO app:layoutDescription tag"); 4038 } 4039 if (!apply) { 4040 mScene = null; 4041 } 4042 } 4043 if (mDebugPath != 0) { 4044 checkStructure(); 4045 } 4046 if (mCurrentState == UNSET && mScene != null) { 4047 4048 mCurrentState = mScene.getStartId(); 4049 mBeginState = mScene.getStartId(); 4050 if (DEBUG) { 4051 Log.v(TAG, " ============= init end is " 4052 + Debug.getName(getContext(), mEndState)); 4053 } 4054 mEndState = mScene.getEndId(); 4055 if (DEBUG) { 4056 Log.v(TAG, " ============= init setting end to " 4057 + Debug.getName(getContext(), mEndState)); 4058 } 4059 } 4060 } 4061 4062 /** 4063 * Sets a motion scene to the layout. Subsequent calls to it will override the previous scene. 4064 */ 4065 public void setScene(MotionScene scene) { 4066 mScene = scene; 4067 mScene.setRtl(isRtl()); 4068 rebuildScene(); 4069 } 4070 4071 /** 4072 * Get the motion scene of the layout. 4073 * Warning! This gives you direct access to the internal 4074 * state of the MotionLayout making it easy 4075 * corrupt the state. 4076 * @return the motion scene 4077 */ 4078 public MotionScene getScene() { 4079 return mScene; 4080 } 4081 4082 private void checkStructure() { 4083 if (mScene == null) { 4084 Log.e(TAG, "CHECK: motion scene not set! set \"app:layoutDescription=\"@xml/file\""); 4085 return; 4086 } 4087 4088 checkStructure(mScene.getStartId(), mScene.getConstraintSet(mScene.getStartId())); 4089 SparseIntArray startToEnd = new SparseIntArray(); 4090 SparseIntArray endToStart = new SparseIntArray(); 4091 for (MotionScene.Transition definedTransition : mScene.getDefinedTransitions()) { 4092 if (definedTransition == mScene.mCurrentTransition) { 4093 Log.v(TAG, "CHECK: CURRENT"); 4094 } 4095 checkStructure(definedTransition); 4096 int startId = definedTransition.getStartConstraintSetId(); 4097 int endId = definedTransition.getEndConstraintSetId(); 4098 String startString = Debug.getName(getContext(), startId); 4099 String endString = Debug.getName(getContext(), endId); 4100 if (startToEnd.get(startId) == endId) { 4101 4102 Log.e(TAG, "CHECK: two transitions with the same start and end " 4103 + startString + "->" + endString); 4104 } 4105 if (endToStart.get(endId) == startId) { 4106 4107 Log.e(TAG, "CHECK: you can't have reverse transitions" 4108 + startString + "->" + endString); 4109 } 4110 startToEnd.put(startId, endId); 4111 endToStart.put(endId, startId); 4112 if (mScene.getConstraintSet(startId) == null) { 4113 Log.e(TAG, " no such constraintSetStart " + startString); 4114 } 4115 4116 if (mScene.getConstraintSet(endId) == null) { 4117 Log.e(TAG, " no such constraintSetEnd " + startString); 4118 } 4119 } 4120 } 4121 4122 private void checkStructure(int csetId, ConstraintSet set) { 4123 String setName = Debug.getName(getContext(), csetId); 4124 int size = getChildCount(); 4125 for (int i = 0; i < size; i++) { 4126 View v = getChildAt(i); 4127 int id = v.getId(); 4128 if (id == -1) { 4129 Log.w(TAG, "CHECK: " + setName + " ALL VIEWS SHOULD HAVE ID's " 4130 + v.getClass().getName() + " does not!"); 4131 } 4132 ConstraintSet.Constraint c = set.getConstraint(id); 4133 if (c == null) { 4134 Log.w(TAG, "CHECK: " + setName + " NO CONSTRAINTS for " + Debug.getName(v)); 4135 } 4136 } 4137 int[] ids = set.getKnownIds(); 4138 for (int i = 0; i < ids.length; i++) { 4139 int id = ids[i]; 4140 String idString = Debug.getName(getContext(), id); 4141 if (null == findViewById(ids[i])) { 4142 Log.w(TAG, "CHECK: " + setName + " NO View matches id " + idString); 4143 } 4144 if (set.getHeight(id) == UNSET) { 4145 Log.w(TAG, "CHECK: " + setName + "(" + idString + ") no LAYOUT_HEIGHT"); 4146 } 4147 if (set.getWidth(id) == UNSET) { 4148 Log.w(TAG, "CHECK: " + setName + "(" + idString + ") no LAYOUT_HEIGHT"); 4149 } 4150 } 4151 } 4152 4153 private void checkStructure(MotionScene.Transition transition) { 4154 if (DEBUG) { 4155 Log.v(TAG, "CHECK: transition = " + transition.debugString(getContext())); 4156 Log.v(TAG, "CHECK: transition.setDuration = " + transition.getDuration()); 4157 } 4158 if (transition.getStartConstraintSetId() == transition.getEndConstraintSetId()) { 4159 Log.e(TAG, "CHECK: start and end constraint set should not be the same!"); 4160 } 4161 } 4162 4163 /** 4164 * Display the debugging information such as paths information 4165 * 4166 * @param debugMode integer representing various debug modes 4167 * 4168 */ 4169 public void setDebugMode(int debugMode) { 4170 mDebugPath = debugMode; 4171 invalidate(); 4172 } 4173 4174 private RectF mBoundsCheck = new RectF(); 4175 private View mRegionView = null; 4176 private Matrix mInverseMatrix = null; 4177 4178 private boolean callTransformedTouchEvent(View view, 4179 MotionEvent event, 4180 float offsetX, 4181 float offsetY) { 4182 Matrix viewMatrix = view.getMatrix(); 4183 4184 if (viewMatrix.isIdentity()) { 4185 event.offsetLocation(offsetX, offsetY); 4186 boolean handled = view.onTouchEvent(event); 4187 event.offsetLocation(-offsetX, -offsetY); 4188 4189 return handled; 4190 } 4191 4192 MotionEvent transformedEvent = MotionEvent.obtain(event); 4193 4194 transformedEvent.offsetLocation(offsetX, offsetY); 4195 4196 if (mInverseMatrix == null) { 4197 mInverseMatrix = new Matrix(); 4198 } 4199 4200 viewMatrix.invert(mInverseMatrix); 4201 transformedEvent.transform(mInverseMatrix); 4202 4203 boolean handled = view.onTouchEvent(transformedEvent); 4204 4205 transformedEvent.recycle(); 4206 4207 return handled; 4208 } 4209 4210 /** 4211 * Walk the view tree to see if a child view handles a touch event. 4212 * 4213 * @param x 4214 * @param y 4215 * @param view 4216 * @param event 4217 * @return 4218 */ 4219 private boolean handlesTouchEvent(float x, float y, View view, MotionEvent event) { 4220 boolean handled = false; 4221 if (view instanceof ViewGroup) { 4222 ViewGroup group = (ViewGroup) view; 4223 final int childCount = group.getChildCount(); 4224 for (int i = childCount - 1; i >= 0; i--) { 4225 View child = group.getChildAt(i); 4226 if (handlesTouchEvent(x + child.getLeft() - view.getScrollX(), 4227 y + child.getTop() - view.getScrollY(), 4228 child, event)) { 4229 handled = true; 4230 break; 4231 } 4232 } 4233 } 4234 4235 if (!handled) { 4236 mBoundsCheck.set(x, y, 4237 x + view.getRight() - view.getLeft(), 4238 y + view.getBottom() - view.getTop()); 4239 4240 if (event.getAction() != MotionEvent.ACTION_DOWN 4241 || mBoundsCheck.contains(event.getX(), event.getY())) { 4242 if (callTransformedTouchEvent(view, event, -x, -y)) { 4243 handled = true; 4244 } 4245 } 4246 } 4247 4248 return handled; 4249 } 4250 4251 /** 4252 * Intercepts the touch event to correctly handle touch region id handover 4253 * 4254 * @param event 4255 * @return 4256 */ 4257 @Override 4258 public boolean onInterceptTouchEvent(MotionEvent event) { 4259 if (mScene == null || !mInteractionEnabled) { 4260 return false; 4261 } 4262 4263 if (mScene.mViewTransitionController != null) { 4264 mScene.mViewTransitionController.touchEvent(event); 4265 } 4266 MotionScene.Transition currentTransition = mScene.mCurrentTransition; 4267 if (currentTransition != null && currentTransition.isEnabled()) { 4268 TouchResponse touchResponse = currentTransition.getTouchResponse(); 4269 if (touchResponse != null) { 4270 if (event.getAction() == MotionEvent.ACTION_DOWN) { 4271 RectF region = touchResponse.getTouchRegion(this, new RectF()); 4272 if (region != null 4273 && !region.contains(event.getX(), event.getY())) { 4274 return false; 4275 } 4276 } 4277 int regionId = touchResponse.getTouchRegionId(); 4278 if (regionId != MotionScene.UNSET) { 4279 if (mRegionView == null || mRegionView.getId() != regionId) { 4280 mRegionView = findViewById(regionId); 4281 } 4282 if (mRegionView != null) { 4283 mBoundsCheck.set(mRegionView.getLeft(), 4284 mRegionView.getTop(), 4285 mRegionView.getRight(), 4286 mRegionView.getBottom()); 4287 if (mBoundsCheck.contains(event.getX(), event.getY())) { 4288 // In case of region id, if the view or a child of the view 4289 // handles an event we don't need to do anything; 4290 if (!handlesTouchEvent(mRegionView.getLeft(), mRegionView.getTop(), 4291 mRegionView, event)) { 4292 // but if not, then *we* need to handle the event. 4293 return onTouchEvent(event); 4294 } 4295 } 4296 } 4297 } 4298 } 4299 } 4300 return false; 4301 } 4302 4303 @Override 4304 public boolean onTouchEvent(MotionEvent event) { 4305 if (DEBUG) { 4306 Log.v(TAG, Debug.getLocation() + " onTouchEvent = " + mTransitionLastPosition); 4307 } 4308 if (mScene != null && mInteractionEnabled && mScene.supportTouch()) { 4309 MotionScene.Transition currentTransition = mScene.mCurrentTransition; 4310 if (currentTransition != null && !currentTransition.isEnabled()) { 4311 return super.onTouchEvent(event); 4312 } 4313 mScene.processTouchEvent(event, getCurrentState(), this); 4314 if (mScene.mCurrentTransition.isTransitionFlag(TRANSITION_FLAG_INTERCEPT_TOUCH)) { 4315 return mScene.mCurrentTransition.getTouchResponse().isDragStarted(); 4316 } 4317 return true; 4318 } 4319 if (DEBUG) { 4320 Log.v(TAG, Debug.getLocation() + " mTransitionLastPosition = " 4321 + mTransitionLastPosition); 4322 } 4323 return super.onTouchEvent(event); 4324 } 4325 4326 @Override 4327 protected void onAttachedToWindow() { 4328 super.onAttachedToWindow(); 4329 Display display = getDisplay(); 4330 if (display != null) { 4331 mPreviouseRotation = display.getRotation(); 4332 } 4333 if (mScene != null && mCurrentState != UNSET) { 4334 ConstraintSet cSet = mScene.getConstraintSet(mCurrentState); 4335 mScene.readFallback(this); 4336 if (mDecoratorsHelpers != null) { 4337 for (MotionHelper mh : mDecoratorsHelpers) { 4338 mh.onFinishedMotionScene(this); 4339 } 4340 } 4341 if (cSet != null) { 4342 cSet.applyTo(this); 4343 } 4344 mBeginState = mCurrentState; 4345 } 4346 onNewStateAttachHandlers(); 4347 if (mStateCache != null) { 4348 if (mDelayedApply) { 4349 post(new Runnable() { 4350 @Override 4351 public void run() { 4352 mStateCache.apply(); 4353 } 4354 }); 4355 } else { 4356 mStateCache.apply(); 4357 } 4358 } else { 4359 if (mScene != null && mScene.mCurrentTransition != null) { 4360 if (mScene.mCurrentTransition.getAutoTransition() 4361 == MotionScene.Transition.AUTO_ANIMATE_TO_END) { 4362 transitionToEnd(); 4363 setState(TransitionState.SETUP); 4364 setState(TransitionState.MOVING); 4365 } 4366 } 4367 } 4368 } 4369 4370 @Override 4371 public void onRtlPropertiesChanged(int layoutDirection) { 4372 if (mScene != null) { 4373 mScene.setRtl(isRtl()); 4374 } 4375 } 4376 4377 /** 4378 * This function will set up various handlers (swipe, click...) whenever 4379 * a new state is reached. 4380 */ 4381 void onNewStateAttachHandlers() { 4382 if (mScene == null) { 4383 return; 4384 } 4385 if (mScene.autoTransition(this, mCurrentState)) { 4386 requestLayout(); 4387 return; 4388 } 4389 if (mCurrentState != UNSET) { 4390 mScene.addOnClickListeners(this, mCurrentState); 4391 } 4392 if (mScene.supportTouch()) { 4393 mScene.setupTouch(); 4394 } 4395 } 4396 4397 /** 4398 * Return the current state id 4399 * 4400 * @return current state id 4401 */ 4402 public int getCurrentState() { 4403 return mCurrentState; 4404 } 4405 4406 /** 4407 * Get current position during an animation. 4408 * 4409 * @return current position from 0.0 to 1.0 inclusive 4410 */ 4411 public float getProgress() { 4412 return mTransitionLastPosition; 4413 } 4414 4415 /** 4416 * Provide an estimate of the motion with respect to change in transitionPosition 4417 * (assume you are currently in a transition) 4418 * 4419 * @param mTouchAnchorId id of the anchor view that will be "moved" by touch 4420 * @param pos the transition position at which to estimate the position 4421 * @param locationX the x location within the view (0.0 = left , 1.0 = right) 4422 * @param locationY the y location within the view (0.0 = left , 1.0 = right) 4423 * @param mAnchorDpDt returns the dx/dp and dy/dp 4424 */ 4425 void getAnchorDpDt(int mTouchAnchorId, 4426 float pos, 4427 float locationX, float locationY, 4428 float[] mAnchorDpDt) { 4429 View v; 4430 MotionController f = mFrameArrayList.get(v = getViewById(mTouchAnchorId)); 4431 if (DEBUG) { 4432 Log.v(TAG, " getAnchorDpDt " + Debug.getName(v) + " " + Debug.getLocation()); 4433 } 4434 if (f != null) { 4435 f.getDpDt(pos, locationX, locationY, mAnchorDpDt); 4436 float y = v.getY(); 4437 float deltaPos = pos - mLastPos; 4438 float deltaY = y - mLastY; 4439 @SuppressWarnings("unused") 4440 float dydp = (deltaPos != 0.0f) ? deltaY / deltaPos : Float.NaN; 4441 if (DEBUG) { 4442 Log.v(TAG, " getAnchorDpDt " + Debug.getName(v) + " " 4443 + Debug.getLocation() + " " + Arrays.toString(mAnchorDpDt)); 4444 } 4445 4446 mLastPos = pos; 4447 mLastY = y; 4448 } else { 4449 String idName = (v == null) ? "" + mTouchAnchorId : 4450 v.getContext().getResources().getResourceName(mTouchAnchorId); 4451 Log.w(TAG, "WARNING could not find view id " + idName); 4452 } 4453 } 4454 4455 /** 4456 * Gets the time of the currently set animation. 4457 * 4458 * @return time in Milliseconds 4459 */ 4460 public long getTransitionTimeMs() { 4461 if (mScene != null) { 4462 mTransitionDuration = mScene.getDuration() / 1000f; 4463 } 4464 return (long) (mTransitionDuration * 1000); 4465 } 4466 4467 /** 4468 * Set a listener to be notified of drawer events. 4469 * 4470 * @param listener Listener to notify when drawer events occur 4471 * @see TransitionListener 4472 */ 4473 public void setTransitionListener(TransitionListener listener) { 4474 mTransitionListener = listener; 4475 } 4476 4477 /** 4478 * adds a listener to be notified of drawer events. 4479 * 4480 * @param listener Listener to notify when drawer events occur 4481 * @see TransitionListener 4482 */ 4483 public void addTransitionListener(TransitionListener listener) { 4484 if (mTransitionListeners == null) { 4485 mTransitionListeners = new CopyOnWriteArrayList<>(); 4486 } 4487 mTransitionListeners.add(listener); 4488 } 4489 4490 /** 4491 * adds a listener to be notified of drawer events. 4492 * 4493 * @param listener Listener to notify when drawer events occur 4494 * @return <tt>true</tt> if it contained the specified listener 4495 * @see TransitionListener 4496 */ 4497 public boolean removeTransitionListener(TransitionListener listener) { 4498 if (mTransitionListeners == null) { 4499 return false; 4500 } 4501 return mTransitionListeners.remove(listener); 4502 } 4503 4504 /** 4505 * Listener for monitoring events about TransitionLayout. <b>Added in 2.0</b> 4506 */ 4507 public interface TransitionListener { 4508 /** 4509 * Called when a drawer is about to start a transition. 4510 * Note. startId may be -1 if starting from an "undefined state" 4511 * 4512 * @param motionLayout The TransitionLayout view that was moved 4513 * @param startId the id of the start state (or ConstraintSet). Will be -1 if unknown. 4514 * @param endId the id of the end state (or ConstraintSet). 4515 */ 4516 void onTransitionStarted(MotionLayout motionLayout, 4517 int startId, int endId); 4518 4519 /** 4520 * Called when a drawer's position changes. 4521 * 4522 * @param motionLayout The TransitionLayout view that was moved 4523 * @param startId the id of the start state (or ConstraintSet). Will be -1 if unknown. 4524 * @param endId the id of the end state (or ConstraintSet). 4525 * @param progress The progress on this transition, from 0 to 1. 4526 */ 4527 void onTransitionChange(MotionLayout motionLayout, 4528 int startId, int endId, 4529 float progress); 4530 4531 /** 4532 * Called when a drawer has settled completely a state. 4533 * The TransitionLayout is interactive at this point. 4534 * 4535 * @param motionLayout Drawer view that is now open 4536 * @param currentId the id it has reached 4537 */ 4538 void onTransitionCompleted(MotionLayout motionLayout, int currentId); 4539 4540 /** 4541 * Call when a trigger is fired 4542 * 4543 * @param motionLayout 4544 * @param triggerId The id set set with triggerID 4545 * @param positive for positive transition edge 4546 * @param progress 4547 */ 4548 void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive, 4549 float progress); 4550 } 4551 4552 /** 4553 * This causes the callback onTransitionTrigger to be called 4554 * 4555 * @param triggerId The id set set with triggerID 4556 * @param positive for positive transition edge 4557 * @param progress the current progress 4558 */ 4559 public void fireTrigger(int triggerId, boolean positive, float progress) { 4560 if (mTransitionListener != null) { 4561 mTransitionListener.onTransitionTrigger(this, triggerId, positive, progress); 4562 } 4563 if (mTransitionListeners != null) { 4564 for (TransitionListener listeners : mTransitionListeners) { 4565 listeners.onTransitionTrigger(this, triggerId, positive, progress); 4566 } 4567 } 4568 } 4569 4570 private void fireTransitionChange() { 4571 if (mTransitionListener != null 4572 || (mTransitionListeners != null && !mTransitionListeners.isEmpty())) { 4573 if (mListenerPosition != mTransitionPosition) { 4574 if (mListenerState != UNSET) { 4575 fireTransitionStarted(); 4576 mIsAnimating = true; 4577 } 4578 mListenerState = UNSET; 4579 mListenerPosition = mTransitionPosition; 4580 if (mTransitionListener != null) { 4581 mTransitionListener.onTransitionChange(this, 4582 mBeginState, mEndState, mTransitionPosition); 4583 } 4584 if (mTransitionListeners != null) { 4585 for (TransitionListener listeners : mTransitionListeners) { 4586 listeners.onTransitionChange(this, 4587 mBeginState, mEndState, mTransitionPosition); 4588 } 4589 } 4590 mIsAnimating = true; 4591 } 4592 } 4593 } 4594 4595 ArrayList<Integer> mTransitionCompleted = new ArrayList<>(); 4596 4597 /** 4598 * This causes the callback TransitionCompleted to be called 4599 */ 4600 protected void fireTransitionCompleted() { 4601 if (mTransitionListener != null 4602 || (mTransitionListeners != null && !mTransitionListeners.isEmpty())) { 4603 if (mListenerState == UNSET) { 4604 mListenerState = mCurrentState; 4605 int lastState = UNSET; 4606 if (!mTransitionCompleted.isEmpty()) { 4607 lastState = mTransitionCompleted.get(mTransitionCompleted.size() - 1); 4608 } 4609 if (lastState != mCurrentState && mCurrentState != -1) { 4610 mTransitionCompleted.add(mCurrentState); 4611 } 4612 } 4613 } 4614 processTransitionCompleted(); 4615 if (mOnComplete != null) { 4616 mOnComplete.run(); 4617 mOnComplete = null; 4618 } 4619 4620 if (mScheduledTransitionTo != null && mScheduledTransitions > 0) { 4621 transitionToState(mScheduledTransitionTo[0]); 4622 System.arraycopy(mScheduledTransitionTo, 4623 1, mScheduledTransitionTo, 4624 0, mScheduledTransitionTo.length - 1); 4625 mScheduledTransitions--; 4626 } 4627 } 4628 4629 private void processTransitionCompleted() { 4630 if (mTransitionListener == null 4631 && (mTransitionListeners == null || mTransitionListeners.isEmpty())) { 4632 return; 4633 } 4634 mIsAnimating = false; 4635 for (Integer state : mTransitionCompleted) { 4636 if (mTransitionListener != null) { 4637 mTransitionListener.onTransitionCompleted(this, state); 4638 } 4639 if (mTransitionListeners != null) { 4640 for (TransitionListener listeners : mTransitionListeners) { 4641 listeners.onTransitionCompleted(this, state); 4642 } 4643 } 4644 } 4645 mTransitionCompleted.clear(); 4646 } 4647 4648 /** 4649 * 4650 */ 4651 public DesignTool getDesignTool() { 4652 if (mDesignTool == null) { 4653 mDesignTool = new DesignTool(this); 4654 } 4655 return mDesignTool; 4656 } 4657 4658 /** 4659 * 4660 */ 4661 @Override 4662 public void onViewAdded(View view) { 4663 super.onViewAdded(view); 4664 if (view instanceof MotionHelper) { 4665 MotionHelper helper = (MotionHelper) view; 4666 if (mTransitionListeners == null) { 4667 mTransitionListeners = new CopyOnWriteArrayList<>(); 4668 } 4669 mTransitionListeners.add(helper); 4670 4671 if (helper.isUsedOnShow()) { 4672 if (mOnShowHelpers == null) { 4673 mOnShowHelpers = new ArrayList<>(); 4674 } 4675 mOnShowHelpers.add(helper); 4676 } 4677 if (helper.isUseOnHide()) { 4678 if (mOnHideHelpers == null) { 4679 mOnHideHelpers = new ArrayList<>(); 4680 } 4681 mOnHideHelpers.add(helper); 4682 } 4683 if (helper.isDecorator()) { 4684 if (mDecoratorsHelpers == null) { 4685 mDecoratorsHelpers = new ArrayList<>(); 4686 } 4687 mDecoratorsHelpers.add(helper); 4688 } 4689 } 4690 } 4691 4692 /** 4693 * 4694 */ 4695 @Override 4696 public void onViewRemoved(View view) { 4697 super.onViewRemoved(view); 4698 if (mOnShowHelpers != null) { 4699 mOnShowHelpers.remove(view); 4700 } 4701 if (mOnHideHelpers != null) { 4702 mOnHideHelpers.remove(view); 4703 } 4704 } 4705 4706 /** 4707 * Notify OnShow motion helpers 4708 * @param progress 4709 */ 4710 public void setOnShow(float progress) { 4711 if (mOnShowHelpers != null) { 4712 final int count = mOnShowHelpers.size(); 4713 for (int i = 0; i < count; i++) { 4714 MotionHelper helper = mOnShowHelpers.get(i); 4715 helper.setProgress(progress); 4716 } 4717 } 4718 } 4719 4720 /** 4721 * Notify OnHide motion helpers 4722 * @param progress 4723 */ 4724 public void setOnHide(float progress) { 4725 if (mOnHideHelpers != null) { 4726 final int count = mOnHideHelpers.size(); 4727 for (int i = 0; i < count; i++) { 4728 MotionHelper helper = mOnHideHelpers.get(i); 4729 helper.setProgress(progress); 4730 } 4731 } 4732 } 4733 4734 /** 4735 * Get the id's of all constraintSets used by MotionLayout 4736 * 4737 * @return 4738 */ 4739 public @IdRes 4740 int[] getConstraintSetIds() { 4741 if (mScene == null) { 4742 return null; 4743 } 4744 return mScene.getConstraintSetIds(); 4745 } 4746 4747 /** 4748 * Get the id's of all constraintSets with the matching types 4749 * 4750 * @return 4751 */ 4752 public int[] getMatchingConstraintSetIds(String ... types) { 4753 if (mScene == null) { 4754 return null; 4755 } 4756 return mScene.getMatchingStateLabels(types); 4757 } 4758 4759 /** 4760 * Get the ConstraintSet associated with an id 4761 * This returns a link to the constraintSet 4762 * But in most cases can be used. 4763 * createConstraintSet makes a copy which is more expensive. 4764 * 4765 * @param id of the constraintSet 4766 * @return ConstraintSet of MotionLayout 4767 * @see #cloneConstraintSet(int) 4768 */ 4769 public ConstraintSet getConstraintSet(int id) { 4770 if (mScene == null) { 4771 return null; 4772 } 4773 return mScene.getConstraintSet(id); 4774 } 4775 4776 /** 4777 * Creates a ConstraintSet based on an existing 4778 * constraintSet. 4779 * This makes a copy of the ConstraintSet. 4780 * 4781 * @param id The ide of the ConstraintSet 4782 * @return the ConstraintSet 4783 */ 4784 public ConstraintSet cloneConstraintSet(int id) { 4785 if (mScene == null) { 4786 return null; 4787 } 4788 ConstraintSet orig = mScene.getConstraintSet(id); 4789 ConstraintSet ret = new ConstraintSet(); 4790 ret.clone(orig); 4791 return ret; 4792 } 4793 4794 /** 4795 * rebuild the motion Layouts 4796 * 4797 * @deprecated Please call rebuildScene() instead. 4798 */ 4799 @Deprecated 4800 public void rebuildMotion() { 4801 Log.e(TAG, "This method is deprecated. Please call rebuildScene() instead."); 4802 rebuildScene(); 4803 } 4804 4805 /** 4806 * rebuild the motion Layouts 4807 */ 4808 public void rebuildScene() { 4809 mModel.reEvaluateState(); 4810 invalidate(); 4811 } 4812 4813 /** 4814 * update a ConstraintSet under the id. 4815 * 4816 * @param stateId id of the ConstraintSet 4817 * @param set The constraintSet 4818 */ 4819 public void updateState(int stateId, ConstraintSet set) { 4820 if (mScene != null) { 4821 mScene.setConstraintSet(stateId, set); 4822 } 4823 updateState(); 4824 if (mCurrentState == stateId) { 4825 set.applyTo(this); 4826 } 4827 } 4828 4829 /** 4830 * Update a ConstraintSet but animate the change. 4831 * 4832 * @param stateId id of the ConstraintSet 4833 * @param set The constraintSet 4834 * @param duration The length of time to perform the animation 4835 */ 4836 public void updateStateAnimate(int stateId, ConstraintSet set, int duration) { 4837 if (mScene == null) { 4838 return; 4839 } 4840 4841 if (mCurrentState == stateId) { 4842 updateState(R.id.view_transition, getConstraintSet(stateId)); 4843 setState(R.id.view_transition, -1, -1); 4844 updateState(stateId, set); 4845 MotionScene.Transition tmpTransition = 4846 new MotionScene.Transition(-1, mScene, R.id.view_transition, stateId); 4847 tmpTransition.setDuration(duration); 4848 setTransition(tmpTransition); 4849 transitionToEnd(); 4850 } 4851 } 4852 4853 /** 4854 * on completing the current transition, transition to this state. 4855 * 4856 * @param id 4857 */ 4858 public void scheduleTransitionTo(int id) { 4859 if (getCurrentState() == -1) { 4860 transitionToState(id); 4861 } else { 4862 if (mScheduledTransitionTo == null) { 4863 mScheduledTransitionTo = new int[4]; 4864 } else if (mScheduledTransitionTo.length <= mScheduledTransitions) { 4865 mScheduledTransitionTo = 4866 Arrays.copyOf(mScheduledTransitionTo, mScheduledTransitionTo.length * 2); 4867 } 4868 mScheduledTransitionTo[mScheduledTransitions++] = id; 4869 } 4870 } 4871 4872 /** 4873 * Not sure we want this 4874 * 4875 * 4876 */ 4877 public void updateState() { 4878 mModel.initFrom(mLayoutWidget, 4879 mScene.getConstraintSet(mBeginState), 4880 mScene.getConstraintSet(mEndState)); 4881 rebuildScene(); 4882 } 4883 4884 /** 4885 * Get all Transitions known to the system. 4886 * 4887 * @return 4888 */ 4889 public ArrayList<MotionScene.Transition> getDefinedTransitions() { 4890 if (mScene == null) { 4891 return null; 4892 } 4893 return mScene.getDefinedTransitions(); 4894 } 4895 4896 /** 4897 * Gets the state you are currently transitioning from. 4898 * If you are transitioning from an unknown state returns -1 4899 * 4900 * @return State you are transitioning from. 4901 */ 4902 public int getStartState() { 4903 return mBeginState; 4904 } 4905 4906 /** 4907 * Gets the state you are currently transition to. 4908 * 4909 * @return The State you are transitioning to. 4910 */ 4911 public int getEndState() { 4912 return mEndState; 4913 } 4914 4915 /** 4916 * Gets the position you are animating to typically 0 or 1. 4917 * This is useful during animation after touch up 4918 * 4919 * @return The target position you are moving to 4920 */ 4921 public float getTargetPosition() { 4922 return mTransitionGoalPosition; 4923 } 4924 4925 /** 4926 * Change the current Transition duration. 4927 * 4928 * @param milliseconds duration for transition to complete 4929 */ 4930 public void setTransitionDuration(int milliseconds) { 4931 if (mScene == null) { 4932 Log.e(TAG, "MotionScene not defined"); 4933 return; 4934 } 4935 mScene.setDuration(milliseconds); 4936 } 4937 4938 /** 4939 * This returns the internal Transition Structure 4940 * 4941 * @param id 4942 * @return 4943 */ 4944 public MotionScene.Transition getTransition(int id) { 4945 return mScene.getTransitionById(id); 4946 } 4947 4948 /** 4949 * This looks up the constraintset ID given an id string ( 4950 * 4951 * @param id String id (without the "@+id/") 4952 * @return the integer id of the string 4953 * 4954 */ 4955 int lookUpConstraintId(String id) { 4956 if (mScene == null) { 4957 return 0; 4958 } 4959 return mScene.lookUpConstraintId(id); 4960 } 4961 4962 /** 4963 * does a revers look up to find the ConstraintSets Name 4964 * 4965 * @param id the integer id of the constraintSet 4966 * @return 4967 */ 4968 String getConstraintSetNames(int id) { 4969 if (mScene == null) { 4970 return null; 4971 } 4972 return mScene.lookUpConstraintName(id); 4973 } 4974 4975 /** 4976 * this allow disabling autoTransitions to prevent design surface from being in undefined states 4977 * 4978 * @param disable 4979 */ 4980 void disableAutoTransition(boolean disable) { 4981 if (mScene == null) { 4982 return; 4983 } 4984 mScene.disableAutoTransition(disable); 4985 } 4986 4987 /** 4988 * Enables (or disables) MotionLayout's onClick and onSwipe handling. 4989 * 4990 * @param enabled If true, touch & click is enabled; otherwise it is disabled 4991 */ 4992 public void setInteractionEnabled(boolean enabled) { 4993 mInteractionEnabled = enabled; 4994 } 4995 4996 /** 4997 * Determines whether MotionLayout's touch & click handling are enabled. 4998 * An interaction enabled MotionLayout can respond to user input and initiate and control. 4999 * MotionLayout interactions are enabled initially by default. 5000 * MotionLayout touch & click handling may be enabled or disabled by calling its 5001 * setInteractionEnabled method. 5002 * 5003 * @return true if MotionLayout's touch & click is enabled, false otherwise 5004 */ 5005 public boolean isInteractionEnabled() { 5006 return mInteractionEnabled; 5007 } 5008 5009 private void fireTransitionStarted() { 5010 if (mTransitionListener != null) { 5011 mTransitionListener.onTransitionStarted(this, mBeginState, mEndState); 5012 } 5013 if (mTransitionListeners != null) { 5014 for (TransitionListener listeners : mTransitionListeners) { 5015 listeners.onTransitionStarted(this, mBeginState, mEndState); 5016 } 5017 } 5018 } 5019 5020 /** 5021 * Execute a ViewTransition. 5022 * Transition will execute if its conditions are met and it is enabled 5023 * 5024 * @param viewTransitionId 5025 * @param view The views to apply to 5026 */ 5027 public void viewTransition(int viewTransitionId, View... view) { 5028 if (mScene != null) { 5029 mScene.viewTransition(viewTransitionId, view); 5030 } else { 5031 Log.e(TAG, " no motionScene"); 5032 } 5033 } 5034 5035 /** 5036 * Enable a ViewTransition ID. 5037 * 5038 * @param viewTransitionId id of ViewTransition 5039 * @param enable If false view transition cannot be executed. 5040 */ 5041 public void enableViewTransition(int viewTransitionId, boolean enable) { 5042 if (mScene != null) { 5043 mScene.enableViewTransition(viewTransitionId, enable); 5044 } 5045 } 5046 5047 /** 5048 * Is transition id enabled or disabled 5049 * 5050 * @param viewTransitionId the ide of the transition 5051 * @return true if enabled 5052 */ 5053 public boolean isViewTransitionEnabled(int viewTransitionId) { 5054 if (mScene != null) { 5055 return mScene.isViewTransitionEnabled(viewTransitionId); 5056 } 5057 return false; 5058 } 5059 5060 /** 5061 * Apply the view transitions keyFrames to the MotionController. 5062 * Note ConstraintOverride is not used 5063 * 5064 * @param viewTransitionId the id of the view transition 5065 * @param motionController the MotionController to apply the keyframes to 5066 * @return true if it found and applied the viewTransition false otherwise 5067 */ 5068 public boolean applyViewTransition(int viewTransitionId, MotionController motionController) { 5069 if (mScene != null) { 5070 return mScene.applyViewTransition(viewTransitionId, motionController); 5071 } 5072 return false; 5073 } 5074 5075 /** 5076 * Is initial state changes are applied during onAttachedToWindow or after. 5077 * @return 5078 */ 5079 public boolean isDelayedApplicationOfInitialState() { 5080 return mDelayedApply; 5081 } 5082 5083 /** 5084 * Initial state changes are applied during onAttachedToWindow unless this is set to true. 5085 * @param delayedApply 5086 */ 5087 public void setDelayedApplicationOfInitialState(boolean delayedApply) { 5088 this.mDelayedApply = delayedApply; 5089 } 5090 5091 } 5092