diff options
| author | George Mount <mount@google.com> | 2014-06-19 07:51:08 -0700 |
|---|---|---|
| committer | George Mount <mount@google.com> | 2014-06-30 19:25:04 +0000 |
| commit | ecd857be3946283ebb4306e2c03ae70f5c5bb152 (patch) | |
| tree | a755ce8d5752c791015a68e466e1c8352919bfce /core/java/android | |
| parent | 32292eb87bd48cee13a2f65204ad814fcfce305e (diff) | |
| download | frameworks_base-ecd857be3946283ebb4306e2c03ae70f5c5bb152.zip frameworks_base-ecd857be3946283ebb4306e2c03ae70f5c5bb152.tar.gz frameworks_base-ecd857be3946283ebb4306e2c03ae70f5c5bb152.tar.bz2 | |
Add curved motion to Transitions.
Bug 15197527
Added two public PathMotions: PatternMotion and ArcMotion.
ArcMotion is the algorithm provided by UX. PatternMotion
provides a mechanism for using a Path as a pattern.
Change-Id: Ie57fd5f4e62269acc1164eced39853a12c52bd77
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/transition/ArcMotion.java | 272 | ||||
| -rw-r--r-- | core/java/android/transition/AutoTransition.java | 12 | ||||
| -rw-r--r-- | core/java/android/transition/ChangeBounds.java | 112 | ||||
| -rw-r--r-- | core/java/android/transition/ChangeClipBounds.java | 8 | ||||
| -rw-r--r-- | core/java/android/transition/ChangeImageTransform.java | 14 | ||||
| -rw-r--r-- | core/java/android/transition/ChangeTransform.java | 8 | ||||
| -rw-r--r-- | core/java/android/transition/Explode.java | 6 | ||||
| -rw-r--r-- | core/java/android/transition/Fade.java | 12 | ||||
| -rw-r--r-- | core/java/android/transition/MoveImage.java | 12 | ||||
| -rw-r--r-- | core/java/android/transition/PathMotion.java | 59 | ||||
| -rw-r--r-- | core/java/android/transition/PatternMotion.java | 149 | ||||
| -rw-r--r-- | core/java/android/transition/Recolor.java | 8 | ||||
| -rw-r--r-- | core/java/android/transition/Slide.java | 27 | ||||
| -rw-r--r-- | core/java/android/transition/Transition.java | 120 | ||||
| -rw-r--r-- | core/java/android/transition/TransitionInflater.java | 262 | ||||
| -rw-r--r-- | core/java/android/transition/TransitionSet.java | 23 | ||||
| -rw-r--r-- | core/java/android/transition/Visibility.java | 30 |
17 files changed, 915 insertions, 219 deletions
diff --git a/core/java/android/transition/ArcMotion.java b/core/java/android/transition/ArcMotion.java new file mode 100644 index 0000000..a27063d --- /dev/null +++ b/core/java/android/transition/ArcMotion.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2014 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 android.transition; + +import com.android.internal.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Path; +import android.util.AttributeSet; +import android.util.FloatMath; + +/** + * A PathMotion that generates a curved path along an arc on an imaginary circle containing + * the two points. If the horizontal distance between the points is less than the vertical + * distance, then the circle's center point will be horizontally aligned with the end point. If the + * vertical distance is less than the horizontal distance then the circle's center point + * will be vertically aligned with the end point. + * <p> + * When the two points are near horizontal or vertical, the curve of the motion will be + * small as the center of the circle will be far from both points. To force curvature of + * the path, {@link #setMinimumHorizontalAngle(float)} and + * {@link #setMinimumVerticalAngle(float)} may be used to set the minimum angle of the + * arc between two points. + * </p> + * <p>This may be used in XML as an element inside a transition.</p> + * <pre> + * {@code + * <changeBounds> + * <arcMotion android:minimumHorizontalAngle="15" + * android:minimumVerticalAngle="0" android:maximumAngle="90"/> + * </changeBounds> + * } + * </pre> + */ +public class ArcMotion extends PathMotion { + + private static final float DEFAULT_MIN_ANGLE_DEGREES = 0; + private static final float DEFAULT_MAX_ANGLE_DEGREES = 70; + private static final float DEFAULT_MAX_TANGENT = (float) + Math.tan(Math.toRadians(DEFAULT_MAX_ANGLE_DEGREES/2)); + + private float mMinimumHorizontalAngle = 0; + private float mMinimumVerticalAngle = 0; + private float mMaximumAngle = DEFAULT_MAX_ANGLE_DEGREES; + private float mMinimumHorizontalTangent = 0; + private float mMinimumVerticalTangent = 0; + private float mMaximumTangent = DEFAULT_MAX_TANGENT; + + public ArcMotion() {} + + public ArcMotion(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ArcMotion); + float minimumVerticalAngle = a.getFloat(R.styleable.ArcMotion_minimumVerticalAngle, + DEFAULT_MIN_ANGLE_DEGREES); + setMinimumVerticalAngle(minimumVerticalAngle); + float minimumHorizontalAngle = a.getFloat(R.styleable.ArcMotion_minimumHorizontalAngle, + DEFAULT_MIN_ANGLE_DEGREES); + setMinimumHorizontalAngle(minimumHorizontalAngle); + float maximumAngle = a.getFloat(R.styleable.ArcMotion_maximumAngle, + DEFAULT_MAX_ANGLE_DEGREES); + setMaximumAngle(maximumAngle); + a.recycle(); + } + + /** + * Sets the minimum arc along the circle between two points aligned near horizontally. + * When start and end points are close to horizontal, the calculated center point of the + * circle will be far from both points, giving a near straight path between the points. + * By setting a minimum angle, this forces the center point to be closer and give an + * exaggerated curve to the path. + * <p>The default value is 0.</p> + * + * @param angleInDegrees The minimum angle of the arc on a circle describing the Path + * between two nearly horizontally-separated points. + * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle + */ + public void setMinimumHorizontalAngle(float angleInDegrees) { + mMinimumHorizontalAngle = angleInDegrees; + mMinimumHorizontalTangent = toTangent(angleInDegrees); + } + + /** + * Returns the minimum arc along the circle between two points aligned near horizontally. + * When start and end points are close to horizontal, the calculated center point of the + * circle will be far from both points, giving a near straight path between the points. + * By setting a minimum angle, this forces the center point to be closer and give an + * exaggerated curve to the path. + * <p>The default value is 0.</p> + * + * @return The minimum arc along the circle between two points aligned near horizontally. + * @attr ref android.R.styleable#ArcMotion_minimumHorizontalAngle + */ + public float getMinimumHorizontalAngle() { + return mMinimumHorizontalAngle; + } + + /** + * Sets the minimum arc along the circle between two points aligned near vertically. + * When start and end points are close to vertical, the calculated center point of the + * circle will be far from both points, giving a near straight path between the points. + * By setting a minimum angle, this forces the center point to be closer and give an + * exaggerated curve to the path. + * <p>The default value is 0.</p> + * + * @param angleInDegrees The minimum angle of the arc on a circle describing the Path + * between two nearly vertically-separated points. + * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle + */ + public void setMinimumVerticalAngle(float angleInDegrees) { + mMinimumVerticalAngle = angleInDegrees; + mMinimumVerticalTangent = toTangent(angleInDegrees); + } + + /** + * Returns the minimum arc along the circle between two points aligned near vertically. + * When start and end points are close to vertical, the calculated center point of the + * circle will be far from both points, giving a near straight path between the points. + * By setting a minimum angle, this forces the center point to be closer and give an + * exaggerated curve to the path. + * <p>The default value is 0.</p> + * + * @return The minimum angle of the arc on a circle describing the Path + * between two nearly vertically-separated points. + * @attr ref android.R.styleable#ArcMotion_minimumVerticalAngle + */ + public float getMinimumVerticalAngle() { + return mMinimumVerticalAngle; + } + + /** + * Sets the maximum arc along the circle between two points. When start and end points + * have close to equal x and y differences, the curve between them is large. This forces + * the curved path to have an arc of at most the given angle. + * <p>The default value is 70 degrees.</p> + * + * @param angleInDegrees The maximum angle of the arc on a circle describing the Path + * between the start and end points. + * @attr ref android.R.styleable#ArcMotion_maximumAngle + */ + public void setMaximumAngle(float angleInDegrees) { + mMaximumAngle = angleInDegrees; + mMaximumTangent = toTangent(angleInDegrees); + } + + /** + * Returns the maximum arc along the circle between two points. When start and end points + * have close to equal x and y differences, the curve between them is large. This forces + * the curved path to have an arc of at most the given angle. + * <p>The default value is 70 degrees.</p> + * + * @return The maximum angle of the arc on a circle describing the Path + * between the start and end points. + * @attr ref android.R.styleable#ArcMotion_maximumAngle + */ + public float getMaximumAngle() { + return mMaximumAngle; + } + + private static float toTangent(float arcInDegrees) { + if (arcInDegrees < 0 || arcInDegrees > 90) { + throw new IllegalArgumentException("Arc must be between 0 and 90 degrees"); + } + return (float) Math.tan(Math.toRadians(arcInDegrees / 2)); + } + + @Override + public Path getPath(float startX, float startY, float endX, float endY) { + // Here's a little ascii art to show how this is calculated: + // c---------- b + // \ / | + // \ d | + // \ / e + // a----f + // This diagram assumes that the horizontal distance is less than the vertical + // distance between The start point (a) and end point (b). + // d is the midpoint between a and b. c is the center point of the circle with + // This path is formed by assuming that start and end points are in + // an arc on a circle. The end point is centered in the circle vertically + // and start is a point on the circle. + + // Triangles bfa and bde form similar right triangles. The control points + // for the cubic Bezier arc path are the midpoints between a and e and e and b. + + Path path = new Path(); + path.moveTo(startX, startY); + + float ex; + float ey; + if (startY == endY) { + ex = (startX + endX) / 2; + ey = startY + mMinimumHorizontalTangent * Math.abs(endX - startX) / 2; + } else if (startX == endX) { + ex = startX + mMinimumVerticalTangent * Math.abs(endY - startY) / 2; + ey = (startY + endY) / 2; + } else { + float deltaX = endX - startX; + float deltaY = startY - endY; // Y is inverted compared to diagram above. + // hypotenuse squared. + float h2 = deltaX * deltaX + deltaY * deltaY; + + // Midpoint between start and end + float dx = (startX + endX) / 2; + float dy = (startY + endY) / 2; + + // Distance squared between end point and mid point is (1/2 hypotenuse)^2 + float midDist2 = h2 * 0.25f; + + float minimumArcDist2 = 0; + + if (Math.abs(deltaX) < Math.abs(deltaY)) { + // Similar triangles bfa and bde mean that (ab/fb = eb/bd) + // Therefore, eb = ab * bd / fb + // ab = hypotenuse + // bd = hypotenuse/2 + // fb = deltaY + float eDistY = h2 / (2 * deltaY); + ey = endY + eDistY; + ex = endX; + + minimumArcDist2 = midDist2 * mMinimumVerticalTangent + * mMinimumVerticalTangent; + } else { + // Same as above, but flip X & Y + float eDistX = h2 / (2 * deltaX); + ex = endX + eDistX; + ey = endY; + + minimumArcDist2 = midDist2 * mMinimumHorizontalTangent + * mMinimumHorizontalTangent; + } + float arcDistX = dx - ex; + float arcDistY = dy - ey; + float arcDist2 = arcDistX * arcDistX + arcDistY * arcDistY; + + float maximumArcDist2 = midDist2 * mMaximumTangent * mMaximumTangent; + + float newArcDistance2 = 0; + if (arcDist2 < minimumArcDist2) { + newArcDistance2 = minimumArcDist2; + } else if (arcDist2 > maximumArcDist2) { + newArcDistance2 = maximumArcDist2; + } + if (newArcDistance2 != 0) { + float ratio2 = newArcDistance2 / arcDist2; + float ratio = FloatMath.sqrt(ratio2); + ex = dx + (ratio * (ex - dx)); + ey = dy + (ratio * (ey - dy)); + } + } + float controlX1 = (startX + ex) / 2; + float controlY1 = (startY + ey) / 2; + float controlX2 = (ex + endX) / 2; + float controlY2 = (ey + endY) / 2; + path.cubicTo(controlX1, controlY1, controlX2, controlY2, endX, endY); + return path; + } +} diff --git a/core/java/android/transition/AutoTransition.java b/core/java/android/transition/AutoTransition.java index 6e46021..2104737 100644 --- a/core/java/android/transition/AutoTransition.java +++ b/core/java/android/transition/AutoTransition.java @@ -16,6 +16,9 @@ package android.transition; +import android.content.Context; +import android.util.AttributeSet; + /** * Utility class for creating a default transition that automatically fades, * moves, and resizes views during a scene change. @@ -33,6 +36,15 @@ public class AutoTransition extends TransitionSet { * */ public AutoTransition() { + init(); + } + + public AutoTransition(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { setOrdering(ORDERING_SEQUENTIAL); addTransition(new Fade(Fade.OUT)). addTransition(new ChangeBounds()). diff --git a/core/java/android/transition/ChangeBounds.java b/core/java/android/transition/ChangeBounds.java index 8053bff..2591e24 100644 --- a/core/java/android/transition/ChangeBounds.java +++ b/core/java/android/transition/ChangeBounds.java @@ -16,15 +16,24 @@ package android.transition; +import android.animation.TypeConverter; +import android.content.Context; +import android.graphics.PointF; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.RectEvaluator; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Property; import android.view.View; import android.view.ViewGroup; @@ -51,6 +60,24 @@ public class ChangeBounds extends Transition { PROPNAME_WINDOW_Y }; + private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY = + new Property<Drawable, PointF>(PointF.class, "boundsOrigin") { + private Rect mBounds = new Rect(); + + @Override + public void set(Drawable object, PointF value) { + object.copyBounds(mBounds); + mBounds.offsetTo(Math.round(value.x), Math.round(value.y)); + object.setBounds(mBounds); + } + + @Override + public PointF get(Drawable object) { + object.copyBounds(mBounds); + return new PointF(mBounds.left, mBounds.top); + } + }; + int[] tempLocation = new int[2]; boolean mResizeClip = false; boolean mReparent = false; @@ -58,6 +85,12 @@ public class ChangeBounds extends Transition { private static RectEvaluator sRectEvaluator = new RectEvaluator(); + public ChangeBounds() {} + + public ChangeBounds(Context context, AttributeSet attrs) { + super(context, attrs); + } + @Override public String[] getTransitionProperties() { return sTransitionProperties; @@ -138,34 +171,29 @@ public class ChangeBounds extends Transition { int endHeight = endBottom - endTop; int numChanges = 0; if (startWidth != 0 && startHeight != 0 && endWidth != 0 && endHeight != 0) { - if (startLeft != endLeft) ++numChanges; - if (startTop != endTop) ++numChanges; - if (startRight != endRight) ++numChanges; - if (startBottom != endBottom) ++numChanges; + if (startLeft != endLeft || startTop != endTop) ++numChanges; + if (startRight != endRight || startBottom != endBottom) ++numChanges; } if (numChanges > 0) { if (!mResizeClip) { - PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges]; - int pvhIndex = 0; if (startLeft != endLeft) view.setLeft(startLeft); if (startTop != endTop) view.setTop(startTop); if (startRight != endRight) view.setRight(startRight); if (startBottom != endBottom) view.setBottom(startBottom); - if (startLeft != endLeft) { - pvh[pvhIndex++] = PropertyValuesHolder.ofInt("left", startLeft, endLeft); - } - if (startTop != endTop) { - pvh[pvhIndex++] = PropertyValuesHolder.ofInt("top", startTop, endTop); + ObjectAnimator topLeftAnimator = null; + if (startLeft != endLeft || startTop != endTop) { + Path topLeftPath = getPathMotion().getPath(startLeft, startTop, + endLeft, endTop); + topLeftAnimator = ObjectAnimator.ofInt(view, "left", "top", topLeftPath); } - if (startRight != endRight) { - pvh[pvhIndex++] = PropertyValuesHolder.ofInt("right", - startRight, endRight); + ObjectAnimator bottomRightAnimator = null; + if (startRight != endRight || startBottom != endBottom) { + Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, + endRight, endBottom); + bottomRightAnimator = ObjectAnimator.ofInt(view, "right", "bottom", + bottomRightPath); } - if (startBottom != endBottom) { - pvh[pvhIndex++] = PropertyValuesHolder.ofInt("bottom", - startBottom, endBottom); - } - ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh); + Animator anim = mergeAnimators(topLeftAnimator, bottomRightAnimator); if (view.getParent() instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) view.getParent(); parent.suppressLayout(true); @@ -215,23 +243,20 @@ public class ChangeBounds extends Transition { if (transXDelta != 0) numChanges++; if (transYDelta != 0) numChanges++; if (widthDelta != 0 || heightDelta != 0) numChanges++; - PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges]; - int pvhIndex = 0; - if (transXDelta != 0) { - pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationX", - view.getTranslationX(), 0); - } - if (transYDelta != 0) { - pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationY", - view.getTranslationY(), 0); + ObjectAnimator translationAnimator = null; + if (transXDelta != 0 || transYDelta != 0) { + Path topLeftPath = getPathMotion().getPath(0, 0, transXDelta, transYDelta); + translationAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, + View.TRANSLATION_Y, topLeftPath); } + ObjectAnimator clipAnimator = null; if (widthDelta != 0 || heightDelta != 0) { Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight); Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight); - pvh[pvhIndex++] = PropertyValuesHolder.ofObject("clipBounds", - sRectEvaluator, tempStartBounds, tempEndBounds); + clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator, + tempStartBounds, tempEndBounds); } - ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh); + Animator anim = mergeAnimators(translationAnimator, clipAnimator); if (view.getParent() instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) view.getParent(); parent.suppressLayout(true); @@ -287,14 +312,11 @@ public class ChangeBounds extends Transition { final BitmapDrawable drawable = new BitmapDrawable(bitmap); view.setVisibility(View.INVISIBLE); sceneRoot.getOverlay().add(drawable); - Rect startBounds1 = new Rect(startX - tempLocation[0], startY - tempLocation[1], - startX - tempLocation[0] + view.getWidth(), - startY - tempLocation[1] + view.getHeight()); - Rect endBounds1 = new Rect(endX - tempLocation[0], endY - tempLocation[1], - endX - tempLocation[0] + view.getWidth(), - endY - tempLocation[1] + view.getHeight()); - ObjectAnimator anim = ObjectAnimator.ofObject(drawable, "bounds", - sRectEvaluator, startBounds1, endBounds1); + Path topLeftPath = getPathMotion().getPath(startX - tempLocation[0], + startY - tempLocation[1], endX - tempLocation[0], endY - tempLocation[1]); + PropertyValuesHolder origin = PropertyValuesHolder.ofObject( + DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath); + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -307,4 +329,16 @@ public class ChangeBounds extends Transition { } return null; } + + private static Animator mergeAnimators(Animator animator1, Animator animator2) { + if (animator1 == null) { + return animator2; + } else if (animator2 == null) { + return animator1; + } else { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(animator1, animator2); + return animatorSet; + } + } } diff --git a/core/java/android/transition/ChangeClipBounds.java b/core/java/android/transition/ChangeClipBounds.java index a61b29d..8d0943c 100644 --- a/core/java/android/transition/ChangeClipBounds.java +++ b/core/java/android/transition/ChangeClipBounds.java @@ -18,7 +18,9 @@ package android.transition; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.RectEvaluator; +import android.content.Context; import android.graphics.Rect; +import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -37,6 +39,12 @@ public class ChangeClipBounds extends Transition { PROPNAME_CLIP, }; + public ChangeClipBounds() {} + + public ChangeClipBounds(Context context, AttributeSet attrs) { + super(context, attrs); + } + @Override public String[] getTransitionProperties() { return sTransitionProperties; diff --git a/core/java/android/transition/ChangeImageTransform.java b/core/java/android/transition/ChangeImageTransform.java index b003690..12437d7 100644 --- a/core/java/android/transition/ChangeImageTransform.java +++ b/core/java/android/transition/ChangeImageTransform.java @@ -19,9 +19,11 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TypeEvaluator; +import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.util.AttributeSet; import android.util.Property; import android.view.View; import android.view.ViewGroup; @@ -37,10 +39,10 @@ import java.util.Map; */ public class ChangeImageTransform extends Transition { - private static final String TAG = "ChangeScaleType"; + private static final String TAG = "ChangeImageTransform"; - private static final String PROPNAME_MATRIX = "android:changeScaleType:matrix"; - private static final String PROPNAME_BOUNDS = "android:changeScaleType:bounds"; + private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; + private static final String PROPNAME_BOUNDS = "android:changeImageTransform:bounds"; private static final String[] sTransitionProperties = { PROPNAME_MATRIX, @@ -67,6 +69,12 @@ public class ChangeImageTransform extends Transition { } }; + public ChangeImageTransform() {} + + public ChangeImageTransform(Context context, AttributeSet attrs) { + super(context, attrs); + } + private void captureValues(TransitionValues transitionValues) { View view = transitionValues.view; if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) { diff --git a/core/java/android/transition/ChangeTransform.java b/core/java/android/transition/ChangeTransform.java index 8c03e82..e9be3b9 100644 --- a/core/java/android/transition/ChangeTransform.java +++ b/core/java/android/transition/ChangeTransform.java @@ -18,6 +18,8 @@ package android.transition; import android.animation.Animator; import android.animation.FloatArrayEvaluator; import android.animation.ObjectAnimator; +import android.content.Context; +import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Property; import android.view.View; @@ -76,6 +78,12 @@ public class ChangeTransform extends Transition { } }; + public ChangeTransform() {} + + public ChangeTransform(Context context, AttributeSet attrs) { + super(context, attrs); + } + @Override public String[] getTransitionProperties() { return sTransitionProperties; diff --git a/core/java/android/transition/Explode.java b/core/java/android/transition/Explode.java index feb8efd..995702e 100644 --- a/core/java/android/transition/Explode.java +++ b/core/java/android/transition/Explode.java @@ -19,7 +19,9 @@ import com.android.internal.R; import android.animation.Animator; import android.animation.TimeInterpolator; +import android.content.Context; import android.graphics.Rect; +import android.util.AttributeSet; import android.util.FloatMath; import android.view.View; import android.view.ViewGroup; @@ -48,6 +50,10 @@ public class Explode extends Visibility { setPropagation(new CircularPropagation()); } + public Explode(Context context, AttributeSet attrs) { + super(context, attrs); + } + private void captureValues(TransitionValues transitionValues) { View view = transitionValues.view; view.getLocationOnScreen(mTempLoc); diff --git a/core/java/android/transition/Fade.java b/core/java/android/transition/Fade.java index 71559da..420c248 100644 --- a/core/java/android/transition/Fade.java +++ b/core/java/android/transition/Fade.java @@ -16,9 +16,14 @@ package android.transition; +import com.android.internal.R; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -93,6 +98,13 @@ public class Fade extends Visibility { setMode(fadingMode); } + public Fade(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Fade); + int fadingMode = a.getInt(R.styleable.Fade_fadingMode, getMode()); + setMode(fadingMode); + } + /** * Utility method to handle creating and running the Animator. */ diff --git a/core/java/android/transition/MoveImage.java b/core/java/android/transition/MoveImage.java index 48b96ec..2f7945d 100644 --- a/core/java/android/transition/MoveImage.java +++ b/core/java/android/transition/MoveImage.java @@ -15,6 +15,9 @@ */ package android.transition; +import android.content.Context; +import android.util.AttributeSet; + /** * TO BE REMOVED. * Use ChangeImageTransform + ChangeBounds instead. @@ -23,6 +26,15 @@ package android.transition; public class MoveImage extends TransitionSet { public MoveImage() { + init(); + } + + public MoveImage(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { addTransition(new ChangeBounds()); addTransition(new ChangeClipBounds()); addTransition(new ChangeTransform()); diff --git a/core/java/android/transition/PathMotion.java b/core/java/android/transition/PathMotion.java new file mode 100644 index 0000000..651223d --- /dev/null +++ b/core/java/android/transition/PathMotion.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 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 android.transition; + +import android.content.Context; +import android.graphics.Path; +import android.util.AttributeSet; + +/** + * This base class can be extended to provide motion along a Path to Transitions. + * + * <p> + * Transitions such as {@link android.transition.ChangeBounds} move Views, typically + * in a straight path between the start and end positions. Applications that desire to + * have these motions move in a curve can change how Views interpolate in two dimensions + * by extending PathMotion and implementing {@link #getPath(float, float, float, float)}. + * </p> + * <p>This may be used in XML as an element inside a transition.</p> + * <pre> + * {@code + * <changeBounds> + * <pathMotion class="my.app.transition.MyPathMotion"/> + * </changeBounds> + * } + * </pre> + */ +public abstract class PathMotion { + + public PathMotion() {} + + public PathMotion(Context context, AttributeSet attrs) {} + + /** + * Provide a Path to interpolate between two points <code>(startX, startY)</code> and + * <code>(endX, endY)</code>. This allows controlled curved motion along two dimensions. + * + * @param startX The x coordinate of the starting point. + * @param startY The y coordinate of the starting point. + * @param endX The x coordinate of the ending point. + * @param endY The y coordinate of the ending point. + * @return A Path along which the points should be interpolated. The returned Path + * must start at point <code>(startX, startY)</code>, typically using + * {@link android.graphics.Path#moveTo(float, float)} and end at <code>(endX, endY)</code>. + */ + public abstract Path getPath(float startX, float startY, float endX, float endY); +} diff --git a/core/java/android/transition/PatternMotion.java b/core/java/android/transition/PatternMotion.java new file mode 100644 index 0000000..e4045b4 --- /dev/null +++ b/core/java/android/transition/PatternMotion.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2014 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 android.transition; + +import com.android.internal.R; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.util.AttributeSet; +import android.util.FloatMath; +import android.util.PathParser; + +/** + * A PathMotion that takes a Path pattern and applies it to the separation between two points. + * The starting point of the Path will be moved to the origin and the end point will be scaled + * and rotated so that it matches with the target end point. + * <p>This may be used in XML as an element inside a transition.</p> + * <pre> + * {@code + * <changeBounds> + * <patternMotion android:pathData="M0 0 L0 100 L100 100"/> + * </changeBounds> + * } + * </pre> + */ +public class PatternMotion extends PathMotion { + + private Path mOriginalPattern; + + private final Path mPattern = new Path(); + + private final Matrix mTempMatrix = new Matrix(); + + /** + * Constructs a PatternMotion with a straight-line pattern. + */ + public PatternMotion() { + mPattern.lineTo(1, 0); + mOriginalPattern = mPattern; + } + + public PatternMotion(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PatternMotion); + try { + String pathData = a.getString(R.styleable.PatternMotion_pathData); + if (pathData == null) { + throw new RuntimeException("pathData must be supplied for patternMotion"); + } + Path pattern = PathParser.createPathFromPathData(pathData); + setPattern(pattern); + } finally { + a.recycle(); + } + + } + + /** + * Creates a PatternMotion with the Path defining a pattern of motion between two coordinates. + * The pattern will be translated, rotated, and scaled to fit between the start and end points. + * The pattern must not be empty and must have the end point differ from the start point. + * + * @param pattern A Path to be used as a pattern for two-dimensional motion. + */ + public PatternMotion(Path pattern) { + setPattern(pattern); + } + + /** + * Returns the Path defining a pattern of motion between two coordinates. + * The pattern will be translated, rotated, and scaled to fit between the start and end points. + * The pattern must not be empty and must have the end point differ from the start point. + * + * @return the Path defining a pattern of motion between two coordinates. + * @attr ref android.R.styleable#PatternMotion_pathData + */ + public Path getPattern() { + return mOriginalPattern; + } + + /** + * Sets the Path defining a pattern of motion between two coordinates. + * The pattern will be translated, rotated, and scaled to fit between the start and end points. + * The pattern must not be empty and must have the end point differ from the start point. + * + * @param pattern A Path to be used as a pattern for two-dimensional motion. + * @attr ref android.R.styleable#PatternMotion_pathData + */ + public void setPattern(Path pattern) { + PathMeasure pathMeasure = new PathMeasure(pattern, false); + float length = pathMeasure.getLength(); + float[] pos = new float[2]; + pathMeasure.getPosTan(length, pos, null); + float endX = pos[0]; + float endY = pos[1]; + pathMeasure.getPosTan(0, pos, null); + float startX = pos[0]; + float startY = pos[1]; + + if (startX == endX && startY == endY) { + throw new IllegalArgumentException("pattern must not end at the starting point"); + } + + mTempMatrix.setTranslate(-startX, -startY); + float dx = endX - startX; + float dy = endY - startY; + float distance = distance(dx, dy); + float scale = 1 / distance; + mTempMatrix.postScale(scale, scale); + double angle = Math.atan2(dy, dx); + mTempMatrix.postRotate((float) Math.toDegrees(-angle)); + pattern.transform(mTempMatrix, mPattern); + mOriginalPattern = pattern; + } + + @Override + public Path getPath(float startX, float startY, float endX, float endY) { + float dx = endX - startX; + float dy = endY - startY; + float length = distance(dx, dy); + double angle = Math.atan2(dy, dx); + + mTempMatrix.setScale(length, length); + mTempMatrix.postRotate((float) Math.toDegrees(angle)); + mTempMatrix.postTranslate(startX, startY); + Path path = new Path(); + mPattern.transform(mTempMatrix, path); + return path; + } + + private static float distance(float x, float y) { + return FloatMath.sqrt((x * x) + (y * y)); + } +} diff --git a/core/java/android/transition/Recolor.java b/core/java/android/transition/Recolor.java index 1638f67..1a6864a 100644 --- a/core/java/android/transition/Recolor.java +++ b/core/java/android/transition/Recolor.java @@ -18,8 +18,10 @@ package android.transition; import android.animation.Animator; import android.animation.ObjectAnimator; +import android.content.Context; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; @@ -40,6 +42,12 @@ public class Recolor extends Transition { private static final String PROPNAME_BACKGROUND = "android:recolor:background"; private static final String PROPNAME_TEXT_COLOR = "android:recolor:textColor"; + public Recolor() {} + + public Recolor(Context context, AttributeSet attrs) { + super(context, attrs); + } + private void captureValues(TransitionValues transitionValues) { transitionValues.values.put(PROPNAME_BACKGROUND, transitionValues.view.getBackground()); if (transitionValues.view instanceof TextView) { diff --git a/core/java/android/transition/Slide.java b/core/java/android/transition/Slide.java index 0d2e487..ae2e4aa 100644 --- a/core/java/android/transition/Slide.java +++ b/core/java/android/transition/Slide.java @@ -17,11 +17,15 @@ package android.transition; import android.animation.Animator; import android.animation.TimeInterpolator; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; +import com.android.internal.R; /** * This transition tracks changes to the visibility of target views in the @@ -38,6 +42,7 @@ public class Slide extends Visibility { private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); private static final String PROPNAME_SCREEN_POSITION = "android:slide:screenPosition"; private CalculateSlide mSlideCalculator = sCalculateBottom; + private int mSlideEdge = Gravity.BOTTOM; private interface CalculateSlide { @@ -107,6 +112,14 @@ public class Slide extends Visibility { setSlideEdge(slideEdge); } + public Slide(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slide); + int edge = a.getInt(R.styleable.Slide_slideEdge, Gravity.BOTTOM); + a.recycle(); + setSlideEdge(edge); + } + private void captureValues(TransitionValues transitionValues) { View view = transitionValues.view; int[] position = new int[2]; @@ -132,6 +145,7 @@ public class Slide extends Visibility { * @param slideEdge The edge of the scene to use for Views appearing and disappearing. One of * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP}, * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM}. + * @attr ref android.R.styleable#Slide_slideEdge */ public void setSlideEdge(int slideEdge) { switch (slideEdge) { @@ -150,11 +164,24 @@ public class Slide extends Visibility { default: throw new IllegalArgumentException("Invalid slide direction"); } + mSlideEdge = slideEdge; SidePropagation propagation = new SidePropagation(); propagation.setSide(slideEdge); setPropagation(propagation); } + /** + * Returns the edge that Views appear and disappear from. + * + * @return the edge of the scene to use for Views appearing and disappearing. One of + * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM}. + * @attr ref android.R.styleable#Slide_slideEdge + */ + public int getSlideEdge() { + return mSlideEdge; + } + @Override public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 52e6691..880f559 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -19,23 +19,32 @@ package android.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Path; import android.graphics.Rect; import android.util.ArrayMap; +import android.util.AttributeSet; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; import android.util.SparseLongArray; +import android.view.InflateException; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.ViewOverlay; import android.view.WindowId; +import android.view.animation.AnimationUtils; import android.widget.ListView; import android.widget.Spinner; import java.util.ArrayList; import java.util.List; +import java.util.StringTokenizer; + +import com.android.internal.R; /** * A Transition holds information about animations that will be run on its @@ -139,6 +148,13 @@ public abstract class Transition implements Cloneable { private static final int MATCH_LAST = MATCH_ITEM_ID; + private static final String MATCH_INSTANCE_STR = "instance"; + private static final String MATCH_NAME_STR = "name"; + /** To be removed before L release */ + private static final String MATCH_VIEW_NAME_STR = "viewName"; + private static final String MATCH_ID_STR = "id"; + private static final String MATCH_ITEM_ID_STR = "itemId"; + private static final int[] DEFAULT_MATCH_ORDER = { MATCH_NAME, MATCH_INSTANCE, @@ -146,6 +162,16 @@ public abstract class Transition implements Cloneable { MATCH_ITEM_ID, }; + private static final PathMotion STRAIGHT_PATH_MOTION = new PathMotion() { + @Override + public Path getPath(float startX, float startY, float endX, float endY) { + Path path = new Path(); + path.moveTo(startX, startY); + path.lineTo(endX, endY); + return path; + } + }; + private String mName = getClass().getName(); long mStartDelay = -1; @@ -215,6 +241,10 @@ public abstract class Transition implements Cloneable { // transitionNames. ArrayMap<String, String> mNameOverrides; + // The function used to interpolate along two-dimensional points. Typically used + // for adding curves to x/y View motion. + private PathMotion mPathMotion = STRAIGHT_PATH_MOTION; + /** * Constructs a Transition object with no target objects. A transition with * no targets defaults to running on all target objects in the scene hierarchy @@ -224,6 +254,66 @@ public abstract class Transition implements Cloneable { public Transition() {} /** + * Perform inflation from XML and apply a class-specific base style from a + * theme attribute or style resource. This constructor of Transition allows + * subclasses to use their own base style when they are inflating. + * + * @param context The Context the transition is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the transition. + */ + public Transition(Context context, AttributeSet attrs) { + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Transition); + long duration = a.getInt(R.styleable.Transition_duration, -1); + if (duration >= 0) { + setDuration(duration); + } + long startDelay = a.getInt(R.styleable.Transition_startDelay, -1); + if (startDelay > 0) { + setStartDelay(startDelay); + } + final int resID = a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0); + if (resID > 0) { + setInterpolator(AnimationUtils.loadInterpolator(context, resID)); + } + String matchOrder = a.getString(R.styleable.Transition_matchOrder); + if (matchOrder != null) { + setMatchOrder(parseMatchOrder(matchOrder)); + } + a.recycle(); + } + + private static int[] parseMatchOrder(String matchOrderString) { + StringTokenizer st = new StringTokenizer(matchOrderString, ","); + int matches[] = new int[st.countTokens()]; + int index = 0; + while (st.hasMoreTokens()) { + String token = st.nextToken().trim(); + if (MATCH_ID_STR.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_ID; + } else if (MATCH_INSTANCE_STR.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_INSTANCE; + } else if (MATCH_NAME_STR.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_NAME; + } else if (MATCH_VIEW_NAME_STR.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_NAME; + } else if (MATCH_ITEM_ID_STR.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_ITEM_ID; + } else if (token.isEmpty()) { + int[] smallerMatches = new int[matches.length - 1]; + System.arraycopy(matches, 0, smallerMatches, 0, index); + matches = smallerMatches; + index--; + } else { + throw new InflateException("Unknown match type in matchOrder: '" + token + "'"); + } + index++; + } + return matches; + } + + /** * Sets the duration of this transition. By default, there is no duration * (indicated by a negative number), which means that the Animator created by * the transition will have its own specified duration. If the duration of a @@ -1840,6 +1930,36 @@ public abstract class Transition implements Cloneable { } /** + * Sets the algorithm used to calculate two-dimensional interpolation. + * <p> + * Transitions such as {@link android.transition.ChangeBounds} move Views, typically + * in a straight path between the start and end positions. Applications that desire to + * have these motions move in a curve can change how Views interpolate in two dimensions + * by extending PathMotion and implementing + * {@link android.transition.PathMotion#getPath(float, float, float, float)}. + * </p> + * @param pathMotion Algorithm object to use for determining how to interpolate in two + * dimensions. If null, a straight-path algorithm will be used. + */ + public void setPathMotion(PathMotion pathMotion) { + if (pathMotion == null) { + mPathMotion = STRAIGHT_PATH_MOTION; + } else { + mPathMotion = pathMotion; + } + } + + /** + * Returns the algorithm object used to interpolate along two dimensions. This is typically + * used to determine the View motion between two points. + * + * @return The algorithm object used to interpolate along two dimensions. + */ + public PathMotion getPathMotion() { + return mPathMotion; + } + + /** * Sets the method for determining Animator start delays. * When a Transition affects several Views like {@link android.transition.Explode} or * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect diff --git a/core/java/android/transition/TransitionInflater.java b/core/java/android/transition/TransitionInflater.java index e0c3cae..fd3e450 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -16,21 +16,24 @@ package android.transition; +import com.android.internal.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Xml; -import android.view.Gravity; import android.view.InflateException; import android.view.ViewGroup; -import android.view.animation.AnimationUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; -import java.util.StringTokenizer; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; /** * This class inflates scenes and transitions from resource files. @@ -41,12 +44,11 @@ import java.util.StringTokenizer; * and {@link android.R.styleable#TransitionManager}. */ public class TransitionInflater { - private static final String MATCH_INSTANCE = "instance"; - private static final String MATCH_NAME = "name"; - /** To be removed before L release */ - private static final String MATCH_VIEW_NAME = "viewName"; - private static final String MATCH_ID = "id"; - private static final String MATCH_ITEM_ID = "itemId"; + + private static final Class<?>[] sConstructorSignature = new Class[] { + Context.class, AttributeSet.class}; + private final static ArrayMap<String, Constructor> sConstructors = + new ArrayMap<String, Constructor>(); private Context mContext; @@ -118,9 +120,8 @@ public class TransitionInflater { // // Transition loading // - private Transition createTransitionFromXml(XmlPullParser parser, - AttributeSet attrs, TransitionSet transitionSet) + AttributeSet attrs, Transition parent) throws XmlPullParserException, IOException { Transition transition = null; @@ -129,119 +130,107 @@ public class TransitionInflater { int type; int depth = parser.getDepth(); + TransitionSet transitionSet = (parent instanceof TransitionSet) + ? (TransitionSet) parent : null; + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { - boolean newTransition = false; - if (type != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if ("fade".equals(name)) { - TypedArray a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Fade); - int fadingMode = a.getInt(com.android.internal.R.styleable.Fade_fadingMode, - Fade.IN | Fade.OUT); - transition = new Fade(fadingMode); - newTransition = true; + transition = new Fade(mContext, attrs); } else if ("changeBounds".equals(name)) { - transition = new ChangeBounds(); - newTransition = true; + transition = new ChangeBounds(mContext, attrs); } else if ("slide".equals(name)) { - transition = createSlideTransition(attrs); - newTransition = true; + transition = new Slide(mContext, attrs); } else if ("explode".equals(name)) { - transition = new Explode(); - newTransition = true; + transition = new Explode(mContext, attrs); } else if ("moveImage".equals(name)) { - transition = new MoveImage(); - newTransition = true; + transition = new MoveImage(mContext, attrs); } else if ("changeImageTransform".equals(name)) { - transition = new ChangeImageTransform(); - newTransition = true; + transition = new ChangeImageTransform(mContext, attrs); } else if ("changeTransform".equals(name)) { - transition = new ChangeTransform(); - newTransition = true; + transition = new ChangeTransform(mContext, attrs); } else if ("changeClipBounds".equals(name)) { - transition = new ChangeClipBounds(); - newTransition = true; + transition = new ChangeClipBounds(mContext, attrs); } else if ("autoTransition".equals(name)) { - transition = new AutoTransition(); - newTransition = true; + transition = new AutoTransition(mContext, attrs); } else if ("recolor".equals(name)) { - transition = new Recolor(); - newTransition = true; + transition = new Recolor(mContext, attrs); } else if ("transitionSet".equals(name)) { - transition = new TransitionSet(); - TypedArray a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TransitionSet); - int ordering = a.getInt( - com.android.internal.R.styleable.TransitionSet_transitionOrdering, - TransitionSet.ORDERING_TOGETHER); - ((TransitionSet) transition).setOrdering(ordering); - createTransitionFromXml(parser, attrs, ((TransitionSet) transition)); - a.recycle(); - newTransition = true; + transition = new TransitionSet(mContext, attrs); } else if ("transition".equals(name)) { - transition = createCustomTransition(attrs); + transition = (Transition) createCustom(attrs, Transition.class, "transition"); } else if ("targets".equals(name)) { - if (parser.getDepth() - 1 > depth && transition != null) { - // We're inside the child tag - add targets to the child - getTargetIds(parser, attrs, transition); - } else if (parser.getDepth() - 1 == depth && transitionSet != null) { - // add targets to the set - getTargetIds(parser, attrs, transitionSet); - } - } - if (transition != null || "targets".equals(name)) { - if (newTransition) { - loadTransition(transition, attrs); - if (transitionSet != null) { - transitionSet.addTransition(transition); - } - } + getTargetIds(parser, attrs, parent); + } else if ("arcMotion".equals(name)) { + parent.setPathMotion(new ArcMotion(mContext, attrs)); + } else if ("pathMotion".equals(name)) { + parent.setPathMotion((PathMotion)createCustom(attrs, PathMotion.class, "pathMotion")); + } else if ("patternMotion".equals(name)) { + parent.setPathMotion(new PatternMotion(mContext, attrs)); } else { throw new RuntimeException("Unknown scene name: " + parser.getName()); } + if (transition != null) { + if (!parser.isEmptyElementTag()) { + createTransitionFromXml(parser, attrs, transition); + } + if (transitionSet != null) { + transitionSet.addTransition(transition); + transition = null; + } else if (parent != null) { + throw new InflateException("Could not add transition to another transition."); + } + } } return transition; } - private Transition createCustomTransition(AttributeSet attrs) { + private Object createCustom(AttributeSet attrs, Class expectedType, String tag) { String className = attrs.getAttributeValue(null, "class"); if (className == null) { - throw new RuntimeException("transition tag must have a 'class' attribute"); + throw new InflateException(tag + " tag must have a 'class' attribute"); } try { - Class c = Class.forName(className); - if (!Transition.class.isAssignableFrom(c)) { - throw new RuntimeException("transition class must be a Transition: " + className); + synchronized (sConstructors) { + Constructor constructor = sConstructors.get(className); + if (constructor == null) { + Class c = mContext.getClassLoader().loadClass(className) + .asSubclass(expectedType); + if (c != null) { + constructor = c.getConstructor(sConstructorSignature); + sConstructors.put(className, constructor); + } + } + + return constructor.newInstance(mContext, attrs); } - return (Transition) c.newInstance(); } catch (InstantiationException e) { - throw new RuntimeException("Could not instantiate transition class", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Could not access default constructor for transition class " - + className, e); + throw new InflateException("Could not instantiate " + expectedType + " class " + + className, e); } catch (ClassNotFoundException e) { - throw new RuntimeException("Could not find transition class " + className, e); + throw new InflateException("Could not instantiate " + expectedType + " class " + + className, e); + } catch (InvocationTargetException e) { + throw new InflateException("Could not instantiate " + expectedType + " class " + + className, e); + } catch (NoSuchMethodException e) { + throw new InflateException("Could not instantiate " + expectedType + " class " + + className, e); + } catch (IllegalAccessException e) { + throw new InflateException("Could not instantiate " + expectedType + " class " + + className, e); } } - private Slide createSlideTransition(AttributeSet attrs) { - TypedArray a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Slide); - int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Gravity.BOTTOM); - Slide slide = new Slide(edge); - a.recycle(); - return slide; - } - private void getTargetIds(XmlPullParser parser, AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException { @@ -258,34 +247,27 @@ public class TransitionInflater { String name = parser.getName(); if (name.equals("target")) { - TypedArray a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TransitionTarget); - int id = a.getResourceId( - com.android.internal.R.styleable.TransitionTarget_targetId, -1); + TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TransitionTarget); + int id = a.getResourceId(R.styleable.TransitionTarget_targetId, 0); String transitionName; - if (id >= 0) { + if (id != 0) { transition.addTarget(id); - } else if ((id = a.getResourceId( - com.android.internal.R.styleable.TransitionTarget_excludeId, -1)) >= 0) { + } else if ((id = a.getResourceId(R.styleable.TransitionTarget_excludeId, 0)) != 0) { transition.excludeTarget(id, true); - } else if ((transitionName = a.getString( - com.android.internal.R.styleable.TransitionTarget_targetName)) + } else if ((transitionName = a.getString(R.styleable.TransitionTarget_targetName)) != null) { transition.addTarget(transitionName); - } else if ((transitionName = a.getString( - com.android.internal.R.styleable.TransitionTarget_excludeName)) + } else if ((transitionName = a.getString(R.styleable.TransitionTarget_excludeName)) != null) { transition.excludeTarget(transitionName, true); } else { - String className = a.getString( - com.android.internal.R.styleable.TransitionTarget_excludeClass); + String className = a.getString(R.styleable.TransitionTarget_excludeClass); try { if (className != null) { Class clazz = Class.forName(className); transition.excludeTarget(clazz, true); - } else if ((className = a.getString( - com.android.internal.R.styleable.TransitionTarget_targetClass)) - != null) { + } else if ((className = + a.getString(R.styleable.TransitionTarget_targetClass)) != null) { Class clazz = Class.forName(className); transition.addTarget(clazz); } @@ -299,72 +281,6 @@ public class TransitionInflater { } } - private int[] parseMatchOrder(String matchOrderString) { - StringTokenizer st = new StringTokenizer(matchOrderString, ","); - int matches[] = new int[st.countTokens()]; - int index = 0; - while (st.hasMoreTokens()) { - String token = st.nextToken().trim(); - if (MATCH_ID.equalsIgnoreCase(token)) { - matches[index] = Transition.MATCH_ID; - } else if (MATCH_INSTANCE.equalsIgnoreCase(token)) { - matches[index] = Transition.MATCH_INSTANCE; - } else if (MATCH_NAME.equalsIgnoreCase(token)) { - matches[index] = Transition.MATCH_NAME; - } else if (MATCH_VIEW_NAME.equalsIgnoreCase(token)) { - matches[index] = Transition.MATCH_NAME; - } else if (MATCH_ITEM_ID.equalsIgnoreCase(token)) { - matches[index] = Transition.MATCH_ITEM_ID; - } else if (token.isEmpty()) { - int[] smallerMatches = new int[matches.length - 1]; - System.arraycopy(matches, 0, smallerMatches, 0, index); - matches = smallerMatches; - index--; - } else { - throw new RuntimeException("Unknown match type in matchOrder: '" + token + "'"); - } - index++; - } - return matches; - } - - private Transition loadTransition(Transition transition, AttributeSet attrs) - throws Resources.NotFoundException { - - TypedArray a = - mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Transition); - long duration = a.getInt(com.android.internal.R.styleable.Transition_duration, -1); - if (duration >= 0) { - transition.setDuration(duration); - } - long startDelay = a.getInt(com.android.internal.R.styleable.Transition_startDelay, -1); - if (startDelay > 0) { - transition.setStartDelay(startDelay); - } - final int resID = - a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0); - if (resID > 0) { - transition.setInterpolator(AnimationUtils.loadInterpolator(mContext, resID)); - } - String matchOrder = - a.getString(com.android.internal.R.styleable.Transition_matchOrder); - if (matchOrder != null) { - transition.setMatchOrder(parseMatchOrder(matchOrder)); - } - a.recycle(); - if (transition instanceof Visibility) { - a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.VisibilityTransition); - int mode = a.getInt( - com.android.internal.R.styleable.VisibilityTransition_visibilityMode, 0); - a.recycle(); - if (mode != 0) { - ((Visibility)transition).setMode(mode); - } - } - return transition; - } - // // TransitionManager loading // @@ -399,15 +315,11 @@ public class TransitionInflater { private void loadTransition(AttributeSet attrs, ViewGroup sceneRoot, TransitionManager transitionManager) throws Resources.NotFoundException { - TypedArray a = mContext.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.TransitionManager); - int transitionId = a.getResourceId( - com.android.internal.R.styleable.TransitionManager_transition, -1); - int fromId = a.getResourceId( - com.android.internal.R.styleable.TransitionManager_fromScene, -1); + TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TransitionManager); + int transitionId = a.getResourceId(R.styleable.TransitionManager_transition, -1); + int fromId = a.getResourceId(R.styleable.TransitionManager_fromScene, -1); Scene fromScene = (fromId < 0) ? null: Scene.getSceneForLayout(sceneRoot, fromId, mContext); - int toId = a.getResourceId( - com.android.internal.R.styleable.TransitionManager_toScene, -1); + int toId = a.getResourceId(R.styleable.TransitionManager_toScene, -1); Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext); if (transitionId >= 0) { diff --git a/core/java/android/transition/TransitionSet.java b/core/java/android/transition/TransitionSet.java index 495814a..63957e9 100644 --- a/core/java/android/transition/TransitionSet.java +++ b/core/java/android/transition/TransitionSet.java @@ -16,9 +16,13 @@ package android.transition; +import com.android.internal.R; + import android.animation.TimeInterpolator; -import android.graphics.Rect; +import android.content.Context; +import android.content.res.TypedArray; import android.util.AndroidRuntimeException; +import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -77,6 +81,15 @@ public class TransitionSet extends Transition { public TransitionSet() { } + public TransitionSet(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TransitionSet); + int ordering = a.getInt(R.styleable.TransitionSet_transitionOrdering, + TransitionSet.ORDERING_TOGETHER); + setOrdering(ordering); + a.recycle(); + } + /** * Sets the play order of this set's child transitions. * @@ -272,6 +285,14 @@ public class TransitionSet extends Transition { return (TransitionSet) super.removeListener(listener); } + @Override + public void setPathMotion(PathMotion pathMotion) { + super.setPathMotion(pathMotion); + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).setPathMotion(pathMotion); + } + } + /** * Removes the specified child transition from this set. * diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index aa9f04e..1629726 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -16,14 +16,18 @@ package android.transition; +import com.android.internal.R; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; - /** * This transition tracks changes to the visibility of target views in the * start and end scenes. Visibility is determined not just by the @@ -74,12 +78,25 @@ public abstract class Visibility extends Transition { private int mMode = IN | OUT; + public Visibility() {} + + public Visibility(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VisibilityTransition); + int mode = a.getInt(R.styleable.VisibilityTransition_visibilityMode, 0); + a.recycle(); + if (mode != 0) { + setMode(mode); + } + } + /** * Changes the transition to support appearing and/or disappearing Views, depending * on <code>mode</code>. * * @param mode The behavior supported by this transition, a combination of * {@link #IN} and {@link #OUT}. + * @attr ref android.R.styleable#VisibilityTransition_visibilityMode */ public void setMode(int mode) { if ((mode & ~(IN | OUT)) != 0) { @@ -88,6 +105,17 @@ public abstract class Visibility extends Transition { mMode = mode; } + /** + * Returns whether appearing and/or disappearing Views are supported. + * + * Returns whether appearing and/or disappearing Views are supported. A combination of + * {@link #IN} and {@link #OUT}. + * @attr ref android.R.styleable#VisibilityTransition_visibilityMode + */ + public int getMode() { + return mMode; + } + @Override public String[] getTransitionProperties() { return sTransitionProperties; |
