AutoTranslate: Pad view text for compatibility reasons.
Some apps operate on the view text based on layout calculations made on the transformed text. This can cause issues (even crashes) if the transformed text is longer than the original, as is often the case with Translate. A previous temporary fix, I685ae4a9752c817db81f0d736f1d746b2e4a9839, ellipsized the translated text. This change fixes it properly by instead padding the original text. A followup change will make this controllable by API instead of padding every TextView. The en space character, U+2002, is used for the padding here. It seems to work well in practice for multiple languages. We don't make this configurable through API as it could make debugging harder (issues triggered by server-side changes). Bug: 179693024 Test: atest CtsTranslationTestCases Test: manual - toggling between original and translated, scrolling, new views appearing, multiple apps Change-Id: I9d7db5d2bc200e4042baf3267796f00cfc298f19
This commit is contained in:
@@ -78,24 +78,11 @@ public class TranslationTransformationMethod implements TransformationMethod2 {
|
||||
if (TextUtils.isEmpty(translatedText) || isWhitespace(translatedText.toString())) {
|
||||
return source;
|
||||
} else {
|
||||
// TODO(b/179693024): Remove this once we have the fix to pad the view text instead.
|
||||
translatedText = ellipsize(translatedText, ((TextView) view).getText().length());
|
||||
// TODO(b/174283799): apply the spans to the text
|
||||
return translatedText;
|
||||
}
|
||||
}
|
||||
|
||||
private static CharSequence ellipsize(CharSequence text, int newLength) {
|
||||
if (text.length() <= newLength) {
|
||||
return text;
|
||||
}
|
||||
String ellipsis = String.valueOf('\u2026');
|
||||
if (newLength == 1) {
|
||||
return ellipsis;
|
||||
}
|
||||
return TextUtils.concat(TextUtils.trimToSize(text, newLength - 1), ellipsis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChanged(View view, CharSequence sourceText,
|
||||
boolean focused, int direction,
|
||||
|
||||
@@ -362,17 +362,20 @@ public class UiTranslationController {
|
||||
continue;
|
||||
}
|
||||
mActivity.runOnUiThread(() -> {
|
||||
if (view.getViewTranslationCallback() == null) {
|
||||
final ViewTranslationCallback callback = view.getViewTranslationCallback();
|
||||
if (callback == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, view + " doesn't support showing translation because of "
|
||||
+ "null ViewTranslationCallback.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Do this for specific views on request only.
|
||||
callback.enableContentPadding();
|
||||
|
||||
view.onTranslationResponse(response);
|
||||
if (view.getViewTranslationCallback() != null) {
|
||||
view.getViewTranslationCallback().onShowTranslation(view);
|
||||
}
|
||||
callback.onShowTranslation(view);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,16 @@ public interface ViewTranslationCallback {
|
||||
* @return {@code true} if the View handles clearing the translation.
|
||||
*/
|
||||
boolean onClearTranslation(@NonNull View view);
|
||||
|
||||
/**
|
||||
* Enables padding on the view's original content.
|
||||
* <p>
|
||||
* This is useful when we do not modify the content directly, rather use a mechanism like
|
||||
* {@link android.text.method.TransformationMethod}. If the app misbehaves when the displayed
|
||||
* translation and the underlying content have different sizes, the platform intelligence can
|
||||
* request that the original content be padded to make the sizes match.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
default void enableContentPadding() {}
|
||||
}
|
||||
|
||||
@@ -770,6 +770,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
private final boolean mUseInternationalizedInput;
|
||||
// True if fallback fonts that end up getting used should be allowed to affect line spacing.
|
||||
/* package */ boolean mUseFallbackLineSpacing;
|
||||
// True if the view text can be padded for compat reasons, when the view is translated.
|
||||
private final boolean mUseTextPaddingForUiTranslation;
|
||||
|
||||
@ViewDebug.ExportedProperty(category = "text")
|
||||
@UnsupportedAppUsage
|
||||
@@ -1480,6 +1482,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
|
||||
mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O;
|
||||
mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.P;
|
||||
// TODO(b/179693024): Use a ChangeId instead.
|
||||
mUseTextPaddingForUiTranslation = targetSdkVersion <= Build.VERSION_CODES.R;
|
||||
|
||||
if (inputMethod != null) {
|
||||
Class<?> c;
|
||||
@@ -2372,6 +2376,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
@ViewDebug.CapturedViewProperty
|
||||
@InspectableProperty
|
||||
public CharSequence getText() {
|
||||
if (mUseTextPaddingForUiTranslation
|
||||
&& mDefaultTranslationCallback != null
|
||||
&& mDefaultTranslationCallback.isTextPaddingEnabled()
|
||||
&& mDefaultTranslationCallback.isShowingTranslation()) {
|
||||
return mDefaultTranslationCallback.getPaddedText(mText, mTransformed);
|
||||
}
|
||||
return mText;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package android.widget;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Build;
|
||||
import android.text.method.TranslationTransformationMethod;
|
||||
import android.util.Log;
|
||||
@@ -33,7 +34,7 @@ import android.view.translation.ViewTranslationResponse;
|
||||
*/
|
||||
public class TextViewTranslationCallback implements ViewTranslationCallback {
|
||||
|
||||
private static final String TAG = "TextViewTranslationCallback";
|
||||
private static final String TAG = "TextViewTranslationCb";
|
||||
|
||||
// TODO(b/182433547): remove Build.IS_DEBUGGABLE before ship. Enable the logging in debug build
|
||||
// to help the debug during the development phase
|
||||
@@ -41,6 +42,9 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
|
||||
|| Build.IS_DEBUGGABLE;
|
||||
|
||||
private TranslationTransformationMethod mTranslationTransformation;
|
||||
private boolean mIsShowingTranslation = false;
|
||||
private boolean mIsTextPaddingEnabled = false;
|
||||
private CharSequence mPaddedText;
|
||||
|
||||
/**
|
||||
* Invoked by the platform when receiving the successful {@link ViewTranslationResponse} for the
|
||||
@@ -74,6 +78,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
|
||||
*/
|
||||
@Override
|
||||
public boolean onShowTranslation(@NonNull View view) {
|
||||
mIsShowingTranslation = true;
|
||||
if (mTranslationTransformation != null) {
|
||||
((TextView) view).setTransformationMethod(mTranslationTransformation);
|
||||
} else {
|
||||
@@ -90,6 +95,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
|
||||
*/
|
||||
@Override
|
||||
public boolean onHideTranslation(@NonNull View view) {
|
||||
mIsShowingTranslation = false;
|
||||
// Restore to original text content.
|
||||
if (mTranslationTransformation != null) {
|
||||
((TextView) view).setTransformationMethod(
|
||||
@@ -110,9 +116,9 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
|
||||
public boolean onClearTranslation(@NonNull View view) {
|
||||
// Restore to original text content and clear TranslationTransformation
|
||||
if (mTranslationTransformation != null) {
|
||||
((TextView) view).setTransformationMethod(
|
||||
mTranslationTransformation.getOriginalTransformationMethod());
|
||||
onHideTranslation(view);
|
||||
clearTranslationTransformation();
|
||||
mPaddedText = null;
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
// TODO(b/182433547): remove before S release
|
||||
@@ -121,4 +127,59 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isShowingTranslation() {
|
||||
return mIsShowingTranslation;
|
||||
}
|
||||
|
||||
@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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user