Merge "Translate selectable TextViews by temporarily disabling selectable." into tm-dev am: 8bbc7f6554

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18490753

Change-Id: I466b01bb1782380098be540ae2b75402f5a5d6b7
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Mark Punzalan
2022-05-26 21:29:28 +00:00
committed by Automerger Merge Worker
3 changed files with 85 additions and 23 deletions

View File

@@ -52,6 +52,15 @@ public interface ViewTranslationCallback {
* the original text instead of the translated text or use a different approach to display the * the original text instead of the translated text or use a different approach to display the
* translated text. * translated text.
* *
* <p> NOTE: In Android version {@link android.os.Build.VERSION_CODES#TIRAMISU} and later,
* the implementation must be able to handle a selectable {@link android.widget.TextView}
* (i.e., {@link android.widget.TextView#isTextSelectable()} returns {@code true}. The default
* callback implementation for TextView uses a {@link android.text.method.TransformationMethod}
* to show the translated text, which will cause a crash when the translated text is selected.
* Therefore, the default callback temporarily makes the TextView non-selectable while the
* translation text is shown. This is one approach for handling selectable TextViews a
* TransformationMethod is used.
*
* See {@link View#onViewTranslationResponse} for how to get the translated information. * See {@link View#onViewTranslationResponse} for how to get the translated information.
* *
* @return {@code true} if the View handles showing the translation. * @return {@code true} if the View handles showing the translation.

View File

@@ -14240,13 +14240,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* Collects a {@link ViewTranslationRequest} which represents the content to be translated in * Collects a {@link ViewTranslationRequest} which represents the content to be translated in
* the view. * the view.
* *
* <p>NOTE: When overriding the method, it should not translate the password. If the subclass * <p>NOTE: When overriding the method, it should not collect a request to translate this
* uses {@link TransformationMethod} to display the translated result, it's also not recommend * TextView if it is displaying a password.
* to translate text is selectable or editable.
* *
* @param supportedFormats the supported translation format. The value could be {@link * @param supportedFormats the supported translation format. The value could be {@link
* android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}. * android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}.
* @return the {@link ViewTranslationRequest} which contains the information to be translated. * @param requestsCollector {@link Consumer} to receiver the {@link ViewTranslationRequest}
* which contains the information to be translated.
*/ */
@Override @Override
public void onCreateViewTranslationRequest(@NonNull int[] supportedFormats, public void onCreateViewTranslationRequest(@NonNull int[] supportedFormats,
@@ -14268,18 +14268,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return; return;
} }
boolean isPassword = isAnyPasswordInputType() || hasPasswordTransformationMethod(); boolean isPassword = isAnyPasswordInputType() || hasPasswordTransformationMethod();
// TODO(b/177214256): support selectable text translation. if (isTextEditable() || isPassword) {
// 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
// it, it needs broader changes to text APIs, we only allow to translate non selectable
// and editable text in S.
if (isTextEditable() || isPassword || isTextSelectable()) {
if (UiTranslationController.DEBUG) {
Log.w(LOG_TAG, "Cannot create translation request. editable = " Log.w(LOG_TAG, "Cannot create translation request. editable = "
+ isTextEditable() + ", isPassword = " + isPassword + ", selectable = " + isTextEditable() + ", isPassword = " + isPassword);
+ isTextSelectable());
}
return; return;
} }
// TODO(b/176488462): apply the view's important for translation // TODO(b/176488462): apply the view's important for translation

View File

@@ -32,6 +32,8 @@ import android.view.translation.ViewTranslationCallback;
import android.view.translation.ViewTranslationRequest; import android.view.translation.ViewTranslationRequest;
import android.view.translation.ViewTranslationResponse; import android.view.translation.ViewTranslationResponse;
import java.lang.ref.WeakReference;
/** /**
* Default implementation for {@link ViewTranslationCallback} for {@link TextView} components. * Default implementation for {@link ViewTranslationCallback} for {@link TextView} components.
* This class handles how to display the translated information for {@link TextView}. * This class handles how to display the translated information for {@link TextView}.
@@ -48,6 +50,11 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
private boolean mIsShowingTranslation = false; private boolean mIsShowingTranslation = false;
private boolean mAnimationRunning = false; private boolean mAnimationRunning = false;
private boolean mIsTextPaddingEnabled = 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 CharSequence mPaddedText;
private int mAnimationDurationMillis = 250; // default value private int mAnimationDurationMillis = 250; // default value
@@ -81,21 +88,50 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
// update the translation response to keep the result up to date. // update the translation response to keep the result up to date.
// Because TextView.setTransformationMethod() will skip the same TransformationMethod // Because TextView.setTransformationMethod() will skip the same TransformationMethod
// instance, we should create a new one to let new translation can work. // instance, we should create a new one to let new translation can work.
TextView theTextView = (TextView) view;
if (mTranslationTransformation == null if (mTranslationTransformation == null
|| !response.equals(mTranslationTransformation.getViewTranslationResponse())) { || !response.equals(mTranslationTransformation.getViewTranslationResponse())) {
TransformationMethod originalTranslationMethod = TransformationMethod originalTranslationMethod =
((TextView) view).getTransformationMethod(); theTextView.getTransformationMethod();
mTranslationTransformation = new TranslationTransformationMethod(response, mTranslationTransformation = new TranslationTransformationMethod(response,
originalTranslationMethod); originalTranslationMethod);
} }
final TransformationMethod transformation = mTranslationTransformation; final TransformationMethod transformation = mTranslationTransformation;
WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
runChangeTextWithAnimationIfNeeded( runChangeTextWithAnimationIfNeeded(
(TextView) view, theTextView,
() -> { () -> {
mIsShowingTranslation = true; mIsShowingTranslation = true;
mAnimationRunning = false; mAnimationRunning = false;
// TODO(b/178353965): well-handle setTransformationMethod.
((TextView) view).setTransformationMethod(transformation); 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)) { if (response.getKeys().contains(ViewTranslationRequest.ID_CONTENT_DESCRIPTION)) {
CharSequence translatedContentDescription = CharSequence translatedContentDescription =
@@ -122,12 +158,34 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
if (mTranslationTransformation != null) { if (mTranslationTransformation != null) {
final TransformationMethod transformation = final TransformationMethod transformation =
mTranslationTransformation.getOriginalTransformationMethod(); mTranslationTransformation.getOriginalTransformationMethod();
TextView theTextView = (TextView) view;
WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
runChangeTextWithAnimationIfNeeded( runChangeTextWithAnimationIfNeeded(
(TextView) view, theTextView,
() -> { () -> {
mIsShowingTranslation = false; mIsShowingTranslation = false;
mAnimationRunning = false; mAnimationRunning = false;
((TextView) view).setTransformationMethod(transformation);
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)) { if (!TextUtils.isEmpty(mContentDescription)) {
view.setContentDescription(mContentDescription); view.setContentDescription(mContentDescription);
@@ -258,6 +316,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
mAnimator.setRepeatCount(1); mAnimator.setRepeatCount(1);
mAnimator.setDuration(mAnimationDurationMillis); mAnimator.setDuration(mAnimationDurationMillis);
final ColorStateList originalColors = view.getTextColors(); final ColorStateList originalColors = view.getTextColors();
WeakReference<TextView> viewRef = new WeakReference<>(view);
mAnimator.addListener(new Animator.AnimatorListener() { mAnimator.addListener(new Animator.AnimatorListener() {
@Override @Override
public void onAnimationStart(Animator animation) { public void onAnimationStart(Animator animation) {
@@ -265,7 +324,10 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
TextView view = viewRef.get();
if (view != null) {
view.setTextColor(originalColors); view.setTextColor(originalColors);
}
mAnimator = null; mAnimator = null;
} }