1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.constraintlayout.motion.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.content.res.XmlResourceParser; 22 import android.graphics.Rect; 23 import android.util.AttributeSet; 24 import android.util.Log; 25 import android.util.TypedValue; 26 import android.util.Xml; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.animation.AccelerateDecelerateInterpolator; 31 import android.view.animation.AccelerateInterpolator; 32 import android.view.animation.AnimationUtils; 33 import android.view.animation.AnticipateInterpolator; 34 import android.view.animation.BounceInterpolator; 35 import android.view.animation.DecelerateInterpolator; 36 import android.view.animation.Interpolator; 37 import android.view.animation.OvershootInterpolator; 38 39 import androidx.constraintlayout.core.motion.utils.Easing; 40 import androidx.constraintlayout.core.motion.utils.KeyCache; 41 import androidx.constraintlayout.widget.ConstraintAttribute; 42 import androidx.constraintlayout.widget.ConstraintLayout; 43 import androidx.constraintlayout.widget.ConstraintSet; 44 import androidx.constraintlayout.widget.R; 45 46 import org.xmlpull.v1.XmlPullParser; 47 import org.xmlpull.v1.XmlPullParserException; 48 49 import java.io.IOException; 50 import java.util.ArrayList; 51 52 /** 53 * Provides a support for <ViewTransition> tag 54 * it Parses tag 55 * it implement the transition 56 * it will update ConstraintSet or sets 57 * For asynchronous it will create and drive a MotionController. 58 */ 59 public class ViewTransition { 60 private static final String TAG = "ViewTransition"; 61 ConstraintSet mSet; 62 public static final String VIEW_TRANSITION_TAG = "ViewTransition"; 63 public static final String KEY_FRAME_SET_TAG = "KeyFrameSet"; 64 public static final String CONSTRAINT_OVERRIDE = "ConstraintOverride"; 65 public static final String CUSTOM_ATTRIBUTE = "CustomAttribute"; 66 public static final String CUSTOM_METHOD = "CustomMethod"; 67 68 private static final int UNSET = -1; 69 private int mId; 70 // Transition can be up or down of manually fired 71 public static final int ONSTATE_ACTION_DOWN = 1; 72 public static final int ONSTATE_ACTION_UP = 2; 73 public static final int ONSTATE_ACTION_DOWN_UP = 3; 74 public static final int ONSTATE_SHARED_VALUE_SET = 4; 75 public static final int ONSTATE_SHARED_VALUE_UNSET = 5; 76 77 private int mOnStateTransition = UNSET; 78 private boolean mDisabled = false; 79 private int mPathMotionArc = 0; 80 int mViewTransitionMode; 81 static final int VIEWTRANSITIONMODE_CURRENTSTATE = 0; 82 static final int VIEWTRANSITIONMODE_ALLSTATES = 1; 83 static final int VIEWTRANSITIONMODE_NOSTATE = 2; 84 KeyFrames mKeyFrames; 85 ConstraintSet.Constraint mConstraintDelta; 86 private int mDuration = UNSET; 87 private int mUpDuration = UNSET; 88 89 private int mTargetId; 90 private String mTargetString; 91 92 // interpolator code 93 private static final int SPLINE_STRING = -1; 94 private static final int INTERPOLATOR_REFERENCE_ID = -2; 95 private int mDefaultInterpolator = 0; 96 private String mDefaultInterpolatorString = null; 97 private int mDefaultInterpolatorID = -1; 98 static final int EASE_IN_OUT = 0; 99 static final int EASE_IN = 1; 100 static final int EASE_OUT = 2; 101 static final int LINEAR = 3; 102 static final int BOUNCE = 4; 103 static final int OVERSHOOT = 5; 104 static final int ANTICIPATE = 6; 105 106 Context mContext; 107 private int mSetsTag = UNSET; 108 private int mClearsTag = UNSET; 109 private int mIfTagSet = UNSET; 110 private int mIfTagNotSet = UNSET; 111 112 // shared value management. mSharedValueId is the key we are watching, 113 // mSharedValueCurrent the current value for that key, and mSharedValueTarget 114 // is the target we are waiting for to trigger. 115 private int mSharedValueTarget = UNSET; 116 private int mSharedValueID = UNSET; 117 private int mSharedValueCurrent = UNSET; 118 getSharedValueCurrent()119 public int getSharedValueCurrent() { 120 return mSharedValueCurrent; 121 } 122 setSharedValueCurrent(int sharedValueCurrent)123 public void setSharedValueCurrent(int sharedValueCurrent) { 124 this.mSharedValueCurrent = sharedValueCurrent; 125 } 126 127 /** 128 * Gets the type of transition to listen to. 129 * 130 * @return ONSTATE_TRANSITION_* 131 */ getStateTransition()132 public int getStateTransition() { 133 return mOnStateTransition; 134 } 135 136 /** 137 * Sets the type of transition to listen to. 138 * 139 * @param stateTransition 140 */ setStateTransition(int stateTransition)141 public void setStateTransition(int stateTransition) { 142 this.mOnStateTransition = stateTransition; 143 } 144 145 /** 146 * Gets the SharedValue it will be listening for. 147 * 148 * @return 149 */ getSharedValue()150 public int getSharedValue() { 151 return mSharedValueTarget; 152 } 153 154 /** 155 * sets the SharedValue it will be listening for. 156 */ setSharedValue(int sharedValue)157 public void setSharedValue(int sharedValue) { 158 this.mSharedValueTarget = sharedValue; 159 } 160 161 /** 162 * Gets the ID of the SharedValue it will be listening for. 163 * 164 * @return the id of the shared value 165 */ getSharedValueID()166 public int getSharedValueID() { 167 return mSharedValueID; 168 } 169 170 /** 171 * sets the ID of the SharedValue it will be listening for. 172 */ setSharedValueID(int sharedValueID)173 public void setSharedValueID(int sharedValueID) { 174 this.mSharedValueID = sharedValueID; 175 } 176 177 /** 178 * debug string for a ViewTransition 179 * @return 180 */ 181 @Override toString()182 public String toString() { 183 return "ViewTransition(" + Debug.getName(mContext, mId) + ")"; 184 } 185 getInterpolator(Context context)186 Interpolator getInterpolator(Context context) { 187 switch (mDefaultInterpolator) { 188 case SPLINE_STRING: 189 final Easing easing = Easing.getInterpolator(mDefaultInterpolatorString); 190 return new Interpolator() { 191 @Override 192 public float getInterpolation(float v) { 193 return (float) easing.get(v); 194 } 195 }; 196 case INTERPOLATOR_REFERENCE_ID: 197 return AnimationUtils.loadInterpolator(context, 198 mDefaultInterpolatorID); 199 case EASE_IN_OUT: 200 return new AccelerateDecelerateInterpolator(); 201 case EASE_IN: 202 return new AccelerateInterpolator(); 203 case EASE_OUT: 204 return new DecelerateInterpolator(); 205 case LINEAR: 206 return null; 207 case ANTICIPATE: 208 return new AnticipateInterpolator(); 209 case OVERSHOOT: 210 return new OvershootInterpolator(); 211 case BOUNCE: 212 return new BounceInterpolator(); 213 } 214 return null; 215 } 216 217 ViewTransition(Context context, XmlPullParser parser) { 218 mContext = context; 219 try { 220 for (int eventType = parser.getEventType(); 221 eventType != XmlResourceParser.END_DOCUMENT; 222 eventType = parser.next()) { 223 switch (eventType) { 224 case XmlResourceParser.START_DOCUMENT: 225 case XmlResourceParser.TEXT: 226 break; 227 case XmlResourceParser.START_TAG: 228 String tagName = parser.getName(); 229 switch (tagName) { 230 case VIEW_TRANSITION_TAG: 231 parseViewTransitionTags(context, parser); 232 break; 233 case KEY_FRAME_SET_TAG: 234 mKeyFrames = new KeyFrames(context, parser); 235 break; 236 case CONSTRAINT_OVERRIDE: 237 mConstraintDelta = ConstraintSet.buildDelta(context, parser); 238 break; 239 case CUSTOM_ATTRIBUTE: 240 case CUSTOM_METHOD: 241 ConstraintAttribute.parse(context, parser, 242 mConstraintDelta.mCustomConstraints); 243 break; 244 default: 245 Log.e(TAG, Debug.getLoc() + " unknown tag " + tagName); 246 Log.e(TAG, ".xml:" + parser.getLineNumber()); 247 } 248 249 break; 250 case XmlResourceParser.END_TAG: 251 if (VIEW_TRANSITION_TAG.equals(parser.getName())) { 252 return; 253 } 254 break; 255 } 256 } 257 } catch (XmlPullParserException e) { 258 Log.e(TAG, "Error parsing XML resource", e); 259 } catch (IOException e) { 260 Log.e(TAG, "Error parsing XML resource", e); 261 } 262 } 263 264 private void parseViewTransitionTags(Context context, XmlPullParser parser) { 265 AttributeSet attrs = Xml.asAttributeSet(parser); 266 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewTransition); 267 final int count = a.getIndexCount(); 268 for (int i = 0; i < count; i++) { 269 int attr = a.getIndex(i); 270 if (attr == R.styleable.ViewTransition_android_id) { 271 mId = a.getResourceId(attr, mId); 272 } else if (attr == R.styleable.ViewTransition_motionTarget) { 273 if (MotionLayout.IS_IN_EDIT_MODE) { 274 mTargetId = a.getResourceId(attr, mTargetId); 275 if (mTargetId == -1) { 276 mTargetString = a.getString(attr); 277 } 278 } else { 279 if (a.peekValue(attr).type == TypedValue.TYPE_STRING) { 280 mTargetString = a.getString(attr); 281 } else { 282 mTargetId = a.getResourceId(attr, mTargetId); 283 } 284 } 285 } else if (attr == R.styleable.ViewTransition_onStateTransition) { 286 mOnStateTransition = a.getInt(attr, mOnStateTransition); 287 } else if (attr == R.styleable.ViewTransition_transitionDisable) { 288 mDisabled = a.getBoolean(attr, mDisabled); 289 } else if (attr == R.styleable.ViewTransition_pathMotionArc) { 290 mPathMotionArc = a.getInt(attr, mPathMotionArc); 291 } else if (attr == R.styleable.ViewTransition_duration) { 292 mDuration = a.getInt(attr, mDuration); 293 } else if (attr == R.styleable.ViewTransition_upDuration) { 294 mUpDuration = a.getInt(attr, mUpDuration); 295 } else if (attr == R.styleable.ViewTransition_viewTransitionMode) { 296 mViewTransitionMode = a.getInt(attr, mViewTransitionMode); 297 } else if (attr == R.styleable.ViewTransition_motionInterpolator) { 298 TypedValue type = a.peekValue(attr); 299 if (type.type == TypedValue.TYPE_REFERENCE) { 300 mDefaultInterpolatorID = a.getResourceId(attr, -1); 301 if (mDefaultInterpolatorID != UNSET) { 302 mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID; 303 } 304 } else if (type.type == TypedValue.TYPE_STRING) { 305 mDefaultInterpolatorString = a.getString(attr); 306 if (mDefaultInterpolatorString != null 307 && mDefaultInterpolatorString.indexOf("/") > 0) { 308 mDefaultInterpolatorID = a.getResourceId(attr, UNSET); 309 mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID; 310 } else { 311 mDefaultInterpolator = SPLINE_STRING; 312 } 313 } else { 314 mDefaultInterpolator = a.getInteger(attr, mDefaultInterpolator); 315 } 316 } else if (attr == R.styleable.ViewTransition_setsTag) { 317 mSetsTag = a.getResourceId(attr, mSetsTag); 318 } else if (attr == R.styleable.ViewTransition_clearsTag) { 319 mClearsTag = a.getResourceId(attr, mClearsTag); 320 } else if (attr == R.styleable.ViewTransition_ifTagSet) { 321 mIfTagSet = a.getResourceId(attr, mIfTagSet); 322 } else if (attr == R.styleable.ViewTransition_ifTagNotSet) { 323 mIfTagNotSet = a.getResourceId(attr, mIfTagNotSet); 324 } else if (attr == R.styleable.ViewTransition_SharedValueId) { 325 mSharedValueID = a.getResourceId(attr, mSharedValueID); 326 } else if (attr == R.styleable.ViewTransition_SharedValue) { 327 mSharedValueTarget = a.getInteger(attr, mSharedValueTarget); 328 } 329 } 330 a.recycle(); 331 } 332 333 void applyIndependentTransition(ViewTransitionController controller, 334 MotionLayout motionLayout, 335 View view) { 336 MotionController motionController = new MotionController(view); 337 motionController.setBothStates(view); 338 mKeyFrames.addAllFrames(motionController); 339 motionController.setup(motionLayout.getWidth(), motionLayout.getHeight(), 340 mDuration, System.nanoTime()); 341 new Animate(controller, motionController, 342 mDuration, mUpDuration, mOnStateTransition, 343 getInterpolator(motionLayout.getContext()), mSetsTag, mClearsTag); 344 } 345 346 static class Animate { 347 private final int mSetsTag; 348 private final int mClearsTag; 349 long mStart; 350 MotionController mMC; 351 int mDuration; 352 int mUpDuration; 353 KeyCache mCache = new KeyCache(); 354 ViewTransitionController mVtController; 355 Interpolator mInterpolator; 356 boolean mReverse = false; 357 float mPosition; 358 float mDpositionDt; 359 long mLastRender; 360 Rect mTempRec = new Rect(); 361 boolean mHoldAt100 = false; 362 363 Animate(ViewTransitionController controller, 364 MotionController motionController, 365 int duration, int upDuration, int mode, 366 Interpolator interpolator, int setTag, int clearTag) { 367 mVtController = controller; 368 mMC = motionController; 369 mDuration = duration; 370 mUpDuration = upDuration; 371 mStart = System.nanoTime(); 372 mLastRender = mStart; 373 mVtController.addAnimation(this); 374 mInterpolator = interpolator; 375 mSetsTag = setTag; 376 mClearsTag = clearTag; 377 if (mode == ONSTATE_ACTION_DOWN_UP) { 378 mHoldAt100 = true; 379 } 380 mDpositionDt = (duration == 0) ? Float.MAX_VALUE : 1f / duration; 381 mutate(); 382 } 383 384 void reverse(boolean dir) { 385 mReverse = dir; 386 if (mReverse && mUpDuration != UNSET) { 387 mDpositionDt = (mUpDuration == 0) ? Float.MAX_VALUE : 1f / mUpDuration; 388 } 389 mVtController.invalidate(); 390 mLastRender = System.nanoTime(); 391 } 392 393 void mutate() { 394 if (mReverse) { 395 mutateReverse(); 396 } else { 397 mutateForward(); 398 } 399 } 400 401 void mutateReverse() { 402 long current = System.nanoTime(); 403 long elapse = current - mLastRender; 404 mLastRender = current; 405 406 mPosition -= ((float) (elapse * 1E-6)) * mDpositionDt; 407 if (mPosition < 0.0f) { 408 mPosition = 0.0f; 409 } 410 411 float ipos = (mInterpolator == null) ? mPosition 412 : mInterpolator.getInterpolation(mPosition); 413 boolean repaint = mMC.interpolate(mMC.mView, ipos, current, mCache); 414 415 if (mPosition <= 0) { 416 if (mSetsTag != UNSET) { 417 mMC.getView().setTag(mSetsTag, System.nanoTime()); 418 } 419 if (mClearsTag != UNSET) { 420 mMC.getView().setTag(mClearsTag, null); 421 } 422 mVtController.removeAnimation(this); 423 } 424 if (mPosition > 0f || repaint) { 425 mVtController.invalidate(); 426 } 427 } 428 429 void mutateForward() { 430 431 long current = System.nanoTime(); 432 long elapse = current - mLastRender; 433 mLastRender = current; 434 435 mPosition += ((float) (elapse * 1E-6)) * mDpositionDt; 436 if (mPosition >= 1.0f) { 437 mPosition = 1.0f; 438 } 439 440 float ipos = (mInterpolator == null) ? mPosition 441 : mInterpolator.getInterpolation(mPosition); 442 boolean repaint = mMC.interpolate(mMC.mView, ipos, current, mCache); 443 444 if (mPosition >= 1) { 445 if (mSetsTag != UNSET) { 446 mMC.getView().setTag(mSetsTag, System.nanoTime()); 447 } 448 if (mClearsTag != UNSET) { 449 mMC.getView().setTag(mClearsTag, null); 450 } 451 if (!mHoldAt100) { 452 mVtController.removeAnimation(this); 453 } 454 } 455 if (mPosition < 1f || repaint) { 456 mVtController.invalidate(); 457 } 458 } 459 460 public void reactTo(int action, float x, float y) { 461 switch (action) { 462 case MotionEvent.ACTION_UP: 463 if (!mReverse) { 464 reverse(true); 465 } 466 return; 467 case MotionEvent.ACTION_MOVE: 468 View view = mMC.getView(); 469 view.getHitRect(mTempRec); 470 if (!mTempRec.contains((int) x, (int) y)) { 471 if (!mReverse) { 472 reverse(true); 473 } 474 } 475 } 476 } 477 } 478 479 void applyTransition(ViewTransitionController controller, 480 MotionLayout layout, 481 int fromId, 482 ConstraintSet current, 483 View... views) { 484 if (mDisabled) { 485 return; 486 } 487 if (mViewTransitionMode == VIEWTRANSITIONMODE_NOSTATE) { 488 applyIndependentTransition(controller, layout, views[0]); 489 return; 490 } 491 if (mViewTransitionMode == VIEWTRANSITIONMODE_ALLSTATES) { 492 int[] ids = layout.getConstraintSetIds(); 493 for (int i = 0; i < ids.length; i++) { 494 int id = ids[i]; 495 if (id == fromId) { 496 continue; 497 } 498 ConstraintSet cSet = layout.getConstraintSet(id); 499 for (View view : views) { 500 ConstraintSet.Constraint constraint = cSet.getConstraint(view.getId()); 501 if (mConstraintDelta != null) { 502 mConstraintDelta.applyDelta(constraint); 503 constraint.mCustomConstraints.putAll(mConstraintDelta.mCustomConstraints); 504 } 505 } 506 } 507 } 508 509 ConstraintSet transformedState = new ConstraintSet(); 510 transformedState.clone(current); 511 for (View view : views) { 512 ConstraintSet.Constraint constraint = transformedState.getConstraint(view.getId()); 513 if (mConstraintDelta != null) { 514 mConstraintDelta.applyDelta(constraint); 515 constraint.mCustomConstraints.putAll(mConstraintDelta.mCustomConstraints); 516 } 517 } 518 519 layout.updateState(fromId, transformedState); 520 layout.updateState(R.id.view_transition, current); 521 layout.setState(R.id.view_transition, -1, -1); 522 MotionScene.Transition tmpTransition = 523 new MotionScene.Transition(-1, layout.mScene, R.id.view_transition, fromId); 524 for (View view : views) { 525 updateTransition(tmpTransition, view); 526 } 527 layout.setTransition(tmpTransition); 528 layout.transitionToEnd(() -> { 529 if (mSetsTag != UNSET) { 530 for (View view : views) { 531 view.setTag(mSetsTag, System.nanoTime()); 532 } 533 } 534 if (mClearsTag != UNSET) { 535 for (View view : views) { 536 view.setTag(mClearsTag, null); 537 } 538 } 539 }); 540 } 541 542 private void updateTransition(MotionScene.Transition transition, View view) { 543 if (mDuration != -1) { 544 transition.setDuration(mDuration); 545 } 546 transition.setPathMotionArc(mPathMotionArc); 547 transition.setInterpolatorInfo(mDefaultInterpolator, 548 mDefaultInterpolatorString, mDefaultInterpolatorID); 549 int id = view.getId(); 550 if (mKeyFrames != null) { 551 ArrayList<Key> keys = mKeyFrames.getKeyFramesForView(KeyFrames.UNSET); 552 KeyFrames keyFrames = new KeyFrames(); 553 for (Key key : keys) { 554 keyFrames.addKey(key.clone().setViewId(id)); 555 } 556 557 transition.addKeyFrame(keyFrames); 558 } 559 } 560 561 int getId() { 562 return mId; 563 } 564 565 void setId(int id) { 566 this.mId = id; 567 } 568 569 boolean matchesView(View view) { 570 if (view == null) { 571 return false; 572 } 573 if (mTargetId == -1 && mTargetString == null) { 574 return false; 575 } 576 if (!checkTags(view)) { 577 return false; 578 } 579 if (view.getId() == mTargetId) { 580 return true; 581 } 582 if (mTargetString == null) { 583 return false; 584 } 585 ViewGroup.LayoutParams lp = view.getLayoutParams(); 586 if (lp instanceof ConstraintLayout.LayoutParams) { 587 String tag = ((ConstraintLayout.LayoutParams) view.getLayoutParams()).constraintTag; 588 if (tag != null && tag.matches(mTargetString)) { 589 return true; 590 } 591 } 592 return false; 593 } 594 595 boolean supports(int action) { 596 if (mOnStateTransition == ONSTATE_ACTION_DOWN) { 597 return action == MotionEvent.ACTION_DOWN; 598 } 599 if (mOnStateTransition == ONSTATE_ACTION_UP) { 600 return action == MotionEvent.ACTION_UP; 601 } 602 if (mOnStateTransition == ONSTATE_ACTION_DOWN_UP) { 603 return action == MotionEvent.ACTION_DOWN; 604 } 605 return false; 606 } 607 608 boolean isEnabled() { 609 return !mDisabled; 610 } 611 612 void setEnabled(boolean enable) { 613 this.mDisabled = !enable; 614 } 615 616 boolean checkTags(View view) { 617 618 boolean set = (mIfTagSet == UNSET) ? true : (null != view.getTag(mIfTagSet)); 619 boolean notSet = (mIfTagNotSet == UNSET) ? true : null == view.getTag(mIfTagNotSet); 620 return set && notSet; 621 } 622 } 623