From df75689d4d8ccac618ea4d11ac693d68fe1a8de7 Mon Sep 17 00:00:00 2001 From: Ahaan Ugale Date: Wed, 19 May 2021 23:46:14 -0700 Subject: [PATCH] Translate: Text alpha animation on toggling translated state. Apply a simple text alpha animation when toggling between original and translated text. The text is fully faded out, then swapped to the new text, then the fading is reversed. Quick toggles are handled by ending the previous animation (which resets the alpha value) when starting a new one. If the toggle is extremely fast (<250ms for the currently defined animation duration), the text stays in it's original state instead of swapping back-and-forth. This is arguably not ideal, but anyway not worth the complexity for fixing. There is an unhandled edge case where if the color is changed by the app during the animation, the app's change would get overridden. It should be rare and doesn't seem very important to fix. Bug: 178651829 Test: atest CtsTranslationTestCases Test: manual - across several apps, while scrolling, concurrently interacting with the app, concurrently closing the app, selecting text during the animation (with animation speed slowed down to see the effect). Change-Id: I08a26de2253bb345f01186a6748b2d0ff6c2a419 --- .../translation/UiTranslationController.java | 10 ++- .../translation/ViewTranslationCallback.java | 8 ++ .../widget/TextViewTranslationCallback.java | 86 +++++++++++++++++-- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java index 95b3f687c0584..bf2af518969b0 100644 --- a/core/java/android/view/translation/UiTranslationController.java +++ b/core/java/android/view/translation/UiTranslationController.java @@ -404,9 +404,6 @@ public class UiTranslationController { // implememtation. callback = new TextViewTranslationCallback(); view.setViewTranslationCallback(callback); - if (mViewsToPadContent.contains(autofillId)) { - callback.enableContentPadding(); - } } else { if (DEBUG) { Log.d(TAG, view + " doesn't support showing translation because of " @@ -415,6 +412,10 @@ public class UiTranslationController { return; } } + callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS); + if (mViewsToPadContent.contains(autofillId)) { + callback.enableContentPadding(); + } view.onViewTranslationResponse(response); callback.onShowTranslation(view); }); @@ -422,6 +423,9 @@ public class UiTranslationController { } } + // TODO: Use a device config value. + private static final int ANIMATION_DURATION_MILLIS = 250; + /** * Creates a Translator for the given source and target translation specs and start the ui * translation when the Translator is created successfully. diff --git a/core/java/android/view/translation/ViewTranslationCallback.java b/core/java/android/view/translation/ViewTranslationCallback.java index 1f0723b0bd054..6efd621c4caaf 100644 --- a/core/java/android/view/translation/ViewTranslationCallback.java +++ b/core/java/android/view/translation/ViewTranslationCallback.java @@ -64,4 +64,12 @@ public interface ViewTranslationCallback { * @hide */ default void enableContentPadding() {} + + /** + * Sets the duration for animations while transitioning the view between the original and + * translated contents. + * + * @hide + */ + default void setAnimationDurationMillis(int durationMillis) {} } diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java index 92c9142749df3..a7d5ee4652992 100644 --- a/core/java/android/widget/TextViewTranslationCallback.java +++ b/core/java/android/widget/TextViewTranslationCallback.java @@ -16,10 +16,15 @@ package android.widget; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.res.ColorStateList; +import android.graphics.Color; import android.os.Build; import android.text.TextUtils; +import android.text.method.TransformationMethod; import android.text.method.TranslationTransformationMethod; import android.util.Log; import android.view.View; @@ -47,6 +52,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { private boolean mIsShowingTranslation = false; private boolean mIsTextPaddingEnabled = false; private CharSequence mPaddedText; + private int mAnimationDurationMillis = 250; // default value private CharSequence mContentDescription; @@ -82,14 +88,19 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { */ @Override public boolean onShowTranslation(@NonNull View view) { - mIsShowingTranslation = true; if (view.getViewTranslationResponse() == null) { Log.wtf(TAG, "onShowTranslation() shouldn't be called before " + "onViewTranslationResponse()."); return false; } if (mTranslationTransformation != null) { - ((TextView) view).setTransformationMethod(mTranslationTransformation); + final TransformationMethod transformation = mTranslationTransformation; + runWithAnimation( + (TextView) view, + () -> { + mIsShowingTranslation = true; + ((TextView) view).setTransformationMethod(transformation); + }); ViewTranslationResponse response = view.getViewTranslationResponse(); if (response.getKeys().contains(ViewTranslationRequest.ID_CONTENT_DESCRIPTION)) { CharSequence translatedContentDescription = @@ -114,7 +125,6 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { */ @Override public boolean onHideTranslation(@NonNull View view) { - mIsShowingTranslation = false; if (view.getViewTranslationResponse() == null) { Log.wtf(TAG, "onHideTranslation() shouldn't be called before " + "onViewTranslationResponse()."); @@ -122,8 +132,14 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { } // Restore to original text content. if (mTranslationTransformation != null) { - ((TextView) view).setTransformationMethod( - mTranslationTransformation.getOriginalTransformationMethod()); + final TransformationMethod transformation = + mTranslationTransformation.getOriginalTransformationMethod(); + runWithAnimation( + (TextView) view, + () -> { + mIsShowingTranslation = false; + ((TextView) view).setTransformationMethod(transformation); + }); if (!TextUtils.isEmpty(mContentDescription)) { view.setContentDescription(mContentDescription); } @@ -212,4 +228,64 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { } private static final char COMPAT_PAD_CHARACTER = '\u2002'; + + @Override + public void setAnimationDurationMillis(int durationMillis) { + mAnimationDurationMillis = durationMillis; + } + + /** + * Applies a simple text alpha animation when toggling between original and translated text. The + * text is fully faded out, then swapped to the new text, then the fading is reversed. + * + * @param runnable the operation to run on the view after the text is faded out, to change to + * displaying the original or translated text. + */ + private void runWithAnimation(TextView view, Runnable runnable) { + if (mAnimator != null) { + mAnimator.end(); + // Note: mAnimator is now null; do not use again here. + } + int fadedOutColor = colorWithAlpha(view.getCurrentTextColor(), 0); + mAnimator = ValueAnimator.ofArgb(view.getCurrentTextColor(), fadedOutColor); + mAnimator.addUpdateListener( + // Note that if the text has a ColorStateList, this replaces it with a single color + // for all states. The original ColorStateList is restored when the animation ends + // (see below). + (valueAnimator) -> view.setTextColor((Integer) valueAnimator.getAnimatedValue())); + mAnimator.setRepeatMode(ValueAnimator.REVERSE); + mAnimator.setRepeatCount(1); + mAnimator.setDuration(mAnimationDurationMillis); + final ColorStateList originalColors = view.getTextColors(); + mAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + view.setTextColor(originalColors); + mAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + runnable.run(); + } + }); + mAnimator.start(); + } + + private ValueAnimator mAnimator; + + /** + * Returns {@code color} with alpha changed to {@code newAlpha} + */ + private static int colorWithAlpha(int color, int newAlpha) { + return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); + } }