diff options
3 files changed, 104 insertions, 8 deletions
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java index 4b2c34365589..bf2af518969b 100644 --- a/core/java/android/view/translation/UiTranslationController.java +++ b/core/java/android/view/translation/UiTranslationController.java @@ -389,6 +389,14 @@ public class UiTranslationController { continue; } mActivity.runOnUiThread(() -> { + if (view.getViewTranslationResponse() != null + && view.getViewTranslationResponse().equals(response)) { + if (DEBUG) { + Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId + + ". Ignoring."); + } + return; + } ViewTranslationCallback callback = view.getViewTranslationCallback(); if (callback == null) { if (view instanceof TextView) { @@ -396,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 " @@ -407,6 +412,10 @@ public class UiTranslationController { return; } } + callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS); + if (mViewsToPadContent.contains(autofillId)) { + callback.enableContentPadding(); + } view.onViewTranslationResponse(response); callback.onShowTranslation(view); }); @@ -414,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 1f0723b0bd05..6efd621c4caa 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 92c9142749df..a7d5ee465299 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)); + } } |