From 38a82ed829e57b2c155d07d8856ed92a0a6d9c78 Mon Sep 17 00:00:00 2001 From: Michael Gangolf Date: Tue, 28 Apr 2020 15:48:45 +0200 Subject: [PATCH] feat(android): animate elevation (#11646) Fixes TIMOB-27855 --- .../titanium/util/TiAnimationBuilder.java | 444 +----------------- apidoc/Titanium/UI/Animation.yml | 16 +- 2 files changed, 22 insertions(+), 438 deletions(-) diff --git a/android/titanium/src/java/org/appcelerator/titanium/util/TiAnimationBuilder.java b/android/titanium/src/java/org/appcelerator/titanium/util/TiAnimationBuilder.java index fdfe5c7e4a8..2f92c69c571 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/util/TiAnimationBuilder.java +++ b/android/titanium/src/java/org/appcelerator/titanium/util/TiAnimationBuilder.java @@ -26,16 +26,10 @@ import org.appcelerator.titanium.view.TiCompositeLayout.LayoutParams; import org.appcelerator.titanium.view.TiUIView; -import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.TransitionDrawable; import android.os.Build; -import android.os.Looper; -import android.os.MessageQueue; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; @@ -44,7 +38,6 @@ import android.view.animation.Animation; import android.view.animation.AnimationSet; import android.view.animation.Transformation; -import android.view.animation.TranslateAnimation; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorSet; @@ -112,6 +105,7 @@ public class TiAnimationBuilder protected float anchorX; protected float anchorY; + protected float elevation = -1; protected Ti2DMatrix tdm = null; protected Double delay = null; protected Double duration = null; @@ -238,6 +232,10 @@ public void applyOptions(HashMap options) backgroundColor = TiConvert.toColor(options, TiC.PROPERTY_BACKGROUND_COLOR); } + if (options.containsKey(TiC.PROPERTY_ELEVATION)) { + elevation = TiConvert.toFloat(options, TiC.PROPERTY_ELEVATION, -1); + } + this.options = options; } @@ -252,38 +250,6 @@ public void setCallback(KrollFunction callback) this.callback = callback; } - /** - * Builds the Animations used for pre-Honeycomb - * style of animations, i.e., view animations - * instead of property animations. We use this - * only when we can't use NineOldAndroids to accomplish - * the animation, which happens only when the - * animation contains a transform (Ti2DMatrix) that - * is too complicated. See this class documentation - * for more information. - * @return An AnimationSet with the view Animations that - * will be used to accomplish the requested animation. - */ - private AnimationSet buildViewAnimations() - { - if (Log.isDebugModeEnabled()) { - Log.w(TAG, "Using legacy animations"); - } - - ViewParent parent = view.getParent(); - int parentWidth = 0; - int parentHeight = 0; - - if (parent instanceof ViewGroup) { - ViewGroup group = (ViewGroup) parent; - parentHeight = group.getMeasuredHeight(); - parentWidth = group.getMeasuredWidth(); - } - - return buildViewAnimations(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight(), - parentWidth, parentHeight); - } - /** * Builds the Animators used for Honeycomb+ animations * @return AnimatorSet containing the Animator instances @@ -329,6 +295,10 @@ private AnimatorSet buildPropertyAnimators(int x, int y, int w, int h, int paren addAnimator(animators, ObjectAnimator.ofFloat(view, "alpha", toOpacity.floatValue())); } + if (elevation >= 0) { + addAnimator(animators, ObjectAnimator.ofFloat(view, "elevation", elevation)); + } + if (backgroundColor != null) { View bgView = view; if (view instanceof TiBorderWrapperView && ((TiBorderWrapperView) view).getChildCount() > 0) { @@ -504,7 +474,6 @@ private AnimatorSet buildPropertyAnimators(int x, int y, int w, int h, int paren if (right != null) { int afterRight = optionRight.getAsPixels(parentView); - ; TiDimension beforeRightD = ((TiCompositeLayout.LayoutParams) view.getLayoutParams()).optionRight; int beforeRight = 0; @@ -520,7 +489,6 @@ private AnimatorSet buildPropertyAnimators(int x, int y, int w, int h, int paren if (centerX != null) { int afterCenterX = optionCenterX.getAsPixels(parentView); - ; int beforeCenterX = 0; TiDimension beforeCenterXD = ((TiCompositeLayout.LayoutParams) view.getLayoutParams()).optionCenterX; @@ -555,7 +523,6 @@ private AnimatorSet buildPropertyAnimators(int x, int y, int w, int h, int paren if (centerY != null) { int afterCenterY = optionCenterY.getAsPixels(parentView); - ; int beforeCenterY = 0; TiDimension beforeCenterYD = ((TiCompositeLayout.LayoutParams) view.getLayoutParams()).optionCenterY; @@ -700,314 +667,6 @@ public TiMatrixAnimation createMatrixAnimation(Ti2DMatrix matrix) return new TiMatrixAnimation(matrix, anchorX, anchorY); } - /** - * Builds the Animations used for pre-Honeycomb - * style of animations, i.e., view animations - * instead of property animations. We use this - * only when we can't use NineOldAndroids to accomplish - * the animation, which happens only when the - * animation contains a transform (Ti2DMatrix) that - * is too complicated. See this class documentation - * for more information. - * @param x The view's left property. - * @param y The view's top property. - * @param w The view's width. - * @param h The view's height. - * @param parentWidth The view parent's width. - * @param parentHeight The view parent's height. - * @return An AnimationSet with the view Animations that - * will be used to accomplish the requested animation. - */ - private AnimationSet buildViewAnimations(int x, int y, int w, int h, int parentWidth, int parentHeight) - { - boolean includesRotation = false; - AnimationSet as = new AnimationSet(false); - AnimationListener animationListener = new AnimationListener(); - as.setAnimationListener(animationListener); - TiUIView tiView = viewProxy.peekView(); - - if (toOpacity != null) { - // Determine which value to use for "from" value, in this order: - // 1.) If we previously performed an alpha animation on the view, - // use that as the from value. - // 2.) Else, if we have set an opacity property on the view, use - // that as the from value. - // 3.) Else, use 1.0f as the from value. - - float fromOpacity; - float currentAnimatedAlpha = tiView == null ? Float.MIN_VALUE : tiView.getAnimatedAlpha(); - - if (currentAnimatedAlpha != Float.MIN_VALUE) { - // MIN_VALUE is used as a signal that no value has been set. - fromOpacity = currentAnimatedAlpha; - - } else if (viewProxy.hasProperty(TiC.PROPERTY_OPACITY)) { - fromOpacity = TiConvert.toFloat(viewProxy.getProperty(TiC.PROPERTY_OPACITY)); - - } else { - fromOpacity = 1.0f; - } - - Animation animation = new AlphaAnimation(fromOpacity, toOpacity.floatValue()); - - // Remember the toOpacity value for next time, since we no way of - // looking - // up animated alpha values on the Android native view itself. - if (tiView != null) { - tiView.setAnimatedAlpha(toOpacity.floatValue()); - } - - applyOpacity = true; // Used in the animation listener - addAnimation(as, animation); - animation.setAnimationListener(animationListener); - - if (viewProxy.hasProperty(TiC.PROPERTY_OPACITY) && toOpacity != null) { - prepareOpacityForAnimation(); - } - } - - if (backgroundColor != null) { - int fromBackgroundColor = 0; - - if (viewProxy.hasProperty(TiC.PROPERTY_BACKGROUND_COLOR)) { - fromBackgroundColor = - TiConvert.toColor(TiConvert.toString(viewProxy.getProperty(TiC.PROPERTY_BACKGROUND_COLOR))); - } else { - Log.w( - TAG, - "Cannot animate view without a backgroundColor. View doesn't have that property. Using #00000000"); - fromBackgroundColor = Color.argb(0, 0, 0, 0); - } - - Animation a = new TiColorAnimation(view, fromBackgroundColor, backgroundColor); - addAnimation(as, a); - } - - if (tdm != null) { - - Animation anim; - if (tdm.hasScaleOperation() && tiView != null) { - tiView.setAnimatedScaleValues( - tdm.verifyScaleValues(tiView, (autoreverse != null && autoreverse.booleanValue()))); - } - - if (tdm.hasRotateOperation() && tiView != null) { - includesRotation = true; - tiView.setAnimatedRotationDegrees( - tdm.verifyRotationValues(tiView, (autoreverse != null && autoreverse.booleanValue()))); - } - - anim = new TiMatrixAnimation(tdm, anchorX, anchorY); - addAnimation(as, anim); - } - - if (top != null || bottom != null || left != null || right != null || centerX != null || centerY != null) { - TiDimension optionTop = null, optionBottom = null; - TiDimension optionLeft = null, optionRight = null; - TiDimension optionCenterX = null, optionCenterY = null; - - // Note that we're stringifying the values to make sure we - // use the correct TiDimension constructor, except when - // we know the values are expressed for certain in pixels. - if (top != null) { - optionTop = new TiDimension(top, TiDimension.TYPE_TOP); - } else if (bottom == null && centerY == null) { - // Fix a top value since no other y-axis value is being set. - optionTop = new TiDimension(view.getTop(), TiDimension.TYPE_TOP); - optionTop.setUnits(TypedValue.COMPLEX_UNIT_PX); - } - - if (bottom != null) { - optionBottom = new TiDimension(bottom, TiDimension.TYPE_BOTTOM); - } - - if (left != null) { - optionLeft = new TiDimension(left, TiDimension.TYPE_LEFT); - } else if (right == null && centerX == null) { - // Fix a left value since no other x-axis value is being set. - optionLeft = new TiDimension(view.getLeft(), TiDimension.TYPE_LEFT); - optionLeft.setUnits(TypedValue.COMPLEX_UNIT_PX); - } - - if (right != null) { - optionRight = new TiDimension(right, TiDimension.TYPE_RIGHT); - } - - if (centerX != null) { - optionCenterX = new TiDimension(centerX, TiDimension.TYPE_CENTER_X); - } - - if (centerY != null) { - optionCenterY = new TiDimension(centerY, TiDimension.TYPE_CENTER_Y); - } - - int[] horizontal = new int[2]; - int[] vertical = new int[2]; - ViewParent parent = view.getParent(); - View parentView = null; - - if (parent instanceof View) { - parentView = (View) parent; - } - - TiCompositeLayout.computePosition(parentView, optionLeft, optionCenterX, optionRight, w, 0, parentWidth, - horizontal); - TiCompositeLayout.computePosition(parentView, optionTop, optionCenterY, optionBottom, h, 0, parentHeight, - vertical); - - Animation animation = new TranslateAnimation(Animation.ABSOLUTE, 0, Animation.ABSOLUTE, horizontal[0] - x, - Animation.ABSOLUTE, 0, Animation.ABSOLUTE, vertical[0] - y); - - animation.setAnimationListener(animationListener); - addAnimation(as, animation); - - // Will need to update layout params at end of animation - // so that touch events will be recognized at new location, - // and so that view will stay at new location after changes in - // orientation. But if autoreversing to original layout, no - // need to re-layout. But don't do it if a rotation is included - // because re-layout will lose the rotation. - relayoutChild = !includesRotation && (autoreverse == null || !autoreverse.booleanValue()); - - if (Log.isDebugModeEnabled()) { - Log.d(TAG, - "animate " + viewProxy + " relative to self: " + (horizontal[0] - x) + ", " + (vertical[0] - y), - Log.DEBUG_MODE); - } - } - - if (tdm == null && (width != null || height != null)) { - TiDimension optionWidth, optionHeight; - - if (width != null) { - optionWidth = new TiDimension(width, TiDimension.TYPE_WIDTH); - } else { - optionWidth = new TiDimension(w, TiDimension.TYPE_WIDTH); - optionWidth.setUnits(TypedValue.COMPLEX_UNIT_PX); - } - - if (height != null) { - optionHeight = new TiDimension(height, TiDimension.TYPE_HEIGHT); - } else { - optionHeight = new TiDimension(h, TiDimension.TYPE_HEIGHT); - optionHeight.setUnits(TypedValue.COMPLEX_UNIT_PX); - } - - ViewParent parent = view.getParent(); - View parentView = null; - - if (parent instanceof View) { - parentView = (View) parent; - } - - int toWidth = optionWidth.getAsPixels((parentView != null) ? parentView : view); - int toHeight = optionHeight.getAsPixels((parentView != null) ? parentView : view); - - SizeAnimation sizeAnimation = new SizeAnimation(view, w, h, toWidth, toHeight); - - if (duration != null) { - sizeAnimation.setDuration(duration.longValue()); - } - - sizeAnimation.setInterpolator(this.curve.toInterpolator()); - sizeAnimation.setAnimationListener(animationListener); - addAnimation(as, sizeAnimation); - - // Will need to update layout params at end of animation - // so that touch events will be recognized within new - // size rectangle, and so that new size will survive - // any changes in orientation. But if autoreversing - // to original layout, no need to re-layout. But don't do it if a - // rotation is included - // because re-layout will lose the rotation. - relayoutChild = !includesRotation && (autoreverse == null || !autoreverse.booleanValue()); - } - - // Set duration, repeatMode and fillAfter only after adding children. - // The values are pushed down to the child animations. - as.setFillAfter(true); - - if (duration != null) { - as.setDuration(duration.longValue()); - } - - if (autoreverse != null && autoreverse.booleanValue()) { - as.setRepeatMode(Animation.REVERSE); - } else { - as.setRepeatMode(Animation.RESTART); - } - - // startOffset is relevant to the animation set and thus - // not also set on the child animations. - if (delay != null) { - as.setStartOffset(delay.longValue()); - } - - return as; - } - - /** - * Pre-Honeycomb size animation. - */ - protected class SizeAnimation extends Animation - { - - protected View view; - protected float fromWidth, fromHeight, toWidth, toHeight; - protected static final String TAG = "TiSizeAnimation"; - - public SizeAnimation(View view, float fromWidth, float fromHeight, float toWidth, float toHeight) - { - this.view = view; - this.fromWidth = fromWidth; - this.fromHeight = fromHeight; - this.toWidth = toWidth; - this.toHeight = toHeight; - - if (Log.isDebugModeEnabled()) { - Log.d(TAG, - "animate view from (" + fromWidth + "x" + fromHeight + ") to (" + toWidth + "x" + toHeight + ")", - Log.DEBUG_MODE); - } - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation transformation) - { - super.applyTransformation(interpolatedTime, transformation); - - int width = 0; - if (fromWidth == toWidth) { - width = (int) fromWidth; - - } else { - width = (int) Math.floor(fromWidth + ((toWidth - fromWidth) * interpolatedTime)); - } - - int height = 0; - if (fromHeight == toHeight) { - height = (int) fromHeight; - - } else { - height = (int) Math.floor(fromHeight + ((toHeight - fromHeight) * interpolatedTime)); - } - - ViewGroup.LayoutParams params = view.getLayoutParams(); - params.width = width; - params.height = height; - - if (params instanceof TiCompositeLayout.LayoutParams) { - TiCompositeLayout.LayoutParams tiParams = (TiCompositeLayout.LayoutParams) params; - tiParams.optionHeight = new TiDimension(height, TiDimension.TYPE_HEIGHT); - tiParams.optionHeight.setUnits(TypedValue.COMPLEX_UNIT_PX); - tiParams.optionWidth = new TiDimension(width, TiDimension.TYPE_WIDTH); - tiParams.optionWidth.setUnits(TypedValue.COMPLEX_UNIT_PX); - } - - view.setLayoutParams(params); - } - } - /** * Pre-Honeycomb matrix animation. */ @@ -1073,52 +732,6 @@ public void invalidateWithMatrix(View view) } } - /** - * Pre-Honeycomb color animation. - */ - public static class TiColorAnimation extends Animation - { - View view; - TransitionDrawable transitionDrawable; - boolean reversing = false; - int duration = 0; - - public TiColorAnimation(View view, int fromColor, int toColor) - { - this.view = view; - - ColorDrawable fromColorDrawable = new ColorDrawable(fromColor); - ColorDrawable toColorDrawable = new ColorDrawable(toColor); - transitionDrawable = new TransitionDrawable(new Drawable[] { fromColorDrawable, toColorDrawable }); - - this.setAnimationListener(new android.view.animation.Animation.AnimationListener() { - @SuppressWarnings("deprecation") - public void onAnimationStart(Animation animation) - { - TiColorAnimation.this.view.setBackgroundDrawable(transitionDrawable); - TiColorAnimation.this.duration = Long.valueOf(animation.getDuration()).intValue(); - transitionDrawable.startTransition(TiColorAnimation.this.duration); - } - - public void onAnimationRepeat(Animation animation) - { - if (animation.getRepeatMode() == Animation.REVERSE) { - reversing = !reversing; - } - if (reversing) { - transitionDrawable.reverseTransition(TiColorAnimation.this.duration); - } else { - transitionDrawable.startTransition(TiColorAnimation.this.duration); - } - } - - public void onAnimationEnd(Animation animation) - { - } - }); - } - } - /** * A helper class for Honeycomb+ Property Animators to animate width/height/top/bottom/left/right/center. * Based on the Android doc http://developer.android.com/guide/topics/graphics/prop-animation.html, to have @@ -1276,22 +889,7 @@ public void onAnimationEnd(Animator animator) } if (animationProxy != null) { - // In versions prior to Honeycomb, don't fire the event - // until the message queue is empty. There appears to be - // a bug in versions before Honeycomb where this - // onAnimationEnd listener can be called even before the - // animation is really complete. - if (Build.VERSION.SDK_INT >= TiC.API_LEVEL_HONEYCOMB) { - animationProxy.fireEvent(TiC.EVENT_COMPLETE, null); - } else { - Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { - public boolean queueIdle() - { - animationProxy.fireEvent(TiC.EVENT_COMPLETE, null); - return false; - } - }); - } + animationProxy.fireEvent(TiC.EVENT_COMPLETE, null); } } } @@ -1360,22 +958,7 @@ public void onAnimationEnd(Animation a) } if (animationProxy != null) { - // In versions prior to Honeycomb, don't fire the event - // until the message queue is empty. There appears to be - // a bug in versions before Honeycomb where this - // onAnimationEnd listener can be called even before the - // animation is really complete. - if (Build.VERSION.SDK_INT >= TiC.API_LEVEL_HONEYCOMB) { - animationProxy.fireEvent(TiC.EVENT_COMPLETE, null); - } else { - Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { - public boolean queueIdle() - { - animationProxy.fireEvent(TiC.EVENT_COMPLETE, null); - return false; - } - }); - } + animationProxy.fireEvent(TiC.EVENT_COMPLETE, null); } } } @@ -1417,11 +1000,6 @@ public void start(TiViewProxy viewProxy, View view) // We can use Honeycomb+ property Animators via the // NineOldAndroids library. buildPropertyAnimators().start(); - } else { - // We cannot use Honeycomb+ property Animators - // because a matrix transform is too complicated - // (see top of this file for explanation.) - view.startAnimation(buildViewAnimations()); } } diff --git a/apidoc/Titanium/UI/Animation.yml b/apidoc/Titanium/UI/Animation.yml index 1ed9c7bbaa2..fdcfd7b22da 100644 --- a/apidoc/Titanium/UI/Animation.yml +++ b/apidoc/Titanium/UI/Animation.yml @@ -104,7 +104,7 @@ properties: constants: Titanium.UI.ANIMATION_CURVE_* platforms: [android, iphone, ipad] since: { android: "8.0.0", iphone: "0.9", ipad: "0.9" } - + - name: dampingRatio summary: | The damping ratio for the spring animation as it approaches its quiescent state. @@ -123,6 +123,12 @@ properties: summary: Duration of the animation, in milliseconds. type: Number + - name: elevation + summary: Value of the `elevation` property at the end of the animation. + type: Number + platforms: [android] + since: { android: "9.1.0" } + - name: height summary: Value of the `height` property at the end of the animation. type: Number @@ -151,13 +157,13 @@ properties: - name: right summary: Value of the `right` property at the end of the animation. type: Number - + - name: springVelocity summary: The initial spring velocity. description: | For smooth start to the animation, match this value to the velocity of view as it was prior to attachment. - A value of 1 corresponds to the total animation distance traversed in one second. - For example, if the total animation distance is 200 points and you want the start of the + A value of 1 corresponds to the total animation distance traversed in one second. + For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5. type: Number platforms: [iphone, ipad] @@ -260,7 +266,7 @@ examples: }); box.animate(a); }); - + win.add(box); - title: Using an anchorPoint (Android and iOS)