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
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index 95b3f68..bf2af51 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -404,9 +404,6 @@
// 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 @@
return;
}
}
+ callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS);
+ if (mViewsToPadContent.contains(autofillId)) {
+ callback.enableContentPadding();
+ }
view.onViewTranslationResponse(response);
callback.onShowTranslation(view);
});
@@ -422,6 +423,9 @@
}
}
+ // 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 1f0723b..6efd621 100644
--- a/core/java/android/view/translation/ViewTranslationCallback.java
+++ b/core/java/android/view/translation/ViewTranslationCallback.java
@@ -64,4 +64,12 @@
* @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 92c9142..a7d5ee4 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 @@
private boolean mIsShowingTranslation = false;
private boolean mIsTextPaddingEnabled = false;
private CharSequence mPaddedText;
+ private int mAnimationDurationMillis = 250; // default value
private CharSequence mContentDescription;
@@ -82,14 +88,19 @@
*/
@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 @@
*/
@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 @@
}
// 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 @@
}
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));
+ }
}