script-astra/Android/Sdk/sources/android-35/android/widget/TextViewTranslationCallback.java

355 lines
14 KiB
Java
Raw Normal View History

2025-01-20 15:15:20 +00:00
/*
* 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;
import java.lang.ref.WeakReference;
/**
* 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 mAnimationRunning = false;
private boolean mIsTextPaddingEnabled = false;
private boolean mOriginalIsTextSelectable = false;
private int mOriginalFocusable = 0;
private boolean mOriginalFocusableInTouchMode = false;
private boolean mOriginalClickable = false;
private boolean mOriginalLongClickable = 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) {
if (mIsShowingTranslation) {
if (DEBUG) {
Log.d(TAG, view + " is already showing translated text.");
}
return false;
}
ViewTranslationResponse response = view.getViewTranslationResponse();
if (response == null) {
Log.e(TAG, "onShowTranslation() shouldn't be called before "
+ "onViewTranslationResponse().");
return false;
}
// It is possible user changes text and new translation response returns, system should
// update the translation response to keep the result up to date.
// Because TextView.setTransformationMethod() will skip the same TransformationMethod
// instance, we should create a new one to let new translation can work.
TextView theTextView = (TextView) view;
if (mTranslationTransformation == null
|| !response.equals(mTranslationTransformation.getViewTranslationResponse())) {
TransformationMethod originalTranslationMethod =
theTextView.getTransformationMethod();
mTranslationTransformation = new TranslationTransformationMethod(response,
originalTranslationMethod);
}
final TransformationMethod transformation = mTranslationTransformation;
WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
runChangeTextWithAnimationIfNeeded(
theTextView,
() -> {
mIsShowingTranslation = true;
mAnimationRunning = false;
TextView textView = textViewRef.get();
if (textView == null) {
return;
}
// TODO(b/177214256): support selectable text translation.
// We use the TransformationMethod to implement showing the translated text. The
// TextView does not support the text length change for TransformationMethod.
// If the text is selectable or editable, it will crash while selecting the
// text. To support being able to select translated text, we need broader
// changes to text APIs. For now, the callback makes the text non-selectable
// while translated, and makes it selectable again after translation.
mOriginalIsTextSelectable = textView.isTextSelectable();
if (mOriginalIsTextSelectable) {
// According to documentation for `setTextIsSelectable()`, it sets the
// flags focusable, focusableInTouchMode, clickable, and longClickable
// to the same value. We get the original values to restore when translation
// is hidden.
mOriginalFocusableInTouchMode = textView.isFocusableInTouchMode();
mOriginalFocusable = textView.getFocusable();
mOriginalClickable = textView.isClickable();
mOriginalLongClickable = textView.isLongClickable();
textView.setTextIsSelectable(false);
}
// TODO(b/233406028): We should NOT restore the original
// TransformationMethod and selectable state if it was changed WHILE
// translation was being shown.
textView.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();
TextView theTextView = (TextView) view;
WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
runChangeTextWithAnimationIfNeeded(
theTextView,
() -> {
mIsShowingTranslation = false;
mAnimationRunning = false;
TextView textView = textViewRef.get();
if (textView == null) {
return;
}
// TODO(b/233406028): We should NOT restore the original
// TransformationMethod and selectable state if it was changed WHILE
// translation was being shown.
textView.setTransformationMethod(transformation);
if (mOriginalIsTextSelectable && !textView.isTextSelectable()) {
// According to documentation for `setTextIsSelectable()`, it sets the
// flags focusable, focusableInTouchMode, clickable, and longClickable
// to the same value, and you must call `setFocusable()`, etc. to
// restore all previous flag values.
textView.setTextIsSelectable(true);
textView.setFocusableInTouchMode(mOriginalFocusableInTouchMode);
textView.setFocusable(mOriginalFocusable);
textView.setClickable(mOriginalClickable);
textView.setLongClickable(mOriginalLongClickable);
}
});
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;
}
public boolean isShowingTranslation() {
return mIsShowingTranslation;
}
/**
* Returns whether the view is running animation to show or hide the translation.
*/
public boolean isAnimationRunning() {
return mAnimationRunning;
}
@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 changeTextRunnable the operation to run on the view after the text is faded out, to
* change to displaying the original or translated text.
*/
private void runChangeTextWithAnimationIfNeeded(TextView view, Runnable changeTextRunnable) {
boolean areAnimatorsEnabled = ValueAnimator.areAnimatorsEnabled();
if (!areAnimatorsEnabled) {
// The animation is disabled, just change display text
changeTextRunnable.run();
return;
}
if (mAnimator != null) {
mAnimator.end();
// Note: mAnimator is now null; do not use again here.
}
mAnimationRunning = true;
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();
WeakReference<TextView> viewRef = new WeakReference<>(view);
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
TextView view = viewRef.get();
if (view != null) {
view.setTextColor(originalColors);
}
mAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
changeTextRunnable.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));
}
}