diff options
Diffstat (limited to 'core/java/android/widget/TextViewTranslationCallback.java')
| -rw-r--r-- | core/java/android/widget/TextViewTranslationCallback.java | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java new file mode 100644 index 000000000000..9d60009031f9 --- /dev/null +++ b/core/java/android/widget/TextViewTranslationCallback.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2021 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.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.text.TextUtils; +import android.text.method.TransformationMethod; +import android.text.method.TranslationTransformationMethod; +import android.util.Log; +import android.view.View; +import android.view.translation.UiTranslationManager; +import android.view.translation.ViewTranslationCallback; +import android.view.translation.ViewTranslationRequest; +import android.view.translation.ViewTranslationResponse; + +/** + * Default implementation for {@link ViewTranslationCallback} for {@link TextView} components. + * This class handles how to display the translated information for {@link TextView}. + * + * @hide + */ +public class TextViewTranslationCallback implements ViewTranslationCallback { + + private static final String TAG = "TextViewTranslationCb"; + + private static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG); + + private TranslationTransformationMethod mTranslationTransformation; + private boolean mIsShowingTranslation = false; + private boolean mIsTextPaddingEnabled = false; + private CharSequence mPaddedText; + private int mAnimationDurationMillis = 250; // default value + + private CharSequence mContentDescription; + + private void clearTranslationTransformation() { + if (DEBUG) { + Log.v(TAG, "clearTranslationTransformation: " + mTranslationTransformation); + } + mTranslationTransformation = null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onShowTranslation(@NonNull View view) { + ViewTranslationResponse response = view.getViewTranslationResponse(); + if (response == null) { + Log.e(TAG, "onShowTranslation() shouldn't be called before " + + "onViewTranslationResponse()."); + return false; + } + if (mTranslationTransformation == null) { + TransformationMethod originalTranslationMethod = + ((TextView) view).getTransformationMethod(); + mTranslationTransformation = new TranslationTransformationMethod(response, + originalTranslationMethod); + } + final TransformationMethod transformation = mTranslationTransformation; + runWithAnimation( + (TextView) view, + () -> { + mIsShowingTranslation = true; + // TODO(b/178353965): well-handle setTransformationMethod. + ((TextView) view).setTransformationMethod(transformation); + }); + if (response.getKeys().contains(ViewTranslationRequest.ID_CONTENT_DESCRIPTION)) { + CharSequence translatedContentDescription = + response.getValue(ViewTranslationRequest.ID_CONTENT_DESCRIPTION).getText(); + if (!TextUtils.isEmpty(translatedContentDescription)) { + mContentDescription = view.getContentDescription(); + view.setContentDescription(translatedContentDescription); + } + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onHideTranslation(@NonNull View view) { + if (view.getViewTranslationResponse() == null) { + Log.e(TAG, "onHideTranslation() shouldn't be called before " + + "onViewTranslationResponse()."); + return false; + } + // Restore to original text content. + if (mTranslationTransformation != null) { + final TransformationMethod transformation = + mTranslationTransformation.getOriginalTransformationMethod(); + runWithAnimation( + (TextView) view, + () -> { + mIsShowingTranslation = false; + ((TextView) view).setTransformationMethod(transformation); + }); + if (!TextUtils.isEmpty(mContentDescription)) { + view.setContentDescription(mContentDescription); + } + } else { + if (DEBUG) { + Log.w(TAG, "onHideTranslation(): no translated text."); + } + return false; + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onClearTranslation(@NonNull View view) { + // Restore to original text content and clear TranslationTransformation + if (mTranslationTransformation != null) { + onHideTranslation(view); + clearTranslationTransformation(); + mPaddedText = null; + mContentDescription = null; + } else { + if (DEBUG) { + Log.w(TAG, "onClearTranslation(): no translated text."); + } + return false; + } + return true; + } + + boolean isShowingTranslation() { + return mIsShowingTranslation; + } + + @Override + public void enableContentPadding() { + mIsTextPaddingEnabled = true; + } + + /** + * Returns whether readers of the view text should receive padded text for compatibility + * reasons. The view's original text will be padded to match the length of the translated text. + */ + boolean isTextPaddingEnabled() { + return mIsTextPaddingEnabled; + } + + /** + * Returns the view's original text with padding added. If the translated text isn't longer than + * the original text, returns the original text itself. + * + * @param text the view's original text + * @param translatedText the view's translated text + * @see #isTextPaddingEnabled() + */ + @Nullable + CharSequence getPaddedText(CharSequence text, CharSequence translatedText) { + if (text == null) { + return null; + } + if (mPaddedText == null) { + mPaddedText = computePaddedText(text, translatedText); + } + return mPaddedText; + } + + @NonNull + private CharSequence computePaddedText(CharSequence text, CharSequence translatedText) { + if (translatedText == null) { + return text; + } + int newLength = translatedText.length(); + if (newLength <= text.length()) { + return text; + } + StringBuilder sb = new StringBuilder(newLength); + sb.append(text); + for (int i = text.length(); i < newLength; i++) { + sb.append(COMPAT_PAD_CHARACTER); + } + return sb; + } + + 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)); + } +} |
