• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.animation.Animator;
20 import android.animation.ValueAnimator;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.res.ColorStateList;
24 import android.graphics.Color;
25 import android.text.TextUtils;
26 import android.text.method.TransformationMethod;
27 import android.text.method.TranslationTransformationMethod;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.translation.UiTranslationManager;
31 import android.view.translation.ViewTranslationCallback;
32 import android.view.translation.ViewTranslationRequest;
33 import android.view.translation.ViewTranslationResponse;
34 
35 /**
36  * Default implementation for {@link ViewTranslationCallback} for {@link TextView} components.
37  * This class handles how to display the translated information for {@link TextView}.
38  *
39  * @hide
40  */
41 public class TextViewTranslationCallback implements ViewTranslationCallback {
42 
43     private static final String TAG = "TextViewTranslationCb";
44 
45     private static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
46 
47     private TranslationTransformationMethod mTranslationTransformation;
48     private boolean mIsShowingTranslation = false;
49     private boolean mIsTextPaddingEnabled = false;
50     private CharSequence mPaddedText;
51     private int mAnimationDurationMillis = 250; // default value
52 
53     private CharSequence mContentDescription;
54 
clearTranslationTransformation()55     private void clearTranslationTransformation() {
56         if (DEBUG) {
57             Log.v(TAG, "clearTranslationTransformation: " + mTranslationTransformation);
58         }
59         mTranslationTransformation = null;
60     }
61 
62     /**
63      * {@inheritDoc}
64      */
65     @Override
onShowTranslation(@onNull View view)66     public boolean onShowTranslation(@NonNull View view) {
67         ViewTranslationResponse response = view.getViewTranslationResponse();
68         if (response == null) {
69             Log.e(TAG, "onShowTranslation() shouldn't be called before "
70                     + "onViewTranslationResponse().");
71             return false;
72         }
73         if (mTranslationTransformation == null) {
74             TransformationMethod originalTranslationMethod =
75                     ((TextView) view).getTransformationMethod();
76             mTranslationTransformation = new TranslationTransformationMethod(response,
77                     originalTranslationMethod);
78         }
79         final TransformationMethod transformation = mTranslationTransformation;
80         runWithAnimation(
81                 (TextView) view,
82                 () -> {
83                     mIsShowingTranslation = true;
84                     // TODO(b/178353965): well-handle setTransformationMethod.
85                     ((TextView) view).setTransformationMethod(transformation);
86                 });
87         if (response.getKeys().contains(ViewTranslationRequest.ID_CONTENT_DESCRIPTION)) {
88             CharSequence translatedContentDescription =
89                     response.getValue(ViewTranslationRequest.ID_CONTENT_DESCRIPTION).getText();
90             if (!TextUtils.isEmpty(translatedContentDescription)) {
91                 mContentDescription = view.getContentDescription();
92                 view.setContentDescription(translatedContentDescription);
93             }
94         }
95         return true;
96     }
97 
98     /**
99      * {@inheritDoc}
100      */
101     @Override
onHideTranslation(@onNull View view)102     public boolean onHideTranslation(@NonNull View view) {
103         if (view.getViewTranslationResponse() == null) {
104             Log.e(TAG, "onHideTranslation() shouldn't be called before "
105                     + "onViewTranslationResponse().");
106             return false;
107         }
108         // Restore to original text content.
109         if (mTranslationTransformation != null) {
110             final TransformationMethod transformation =
111                     mTranslationTransformation.getOriginalTransformationMethod();
112             runWithAnimation(
113                     (TextView) view,
114                     () -> {
115                         mIsShowingTranslation = false;
116                         ((TextView) view).setTransformationMethod(transformation);
117                     });
118             if (!TextUtils.isEmpty(mContentDescription)) {
119                 view.setContentDescription(mContentDescription);
120             }
121         } else {
122             if (DEBUG) {
123                 Log.w(TAG, "onHideTranslation(): no translated text.");
124             }
125             return false;
126         }
127         return true;
128     }
129 
130     /**
131      * {@inheritDoc}
132      */
133     @Override
onClearTranslation(@onNull View view)134     public boolean onClearTranslation(@NonNull View view) {
135         // Restore to original text content and clear TranslationTransformation
136         if (mTranslationTransformation != null) {
137             onHideTranslation(view);
138             clearTranslationTransformation();
139             mPaddedText = null;
140             mContentDescription = null;
141         } else {
142             if (DEBUG) {
143                 Log.w(TAG, "onClearTranslation(): no translated text.");
144             }
145             return false;
146         }
147         return true;
148     }
149 
isShowingTranslation()150     boolean isShowingTranslation() {
151         return mIsShowingTranslation;
152     }
153 
154     @Override
enableContentPadding()155     public void enableContentPadding() {
156         mIsTextPaddingEnabled = true;
157     }
158 
159     /**
160      * Returns whether readers of the view text should receive padded text for compatibility
161      * reasons. The view's original text will be padded to match the length of the translated text.
162      */
isTextPaddingEnabled()163     boolean isTextPaddingEnabled() {
164         return mIsTextPaddingEnabled;
165     }
166 
167     /**
168      * Returns the view's original text with padding added. If the translated text isn't longer than
169      * the original text, returns the original text itself.
170      *
171      * @param text the view's original text
172      * @param translatedText the view's translated text
173      * @see #isTextPaddingEnabled()
174      */
175     @Nullable
getPaddedText(CharSequence text, CharSequence translatedText)176     CharSequence getPaddedText(CharSequence text, CharSequence translatedText) {
177         if (text == null) {
178             return null;
179         }
180         if (mPaddedText == null) {
181             mPaddedText = computePaddedText(text, translatedText);
182         }
183         return mPaddedText;
184     }
185 
186     @NonNull
computePaddedText(CharSequence text, CharSequence translatedText)187     private CharSequence computePaddedText(CharSequence text, CharSequence translatedText) {
188         if (translatedText == null) {
189             return text;
190         }
191         int newLength = translatedText.length();
192         if (newLength <= text.length()) {
193             return text;
194         }
195         StringBuilder sb = new StringBuilder(newLength);
196         sb.append(text);
197         for (int i = text.length(); i < newLength; i++) {
198             sb.append(COMPAT_PAD_CHARACTER);
199         }
200         return sb;
201     }
202 
203     private static final char COMPAT_PAD_CHARACTER = '\u2002';
204 
205     @Override
setAnimationDurationMillis(int durationMillis)206     public void setAnimationDurationMillis(int durationMillis) {
207         mAnimationDurationMillis = durationMillis;
208     }
209 
210     /**
211      * Applies a simple text alpha animation when toggling between original and translated text. The
212      * text is fully faded out, then swapped to the new text, then the fading is reversed.
213      *
214      * @param runnable the operation to run on the view after the text is faded out, to change to
215      * displaying the original or translated text.
216      */
runWithAnimation(TextView view, Runnable runnable)217     private void runWithAnimation(TextView view, Runnable runnable) {
218         if (mAnimator != null) {
219             mAnimator.end();
220             // Note: mAnimator is now null; do not use again here.
221         }
222         int fadedOutColor = colorWithAlpha(view.getCurrentTextColor(), 0);
223         mAnimator = ValueAnimator.ofArgb(view.getCurrentTextColor(), fadedOutColor);
224         mAnimator.addUpdateListener(
225                 // Note that if the text has a ColorStateList, this replaces it with a single color
226                 // for all states. The original ColorStateList is restored when the animation ends
227                 // (see below).
228                 (valueAnimator) -> view.setTextColor((Integer) valueAnimator.getAnimatedValue()));
229         mAnimator.setRepeatMode(ValueAnimator.REVERSE);
230         mAnimator.setRepeatCount(1);
231         mAnimator.setDuration(mAnimationDurationMillis);
232         final ColorStateList originalColors = view.getTextColors();
233         mAnimator.addListener(new Animator.AnimatorListener() {
234             @Override
235             public void onAnimationStart(Animator animation) {
236             }
237 
238             @Override
239             public void onAnimationEnd(Animator animation) {
240                 view.setTextColor(originalColors);
241                 mAnimator = null;
242             }
243 
244             @Override
245             public void onAnimationCancel(Animator animation) {
246             }
247 
248             @Override
249             public void onAnimationRepeat(Animator animation) {
250                 runnable.run();
251             }
252         });
253         mAnimator.start();
254     }
255 
256     private ValueAnimator mAnimator;
257 
258     /**
259      * Returns {@code color} with alpha changed to {@code newAlpha}
260      */
colorWithAlpha(int color, int newAlpha)261     private static int colorWithAlpha(int color, int newAlpha) {
262         return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
263     }
264 }
265