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

Commit 800d202d 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

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



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

@@ -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 =
@@ -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);
@@ -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) {
@@ -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;
            }