/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.constraintlayout.motion.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.util.Xml;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;
import androidx.constraintlayout.core.motion.utils.Easing;
import androidx.constraintlayout.core.motion.utils.KeyCache;
import androidx.constraintlayout.widget.ConstraintAttribute;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
/**
* Provides a support for <ViewTransition> tag
* it Parses tag
* it implement the transition
* it will update ConstraintSet or sets
* For asynchronous it will create and drive a MotionController.
*/
public class ViewTransition {
private static String TAG = "ViewTransition";
ConstraintSet set;
public static final String VIEW_TRANSITION_TAG = "ViewTransition";
public static final String KEY_FRAME_SET_TAG = "KeyFrameSet";
public static final String CONSTRAINT_OVERRIDE = "ConstraintOverride";
public static final String CUSTOM_ATTRIBUTE = "CustomAttribute";
public static final String CUSTOM_METHOD = "CustomMethod";
private static final int UNSET = -1;
private int mId;
// Transition can be up or down of manually fired
public static final int ONSTATE_ACTION_DOWN = 1;
public static final int ONSTATE_ACTION_UP = 2;
public static final int ONSTATE_ACTION_DOWN_UP = 3;
public static final int ONSTATE_SHARED_VALUE_SET = 4;
public static final int ONSTATE_SHARED_VALUE_UNSET = 5;
private int mOnStateTransition = UNSET;
private boolean mDisabled = false;
private int mPathMotionArc = 0;
int mViewTransitionMode;
static final int VIEWTRANSITIONMODE_CURRENTSTATE = 0;
static final int VIEWTRANSITIONMODE_ALLSTATES = 1;
static final int VIEWTRANSITIONMODE_NOSTATE = 2;
KeyFrames mKeyFrames;
ConstraintSet.Constraint mConstraintDelta;
private int mDuration = UNSET;
private int mUpDuration = UNSET;
private int mTargetId;
private String mTargetString;
// interpolator code
private static final int SPLINE_STRING = -1;
private static final int INTERPOLATOR_REFERENCE_ID = -2;
private int mDefaultInterpolator = 0;
private String mDefaultInterpolatorString = null;
private int mDefaultInterpolatorID = -1;
static final int EASE_IN_OUT = 0;
static final int EASE_IN = 1;
static final int EASE_OUT = 2;
static final int LINEAR = 3;
static final int BOUNCE = 4;
static final int OVERSHOOT = 5;
static final int ANTICIPATE = 6;
Context mContext;
private int mSetsTag = UNSET;
private int mClearsTag = UNSET;
private int mIfTagSet = UNSET;
private int mIfTagNotSet = UNSET;
// shared value management. mSharedValueId is the key we are watching,
// mSharedValueCurrent the current value for that key, and mSharedValueTarget
// is the target we are waiting for to trigger.
private int mSharedValueTarget = UNSET;
private int mSharedValueID = UNSET;
private int mSharedValueCurrent = UNSET;
public int getSharedValueCurrent() {
return mSharedValueCurrent;
}
public void setSharedValueCurrent(int sharedValueCurrent) {
this.mSharedValueCurrent = sharedValueCurrent;
}
/**
* Gets the type of transition to listen to.
*
* @return ONSTATE_TRANSITION_*
*/
public int getStateTransition() {
return mOnStateTransition;
}
/**
* Sets the type of transition to listen to.
*
* @param stateTransition
*/
public void setStateTransition(int stateTransition) {
this.mOnStateTransition = stateTransition;
}
/**
* Gets the SharedValue it will be listening for.
*
* @return
*/
public int getSharedValue() {
return mSharedValueTarget;
}
/**
* sets the SharedValue it will be listening for.
*/
public void setSharedValue(int sharedValue) {
this.mSharedValueTarget = sharedValue;
}
/**
* Gets the ID of the SharedValue it will be listening for.
*
* @return the id of the shared value
*/
public int getSharedValueID() {
return mSharedValueID;
}
/**
* sets the ID of the SharedValue it will be listening for.
*/
public void setSharedValueID(int sharedValueID) {
this.mSharedValueID = sharedValueID;
}
public String toString() {
return "ViewTransition(" + Debug.getName(mContext, mId) + ")";
}
Interpolator getInterpolator(Context context) {
switch (mDefaultInterpolator) {
case SPLINE_STRING:
final Easing easing = Easing.getInterpolator(mDefaultInterpolatorString);
return new Interpolator() {
@Override
public float getInterpolation(float v) {
return (float) easing.get(v);
}
};
case INTERPOLATOR_REFERENCE_ID:
return AnimationUtils.loadInterpolator(context,
mDefaultInterpolatorID);
case EASE_IN_OUT:
return new AccelerateDecelerateInterpolator();
case EASE_IN:
return new AccelerateInterpolator();
case EASE_OUT:
return new DecelerateInterpolator();
case LINEAR:
return null;
case ANTICIPATE:
return new AnticipateInterpolator();
case OVERSHOOT:
return new OvershootInterpolator();
case BOUNCE:
return new BounceInterpolator();
}
return null;
}
ViewTransition(Context context, XmlPullParser parser) {
mContext = context;
String tagName = null;
try {
Key key = null;
for (int eventType = parser.getEventType();
eventType != XmlResourceParser.END_DOCUMENT;
eventType = parser.next()) {
switch (eventType) {
case XmlResourceParser.START_DOCUMENT:
break;
case XmlResourceParser.START_TAG:
tagName = parser.getName();
switch (tagName) {
case VIEW_TRANSITION_TAG:
parseViewTransitionTags(context, parser);
break;
case KEY_FRAME_SET_TAG:
mKeyFrames = new KeyFrames(context, parser);
break;
case CONSTRAINT_OVERRIDE:
mConstraintDelta = ConstraintSet.buildDelta(context, parser);
break;
case CUSTOM_ATTRIBUTE:
case CUSTOM_METHOD:
ConstraintAttribute.parse(context, parser, mConstraintDelta.mCustomConstraints);
break;
default:
Log.e(TAG, Debug.getLoc() + " unknown tag " + tagName);
Log.e(TAG, ".xml:" + parser.getLineNumber());
}
break;
case XmlResourceParser.END_TAG:
if (VIEW_TRANSITION_TAG.equals(parser.getName())) {
return;
}
break;
case XmlResourceParser.TEXT:
break;
}
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void parseViewTransitionTags(Context context, XmlPullParser parser) {
AttributeSet attrs = Xml.asAttributeSet(parser);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewTransition);
final int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = a.getIndex(i);
if (attr == R.styleable.ViewTransition_android_id) {
mId = a.getResourceId(attr, mId);
} else if (attr == R.styleable.ViewTransition_motionTarget) {
if (MotionLayout.IS_IN_EDIT_MODE) {
mTargetId = a.getResourceId(attr, mTargetId);
if (mTargetId == -1) {
mTargetString = a.getString(attr);
}
} else {
if (a.peekValue(attr).type == TypedValue.TYPE_STRING) {
mTargetString = a.getString(attr);
} else {
mTargetId = a.getResourceId(attr, mTargetId);
}
}
} else if (attr == R.styleable.ViewTransition_onStateTransition) {
mOnStateTransition = a.getInt(attr, mOnStateTransition);
} else if (attr == R.styleable.ViewTransition_transitionDisable) {
mDisabled = a.getBoolean(attr, mDisabled);
} else if (attr == R.styleable.ViewTransition_pathMotionArc) {
mPathMotionArc = a.getInt(attr, mPathMotionArc);
} else if (attr == R.styleable.ViewTransition_duration) {
mDuration = a.getInt(attr, mDuration);
} else if (attr == R.styleable.ViewTransition_upDuration) {
mUpDuration = a.getInt(attr, mUpDuration);
} else if (attr == R.styleable.ViewTransition_viewTransitionMode) {
mViewTransitionMode = a.getInt(attr, mViewTransitionMode);
} else if (attr == R.styleable.ViewTransition_motionInterpolator) {
TypedValue type = a.peekValue(attr);
if (type.type == TypedValue.TYPE_REFERENCE) {
mDefaultInterpolatorID = a.getResourceId(attr, -1);
if (mDefaultInterpolatorID != UNSET) {
mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
}
} else if (type.type == TypedValue.TYPE_STRING) {
mDefaultInterpolatorString = a.getString(attr);
if (mDefaultInterpolatorString != null && mDefaultInterpolatorString.indexOf("/") > 0) {
mDefaultInterpolatorID = a.getResourceId(attr, UNSET);
mDefaultInterpolator = INTERPOLATOR_REFERENCE_ID;
} else {
mDefaultInterpolator = SPLINE_STRING;
}
} else {
mDefaultInterpolator = a.getInteger(attr, mDefaultInterpolator);
}
} else if (attr == R.styleable.ViewTransition_setsTag) {
mSetsTag = a.getResourceId(attr, mSetsTag);
} else if (attr == R.styleable.ViewTransition_clearsTag) {
mClearsTag = a.getResourceId(attr, mClearsTag);
} else if (attr == R.styleable.ViewTransition_ifTagSet) {
mIfTagSet = a.getResourceId(attr, mIfTagSet);
} else if (attr == R.styleable.ViewTransition_ifTagNotSet) {
mIfTagNotSet = a.getResourceId(attr, mIfTagNotSet);
} else if (attr == R.styleable.ViewTransition_SharedValueId) {
mSharedValueID = a.getResourceId(attr, mSharedValueID);
} else if (attr == R.styleable.ViewTransition_SharedValue) {
mSharedValueTarget = a.getInteger(attr, mSharedValueTarget);
}
}
a.recycle();
}
void applyIndependentTransition(ViewTransitionController controller, MotionLayout motionLayout, View view) {
MotionController motionController = new MotionController(view);
motionController.setBothStates(view);
mKeyFrames.addAllFrames(motionController);
motionController.setup(motionLayout.getWidth(), motionLayout.getHeight(), mDuration, System.nanoTime());
new Animate(controller, motionController,
mDuration, mUpDuration, mOnStateTransition,
getInterpolator(motionLayout.getContext()), mSetsTag, mClearsTag);
}
static class Animate {
private final int mSetsTag;
private final int mClearsTag;
long mStart;
MotionController mMC;
int mDuration;
int mUpDuration;
KeyCache mCache = new KeyCache();
ViewTransitionController mVtController;
Interpolator mInterpolator;
boolean reverse = false;
float mPosition;
float mDpositionDt;
long mLastRender;
Rect mTempRec = new Rect();
boolean hold_at_100 = false;
Animate(ViewTransitionController controller,
MotionController motionController,
int duration, int upDuration, int mode,
Interpolator interpolator, int setTag, int clearTag) {
mVtController = controller;
mMC = motionController;
mDuration = duration;
mUpDuration = upDuration;
mStart = System.nanoTime();
mLastRender = mStart;
mVtController.addAnimation(this);
mInterpolator = interpolator;
mSetsTag = setTag;
mClearsTag = clearTag;
if (mode == ONSTATE_ACTION_DOWN_UP) {
hold_at_100 = true;
}
mDpositionDt = (duration == 0) ? Float.MAX_VALUE : 1f / duration;
mutate();
}
void reverse(boolean dir) {
reverse = dir;
if (reverse && mUpDuration != UNSET) {
mDpositionDt = (mUpDuration == 0) ? Float.MAX_VALUE : 1f / mUpDuration;
}
mVtController.invalidate();
mLastRender = System.nanoTime();
}
void mutate() {
if (reverse) {
mutateReverse();
} else {
mutateForward();
}
}
void mutateReverse() {
long current = System.nanoTime();
long elapse = current - mLastRender;
mLastRender = current;
mPosition -= ((float) (elapse * 1E-6)) * mDpositionDt;
if (mPosition < 0.0f) {
mPosition = 0.0f;
}
float ipos = (mInterpolator == null) ? mPosition : mInterpolator.getInterpolation(mPosition);
boolean repaint = mMC.interpolate(mMC.mView, ipos, current, mCache);
if (mPosition <= 0) {
if (mSetsTag != UNSET) {
mMC.getView().setTag(mSetsTag, System.nanoTime());
}
if (mClearsTag != UNSET) {
mMC.getView().setTag(mClearsTag, null);
}
mVtController.removeAnimation(this);
}
if (mPosition > 0f || repaint) {
mVtController.invalidate();
}
}
void mutateForward() {
long current = System.nanoTime();
long elapse = current - mLastRender;
mLastRender = current;
mPosition += ((float) (elapse * 1E-6)) * mDpositionDt;
if (mPosition >= 1.0f) {
mPosition = 1.0f;
}
float ipos = (mInterpolator == null) ? mPosition : mInterpolator.getInterpolation(mPosition);
boolean repaint = mMC.interpolate(mMC.mView, ipos, current, mCache);
if (mPosition >= 1) {
if (mSetsTag != UNSET) {
mMC.getView().setTag(mSetsTag, System.nanoTime());
}
if (mClearsTag != UNSET) {
mMC.getView().setTag(mClearsTag, null);
}
if (!hold_at_100) {
mVtController.removeAnimation(this);
}
}
if (mPosition < 1f || repaint) {
mVtController.invalidate();
}
}
public void reactTo(int action, float x, float y) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!reverse) {
reverse(true);
}
return;
case MotionEvent.ACTION_MOVE:
View view = mMC.getView();
view.getHitRect(mTempRec);
if (!mTempRec.contains((int) x, (int) y)) {
if (!reverse)
reverse(true);
}
}
}
}
void applyTransition(ViewTransitionController controller,
MotionLayout layout,
int fromId,
ConstraintSet current,
View... views) {
if (mDisabled) {
return;
}
if (mViewTransitionMode == VIEWTRANSITIONMODE_NOSTATE) {
applyIndependentTransition(controller, layout, views[0]);
return;
}
if (mViewTransitionMode == VIEWTRANSITIONMODE_ALLSTATES) {
int[] ids = layout.getConstraintSetIds();
for (int i = 0; i < ids.length; i++) {
int id = ids[i];
if (id == fromId) {
continue;
}
ConstraintSet cSet = layout.getConstraintSet(id);
for (View view : views) {
ConstraintSet.Constraint constraint = cSet.getConstraint(view.getId());
if (mConstraintDelta != null) {
mConstraintDelta.applyDelta(constraint);
constraint.mCustomConstraints.putAll(mConstraintDelta.mCustomConstraints);
}
}
}
}
ConstraintSet transformedState = new ConstraintSet();
transformedState.clone(current);
for (View view : views) {
ConstraintSet.Constraint constraint = transformedState.getConstraint(view.getId());
if (mConstraintDelta != null) {
mConstraintDelta.applyDelta(constraint);
constraint.mCustomConstraints.putAll(mConstraintDelta.mCustomConstraints);
}
}
layout.updateState(fromId, transformedState);
layout.updateState(R.id.view_transition, current);
layout.setState(R.id.view_transition, -1, -1);
MotionScene.Transition tmpTransition = new MotionScene.Transition(-1, layout.mScene, R.id.view_transition, fromId);
for (View view : views) {
updateTransition(tmpTransition, view);
}
layout.setTransition(tmpTransition);
layout.transitionToEnd(() -> {
if (mSetsTag != UNSET) {
for (View view : views) {
view.setTag(mSetsTag, System.nanoTime());
}
}
if (mClearsTag != UNSET) {
for (View view : views) {
view.setTag(mClearsTag, null);
}
}
});
}
private void updateTransition(MotionScene.Transition transition, View view) {
if (mDuration != -1) {
transition.setDuration(mDuration);
}
transition.setPathMotionArc(mPathMotionArc);
transition.setInterpolatorInfo(mDefaultInterpolator, mDefaultInterpolatorString, mDefaultInterpolatorID);
int id = view.getId();
if (mKeyFrames != null) {
ArrayList<Key> keys = mKeyFrames.getKeyFramesForView(KeyFrames.UNSET);
KeyFrames keyFrames = new KeyFrames();
for (Key key : keys) {
keyFrames.addKey(key.clone().setViewId(id));
}
transition.addKeyFrame(keyFrames);
}
}
int getId() {
return mId;
}
void setId(int id) {
this.mId = id;
}
boolean matchesView(View view) {
if (view == null) {
return false;
}
if (mTargetId == -1 && mTargetString == null) {
return false;
}
if (!checkTags(view)) {
return false;
}
if (view.getId() == mTargetId) {
return true;
}
if (mTargetString == null) {
return false;
}
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof ConstraintLayout.LayoutParams) {
String tag = ((ConstraintLayout.LayoutParams) (view.getLayoutParams())).constraintTag;
if (tag != null && tag.matches(mTargetString)) {
return true;
}
}
return false;
}
boolean supports(int action) {
if (mOnStateTransition == ONSTATE_ACTION_DOWN) {
return action == MotionEvent.ACTION_DOWN;
}
if (mOnStateTransition == ONSTATE_ACTION_UP) {
return action == MotionEvent.ACTION_UP;
}
if (mOnStateTransition == ONSTATE_ACTION_DOWN_UP) {
return action == MotionEvent.ACTION_DOWN;
}
return false;
}
boolean isEnabled() {
return !mDisabled;
}
void setEnabled(boolean enable) {
this.mDisabled = !enable;
}
boolean checkTags(View view) {
boolean set = (mIfTagSet == UNSET) ? true : (null != view.getTag(mIfTagSet));
boolean notSet = (mIfTagNotSet == UNSET) ? true : null == view.getTag(mIfTagNotSet);
return set && notSet;
}
}