File: TextViewTranslationCallback.java

package info (click to toggle)
android-platform-frameworks-base 1%3A14~beta1-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 326,084 kB
  • sloc: java: 2,032,373; xml: 343,016; cpp: 304,181; python: 3,683; ansic: 2,090; sh: 1,871; makefile: 120; sed: 19
file content (354 lines) | stat: -rw-r--r-- 14,754 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.widget;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.TextUtils;
import android.text.method.TransformationMethod;
import android.text.method.TranslationTransformationMethod;
import android.util.Log;
import android.view.View;
import android.view.translation.UiTranslationManager;
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}.
 *
 * @hide
 */
public class TextViewTranslationCallback implements ViewTranslationCallback {

    private static final String TAG = "TextViewTranslationCb";

    private static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);

    private TranslationTransformationMethod mTranslationTransformation;
    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

    private CharSequence mContentDescription;

    private void clearTranslationTransformation() {
        if (DEBUG) {
            Log.v(TAG, "clearTranslationTransformation: " + mTranslationTransformation);
        }
        mTranslationTransformation = null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onShowTranslation(@NonNull View view) {
        if (mIsShowingTranslation) {
            if (DEBUG) {
                Log.d(TAG, view + " is already showing translated text.");
            }
            return false;
        }
        ViewTranslationResponse response = view.getViewTranslationResponse();
        if (response == null) {
            Log.e(TAG, "onShowTranslation() shouldn't be called before "
                    + "onViewTranslationResponse().");
            return false;
        }
        // It is possible user changes text and new translation response returns, system should
        // 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 =
                    theTextView.getTransformationMethod();
            mTranslationTransformation = new TranslationTransformationMethod(response,
                    originalTranslationMethod);
        }
        final TransformationMethod transformation = mTranslationTransformation;
        WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
        runChangeTextWithAnimationIfNeeded(
                theTextView,
                () -> {
                    mIsShowingTranslation = true;
                    mAnimationRunning = false;

                    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 =
                    response.getValue(ViewTranslationRequest.ID_CONTENT_DESCRIPTION).getText();
            if (!TextUtils.isEmpty(translatedContentDescription)) {
                mContentDescription = view.getContentDescription();
                view.setContentDescription(translatedContentDescription);
            }
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onHideTranslation(@NonNull View view) {
        if (view.getViewTranslationResponse() == null) {
            Log.e(TAG, "onHideTranslation() shouldn't be called before "
                    + "onViewTranslationResponse().");
            return false;
        }
        // Restore to original text content.
        if (mTranslationTransformation != null) {
            final TransformationMethod transformation =
                    mTranslationTransformation.getOriginalTransformationMethod();
            TextView theTextView = (TextView) view;
            WeakReference<TextView> textViewRef = new WeakReference<>(theTextView);
            runChangeTextWithAnimationIfNeeded(
                    theTextView,
                    () -> {
                        mIsShowingTranslation = false;
                        mAnimationRunning = false;

                        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);
            }
        } else {
            if (DEBUG) {
                Log.w(TAG, "onHideTranslation(): no translated text.");
            }
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onClearTranslation(@NonNull View view) {
        // Restore to original text content and clear TranslationTransformation
        if (mTranslationTransformation != null) {
            onHideTranslation(view);
            clearTranslationTransformation();
            mPaddedText = null;
            mContentDescription = null;
        } else {
            if (DEBUG) {
                Log.w(TAG, "onClearTranslation(): no translated text.");
            }
            return false;
        }
        return true;
    }

    public boolean isShowingTranslation() {
        return mIsShowingTranslation;
    }

    /**
     * Returns whether the view is running animation to show or hide the translation.
     */
    public boolean isAnimationRunning() {
        return mAnimationRunning;
    }

    @Override
    public void enableContentPadding() {
        mIsTextPaddingEnabled = true;
    }

    /**
     * Returns whether readers of the view text should receive padded text for compatibility
     * reasons. The view's original text will be padded to match the length of the translated text.
     */
    boolean isTextPaddingEnabled() {
        return mIsTextPaddingEnabled;
    }

    /**
     * Returns the view's original text with padding added. If the translated text isn't longer than
     * the original text, returns the original text itself.
     *
     * @param text the view's original text
     * @param translatedText the view's translated text
     * @see #isTextPaddingEnabled()
     */
    @Nullable
    CharSequence getPaddedText(CharSequence text, CharSequence translatedText) {
        if (text == null) {
            return null;
        }
        if (mPaddedText == null) {
            mPaddedText = computePaddedText(text, translatedText);
        }
        return mPaddedText;
    }

    @NonNull
    private CharSequence computePaddedText(CharSequence text, CharSequence translatedText) {
        if (translatedText == null) {
            return text;
        }
        int newLength = translatedText.length();
        if (newLength <= text.length()) {
            return text;
        }
        StringBuilder sb = new StringBuilder(newLength);
        sb.append(text);
        for (int i = text.length(); i < newLength; i++) {
            sb.append(COMPAT_PAD_CHARACTER);
        }
        return sb;
    }

    private static final char COMPAT_PAD_CHARACTER = '\u2002';

    @Override
    public void setAnimationDurationMillis(int durationMillis) {
        mAnimationDurationMillis = durationMillis;
    }

    /**
     * Applies a simple text alpha animation when toggling between original and translated text. The
     * text is fully faded out, then swapped to the new text, then the fading is reversed.
     *
     * @param changeTextRunnable the operation to run on the view after the text is faded out, to
     * change to displaying the original or translated text.
     */
    private void runChangeTextWithAnimationIfNeeded(TextView view, Runnable changeTextRunnable) {
        boolean areAnimatorsEnabled = ValueAnimator.areAnimatorsEnabled();
        if (!areAnimatorsEnabled) {
            // The animation is disabled, just change display text
            changeTextRunnable.run();
            return;
        }
        if (mAnimator != null) {
            mAnimator.end();
            // Note: mAnimator is now null; do not use again here.
        }
        mAnimationRunning = true;
        int fadedOutColor = colorWithAlpha(view.getCurrentTextColor(), 0);
        mAnimator = ValueAnimator.ofArgb(view.getCurrentTextColor(), fadedOutColor);
        mAnimator.addUpdateListener(
                // Note that if the text has a ColorStateList, this replaces it with a single color
                // for all states. The original ColorStateList is restored when the animation ends
                // (see below).
                (valueAnimator) -> view.setTextColor((Integer) valueAnimator.getAnimatedValue()));
        mAnimator.setRepeatMode(ValueAnimator.REVERSE);
        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) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                TextView view = viewRef.get();
                if (view != null) {
                    view.setTextColor(originalColors);
                }
                mAnimator = null;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                changeTextRunnable.run();
            }
        });
        mAnimator.start();
    }

    private ValueAnimator mAnimator;

    /**
     * Returns {@code color} with alpha changed to {@code newAlpha}
     */
    private static int colorWithAlpha(int color, int newAlpha) {
        return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
    }
}