Merge "Translate selectable TextViews by temporarily disabling selectable." into tm-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
8bbc7f6554
@@ -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.
|
||||||
|
|||||||
@@ -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
|
Log.w(LOG_TAG, "Cannot create translation request. editable = "
|
||||||
// TextView does not support the text length change for TransformationMethod. If the
|
+ isTextEditable() + ", isPassword = " + isPassword);
|
||||||
// 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 = "
|
|
||||||
+ isTextEditable() + ", isPassword = " + isPassword + ", selectable = "
|
|
||||||
+ isTextSelectable());
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO(b/176488462): apply the view's important for translation
|
// TODO(b/176488462): apply the view's important for translation
|
||||||
|
|||||||
@@ -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) {
|
||||||
view.setTextColor(originalColors);
|
TextView view = viewRef.get();
|
||||||
|
if (view != null) {
|
||||||
|
view.setTextColor(originalColors);
|
||||||
|
}
|
||||||
mAnimator = null;
|
mAnimator = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user