Loading core/java/android/view/translation/ViewTranslationCallback.java +9 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,15 @@ public interface ViewTranslationCallback { * the original text instead of the translated text or use a different approach to display the * 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. * * @return {@code true} if the View handles showing the translation. Loading core/java/android/widget/TextView.java +7 −16 Original line number Diff line number Diff line Loading @@ -14240,13 +14240,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Collects a {@link ViewTranslationRequest} which represents the content to be translated in * the view. * * <p>NOTE: When overriding the method, it should not translate the password. If the subclass * uses {@link TransformationMethod} to display the translated result, it's also not recommend * to translate text is selectable or editable. * <p>NOTE: When overriding the method, it should not collect a request to translate this * TextView if it is displaying a password. * * @param supportedFormats the supported translation format. The value could be {@link * 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 public void onCreateViewTranslationRequest(@NonNull int[] supportedFormats, Loading @@ -14268,18 +14268,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return; } boolean isPassword = isAnyPasswordInputType() || hasPasswordTransformationMethod(); // 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 // 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) { if (isTextEditable() || isPassword) { Log.w(LOG_TAG, "Cannot create translation request. editable = " + isTextEditable() + ", isPassword = " + isPassword + ", selectable = " + isTextSelectable()); } + isTextEditable() + ", isPassword = " + isPassword); return; } // TODO(b/176488462): apply the view's important for translation core/java/android/widget/TextViewTranslationCallback.java +69 −7 Original line number Diff line number Diff line Loading @@ -32,6 +32,8 @@ 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}. Loading @@ -48,6 +50,11 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { 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 Loading Loading @@ -81,21 +88,50 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { // 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 = ((TextView) view).getTransformationMethod(); theTextView.getTransformationMethod(); mTranslationTransformation = new TranslationTransformationMethod(response, originalTranslationMethod); } final TransformationMethod transformation = mTranslationTransformation; WeakReference<TextView> textViewRef = new WeakReference<>(theTextView); runChangeTextWithAnimationIfNeeded( (TextView) view, theTextView, () -> { mIsShowingTranslation = true; 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)) { CharSequence translatedContentDescription = Loading @@ -122,12 +158,34 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { if (mTranslationTransformation != null) { final TransformationMethod transformation = mTranslationTransformation.getOriginalTransformationMethod(); TextView theTextView = (TextView) view; WeakReference<TextView> textViewRef = new WeakReference<>(theTextView); runChangeTextWithAnimationIfNeeded( (TextView) view, theTextView, () -> { mIsShowingTranslation = 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)) { view.setContentDescription(mContentDescription); Loading Loading @@ -258,6 +316,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { 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) { Loading @@ -265,7 +324,10 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { @Override public void onAnimationEnd(Animator animation) { TextView view = viewRef.get(); if (view != null) { view.setTextColor(originalColors); } mAnimator = null; } Loading Loading
core/java/android/view/translation/ViewTranslationCallback.java +9 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,15 @@ public interface ViewTranslationCallback { * the original text instead of the translated text or use a different approach to display the * 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. * * @return {@code true} if the View handles showing the translation. Loading
core/java/android/widget/TextView.java +7 −16 Original line number Diff line number Diff line Loading @@ -14240,13 +14240,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Collects a {@link ViewTranslationRequest} which represents the content to be translated in * the view. * * <p>NOTE: When overriding the method, it should not translate the password. If the subclass * uses {@link TransformationMethod} to display the translated result, it's also not recommend * to translate text is selectable or editable. * <p>NOTE: When overriding the method, it should not collect a request to translate this * TextView if it is displaying a password. * * @param supportedFormats the supported translation format. The value could be {@link * 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 public void onCreateViewTranslationRequest(@NonNull int[] supportedFormats, Loading @@ -14268,18 +14268,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return; } boolean isPassword = isAnyPasswordInputType() || hasPasswordTransformationMethod(); // 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 // 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) { if (isTextEditable() || isPassword) { Log.w(LOG_TAG, "Cannot create translation request. editable = " + isTextEditable() + ", isPassword = " + isPassword + ", selectable = " + isTextSelectable()); } + isTextEditable() + ", isPassword = " + isPassword); return; } // TODO(b/176488462): apply the view's important for translation
core/java/android/widget/TextViewTranslationCallback.java +69 −7 Original line number Diff line number Diff line Loading @@ -32,6 +32,8 @@ 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}. Loading @@ -48,6 +50,11 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { 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 Loading Loading @@ -81,21 +88,50 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { // 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 = ((TextView) view).getTransformationMethod(); theTextView.getTransformationMethod(); mTranslationTransformation = new TranslationTransformationMethod(response, originalTranslationMethod); } final TransformationMethod transformation = mTranslationTransformation; WeakReference<TextView> textViewRef = new WeakReference<>(theTextView); runChangeTextWithAnimationIfNeeded( (TextView) view, theTextView, () -> { mIsShowingTranslation = true; 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)) { CharSequence translatedContentDescription = Loading @@ -122,12 +158,34 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { if (mTranslationTransformation != null) { final TransformationMethod transformation = mTranslationTransformation.getOriginalTransformationMethod(); TextView theTextView = (TextView) view; WeakReference<TextView> textViewRef = new WeakReference<>(theTextView); runChangeTextWithAnimationIfNeeded( (TextView) view, theTextView, () -> { mIsShowingTranslation = 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)) { view.setContentDescription(mContentDescription); Loading Loading @@ -258,6 +316,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { 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) { Loading @@ -265,7 +324,10 @@ public class TextViewTranslationCallback implements ViewTranslationCallback { @Override public void onAnimationEnd(Animator animation) { TextView view = viewRef.get(); if (view != null) { view.setTextColor(originalColors); } mAnimator = null; } Loading