Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3b2cf714 authored by Mark Punzalan's avatar Mark Punzalan Committed by Automerger Merge Worker
Browse files

Merge "Translate selectable TextViews by temporarily disabling selectable."...

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

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



Change-Id: I9a376d50c8c28df9bb6257d271aa4c0662f3665e
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 64e1182d 8f853345
Loading
Loading
Loading
Loading
+9 −0
Original line number Original line Diff line number Diff line
@@ -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.
+7 −16
Original line number Original line Diff line number Diff line
@@ -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
+69 −7
Original line number Original line Diff line number Diff line
@@ -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;
            }
            }