| /* |
| * 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)); |
| } |
| } |