[go: nahoru, domu]

Fix bug preventing clickable elements within a Spannable working.

Bug: 189985619
Test: ./gradlew	:wear:tiles:tiles-renderer:build
Relnote: "Fixed bug which prevented clickable element in a Spannable from
being clicked."

Change-Id: Iacc262c7ea07a42507171f42060319f204be537c
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
index b2a8c6c..869a322 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
@@ -40,6 +40,7 @@
 import android.text.Spanned;
 import android.text.TextPaint;
 import android.text.TextUtils.TruncateAt;
+import android.text.method.LinkMovementMethod;
 import android.text.style.AbsoluteSizeSpan;
 import android.text.style.ClickableSpan;
 import android.text.style.ForegroundColorSpan;
@@ -59,6 +60,7 @@
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
 import android.widget.LinearLayout;
+import android.widget.Scroller;
 import android.widget.Space;
 import android.widget.TextView;
 
@@ -1517,15 +1519,26 @@
 
         SpannableStringBuilder builder = new SpannableStringBuilder();
 
+        boolean isAnySpanClickable = false;
         for (Span element : spannable.getSpansList()) {
             switch (element.getInnerCase()) {
                 case IMAGE:
                     SpanImage protoImage = element.getImage();
                     builder = inflateImageInSpannable(builder, protoImage, tv);
+
+                    if (protoImage.getModifiers().hasClickable()) {
+                        isAnySpanClickable = true;
+                    }
+
                     break;
                 case TEXT:
                     SpanText protoText = element.getText();
                     builder = inflateTextInSpannable(builder, protoText);
+
+                    if (protoText.getModifiers().hasClickable()) {
+                        isAnySpanClickable = true;
+                    }
+
                     break;
                 default:
                     Log.w(TAG, "Unknown Span child type.");
@@ -1568,8 +1581,23 @@
 
         tv.setText(builder);
 
+        if (isAnySpanClickable) {
+            // For any ClickableSpans to work, the MovementMethod must be set to LinkMovementMethod.
+            tv.setMovementMethod(LinkMovementMethod.getInstance());
+
+            // Disable the highlight color; if we don't do this, the clicked span will get
+            // highlighted, which will be cleared half a second later if using LoadAction as the
+            // next layout will be delivered, which recreates the elements and clears the highlight.
+            tv.setHighlightColor(Color.TRANSPARENT);
+
+            // Use InhibitingScroller to prevent the text from scrolling when tapped. Setting a
+            // MovementMethod on a TextView (e.g. for clickables in a Spannable) then cause the
+            // TextView to be scrollable, and to jump to the end when tapped.
+            tv.setScroller(new InhibitingScroller(mUiContext));
+        }
+
         View wrappedView = applyModifiers(tv, spannable.getModifiers());
-        parent.addView(applyModifiers(tv, spannable.getModifiers()), layoutParams);
+        parent.addView(wrappedView, layoutParams);
 
         return wrappedView;
     }
@@ -1958,4 +1986,17 @@
             }
         }
     }
+
+    /** Implementation of {@link Scroller} which inhibits all scrolling. */
+    private static class InhibitingScroller extends Scroller {
+        InhibitingScroller(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void startScroll(int startX, int startY, int dx, int dy) {}
+
+        @Override
+        public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
+    }
 }
diff --git a/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java b/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
index 56d3e02..720732c 100644
--- a/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
+++ b/wear/tiles/tiles-renderer/src/test/java/androidx/wear/tiles/renderer/internal/TileRendererInternalTest.java
@@ -29,6 +29,8 @@
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
 import android.os.Looper;
+import android.os.SystemClock;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
@@ -84,6 +86,7 @@
 import androidx.wear.tiles.proto.ModifiersProto.Modifiers;
 import androidx.wear.tiles.proto.ModifiersProto.Padding;
 import androidx.wear.tiles.proto.ModifiersProto.Semantics;
+import androidx.wear.tiles.proto.ModifiersProto.SpanModifiers;
 import androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId;
 import androidx.wear.tiles.proto.ResourceProto.ImageResource;
 import androidx.wear.tiles.proto.ResourceProto.Resources;
@@ -103,6 +106,8 @@
 import org.robolectric.shadows.ShadowPackageManager;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 @RunWith(TilesTestRunner.class)
@@ -1137,6 +1142,60 @@
     }
 
     @Test
+    public void inflate_spannable_onClickCanFire() {
+        LayoutElement root = LayoutElement.newBuilder()
+                .setSpannable(Spannable.newBuilder()
+                        .addSpans(Span.newBuilder()
+                                .setText(SpanText.newBuilder()
+                                        .setText(StringProp.newBuilder()
+                                                .setValue("Hello World"))
+                                        .setModifiers(SpanModifiers.newBuilder()
+                                                .setClickable(Clickable.newBuilder()
+                                                        .setOnClick(Action.newBuilder()
+                                                                .setLoadAction(LoadAction
+                                                                        .getDefaultInstance())))))))
+                        .build();
+
+        List<Boolean> hasFiredList = new ArrayList<>();
+        FrameLayout rootLayout =
+                inflateProto(
+                        root,
+                        /* theme= */0,
+                        resourceResolvers(),
+                        p -> hasFiredList.add(true));
+
+        TextView tv = (TextView) rootLayout.getChildAt(0);
+
+        // Dispatch a click event to the first View; it should trigger the LoadAction...
+        long startTime = SystemClock.uptimeMillis();
+        MotionEvent evt =
+                MotionEvent.obtain(
+                        /* downTime= */ startTime,
+                        /* eventTime= */ startTime,
+                        MotionEvent.ACTION_DOWN,
+                        /* x= */ 5f,
+                        /* y= */ 5f,
+                        /* metaState= */ 0);
+        tv.dispatchTouchEvent(evt);
+        evt.recycle();
+
+        evt =
+                MotionEvent.obtain(
+                        /* downTime= */ startTime,
+                        /* eventTime= */ startTime + 100,
+                        MotionEvent.ACTION_UP,
+                        /* x= */ 5f,
+                        /* y= */ 5f,
+                        /* metaState= */ 0);
+        tv.dispatchTouchEvent(evt);
+        evt.recycle();
+
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(hasFiredList).hasSize(1);
+    }
+
+    @Test
     public void inflate_image_intrinsicSizeIsIgnored() {
         LayoutElement root = LayoutElement.newBuilder()
                 .setBox(Box.newBuilder()