Niedzielski has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/250448

Change subject: Add pronunciation span click listener logic
......................................................................

Add pronunciation span click listener logic

Add logic for responding to click events in spans without obscuring
events out of span bounds. The audio player portion will come in a later
patch so showing pronunciations is always disabled in this patch.

Bug: T114524
Change-Id: Icfb00a4f0453a10fc3b492f777f5d9e52404f0dd
---
A app/src/main/java/org/wikipedia/drawable/CircularProgressDrawable.java
A app/src/main/java/org/wikipedia/media/AvPlayer.java
A app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
A app/src/main/java/org/wikipedia/richtext/AnimatedImageSpan.java
A app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
A app/src/main/java/org/wikipedia/richtext/ClickSpan.java
A app/src/main/java/org/wikipedia/richtext/DrawableSpan.java
D app/src/main/java/org/wikipedia/richtext/IntrinsicImageSpan.java
D app/src/main/java/org/wikipedia/richtext/PronunciationSpan.java
A app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
M app/src/main/java/org/wikipedia/util/ApiUtil.java
A app/src/main/java/org/wikipedia/views/AlienDrawableCallback.java
M app/src/main/java/org/wikipedia/views/AppTextView.java
M app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
M app/src/main/res/values/dimens.xml
15 files changed, 765 insertions(+), 125 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/48/250448/1

diff --git 
a/app/src/main/java/org/wikipedia/drawable/CircularProgressDrawable.java 
b/app/src/main/java/org/wikipedia/drawable/CircularProgressDrawable.java
new file mode 100644
index 0000000..d795f3f
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/drawable/CircularProgressDrawable.java
@@ -0,0 +1,187 @@
+package org.wikipedia.drawable;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AnimationDrawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.util.Property;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+// https://gist.github.com/castorflex/4e46a9dc2c3a4245a28e
+public class CircularProgressDrawable extends AnimationDrawable {
+    @NonNull private static final Interpolator ANGLE_INTERPOLATOR = new 
LinearInterpolator();
+    @NonNull private static final Interpolator SWEEP_INTERPOLATOR = new 
DecelerateInterpolator();
+    private static final int ANGLE_ANIMATOR_DURATION = 2000;
+    private static final int SWEEP_ANIMATOR_DURATION = 600;
+    private static final int MIN_SWEEP_ANGLE = 30;
+    private static final int MAX_SWEEP_ANGLE = 360;
+    @NonNull private final RectF fBounds = new RectF();
+
+    private ObjectAnimator objectAnimatorSweep;
+    private ObjectAnimator objectAnimatorAngle;
+    private boolean modeAppearing;
+    private Paint paint;
+    private float currentGlobalAngleOffset;
+    private float currentGlobalAngle;
+    private float currentSweepAngle;
+    private float borderWidth;
+
+    public CircularProgressDrawable(@ColorInt int color, float borderWidth, 
int radius) {
+        this(color, borderWidth);
+        setBounds(0, 0, radius * 2, radius * 2);
+        start();
+    }
+
+    public CircularProgressDrawable(@ColorInt int color, float borderWidth) {
+        this.borderWidth = borderWidth;
+
+        paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(borderWidth);
+        paint.setColor(color);
+
+        setupAnimations();
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        float startAngle = currentGlobalAngle - currentGlobalAngleOffset;
+        float sweepAngle = currentSweepAngle;
+        if (!modeAppearing) {
+            startAngle = startAngle + sweepAngle;
+            sweepAngle = MAX_SWEEP_ANGLE - sweepAngle - MIN_SWEEP_ANGLE;
+        } else {
+            sweepAngle += MIN_SWEEP_ANGLE;
+        }
+        canvas.drawArc(fBounds, startAngle, sweepAngle, false, paint);
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        paint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        paint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSPARENT;
+    }
+
+    @Override
+    public void start() {
+        super.start();
+        if (isRunning()) {
+            return;
+        }
+        objectAnimatorAngle.start();
+        objectAnimatorSweep.start();
+        invalidateSelf();
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        if (!isRunning()) {
+            return;
+        }
+        objectAnimatorAngle.cancel();
+        objectAnimatorSweep.cancel();
+        invalidateSelf();
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        fBounds.left = bounds.left + borderWidth / 2f + 1 / 2f;
+        fBounds.right = bounds.right - borderWidth / 2f - 1 / 2f;
+        fBounds.top = bounds.top + borderWidth / 2f + 1 / 2f;
+        fBounds.bottom = bounds.bottom - borderWidth / 2f - 1 / 2f;
+    }
+
+    public void setCurrentGlobalAngle(float currentGlobalAngle) {
+        this.currentGlobalAngle = currentGlobalAngle;
+        invalidateSelf();
+    }
+
+    public float getCurrentGlobalAngle() {
+        return currentGlobalAngle;
+    }
+
+    public void setCurrentSweepAngle(float currentSweepAngle) {
+        this.currentSweepAngle = currentSweepAngle;
+        invalidateSelf();
+    }
+
+    public float getCurrentSweepAngle() {
+        return currentSweepAngle;
+    }
+
+    private void toggleAppearingMode() {
+        modeAppearing = !modeAppearing;
+        if (modeAppearing) {
+            currentGlobalAngleOffset = (currentGlobalAngleOffset + 
MIN_SWEEP_ANGLE * 2) % MAX_SWEEP_ANGLE;
+        }
+    }
+
+    private Property<CircularProgressDrawable, Float> mAngleProperty
+            = new Property<CircularProgressDrawable, Float>(Float.class, 
"angle") {
+        @Override
+        public Float get(CircularProgressDrawable object) {
+            return object.getCurrentGlobalAngle();
+        }
+
+        @Override
+        public void set(CircularProgressDrawable object, Float value) {
+            object.setCurrentGlobalAngle(value);
+        }
+    };
+
+    private Property<CircularProgressDrawable, Float> mSweepProperty
+            = new Property<CircularProgressDrawable, Float>(Float.class, 
"arc") {
+        @Override
+        public Float get(CircularProgressDrawable object) {
+            return object.getCurrentSweepAngle();
+        }
+
+        @Override
+        public void set(CircularProgressDrawable object, Float value) {
+            object.setCurrentSweepAngle(value);
+        }
+    };
+
+    private void setupAnimations() {
+        objectAnimatorAngle = ObjectAnimator.ofFloat(this, mAngleProperty, 
MAX_SWEEP_ANGLE);
+        objectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
+        objectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
+        objectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
+        objectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);
+
+        objectAnimatorSweep = ObjectAnimator.ofFloat(this, mSweepProperty, 
MAX_SWEEP_ANGLE - MIN_SWEEP_ANGLE * 2);
+        objectAnimatorSweep.setInterpolator(SWEEP_INTERPOLATOR);
+        objectAnimatorSweep.setDuration(SWEEP_ANIMATOR_DURATION);
+        objectAnimatorSweep.setRepeatMode(ValueAnimator.RESTART);
+        objectAnimatorSweep.setRepeatCount(ValueAnimator.INFINITE);
+        objectAnimatorSweep.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+                toggleAppearingMode();
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/media/AvPlayer.java 
b/app/src/main/java/org/wikipedia/media/AvPlayer.java
new file mode 100644
index 0000000..01107d1
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/media/AvPlayer.java
@@ -0,0 +1,11 @@
+package org.wikipedia.media;
+
+public interface AvPlayer {
+    void init();
+    void deinit();
+    void load(String path);
+    void play();
+    void togglePlayback();
+    void stop();
+    void seek(int millis);
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java 
b/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
new file mode 100644
index 0000000..5514a53
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/media/DefaultAvPlayer.java
@@ -0,0 +1,36 @@
+package org.wikipedia.media;
+
+public class DefaultAvPlayer implements AvPlayer {
+    @Override
+    public void init() {
+    }
+
+    @Override
+    public void deinit() {
+    }
+
+    @Override
+    public void load(String path) {
+
+    }
+
+    @Override
+    public void play() {
+
+    }
+
+    @Override
+    public void togglePlayback() {
+
+    }
+
+    @Override
+    public void stop() {
+
+    }
+
+    @Override
+    public void seek(int millis) {
+
+    }
+}
diff --git a/app/src/main/java/org/wikipedia/richtext/AnimatedImageSpan.java 
b/app/src/main/java/org/wikipedia/richtext/AnimatedImageSpan.java
new file mode 100644
index 0000000..6acdd1c
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/richtext/AnimatedImageSpan.java
@@ -0,0 +1,126 @@
+package org.wikipedia.richtext;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import org.wikipedia.views.AlienDrawableCallback;
+
+public class AnimatedImageSpan extends DrawableSpan {
+    private Drawable.Callback animateCallback;
+
+    public AnimatedImageSpan(@NonNull View view, Bitmap bitmap) {
+        super(view.getContext(), bitmap);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Bitmap bitmap, int 
verticalAlignment) {
+        super(view.getContext(), bitmap, verticalAlignment);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Drawable drawable) {
+        super(drawable);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Drawable drawable, int 
verticalAlignment) {
+        super(drawable, verticalAlignment);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Drawable drawable, String 
source) {
+        super(drawable, source);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Drawable drawable, String 
source, int verticalAlignment) {
+        super(drawable, source, verticalAlignment);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Uri uri) {
+        super(view.getContext(), uri);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, Uri uri, int 
verticalAlignment) {
+        super(view.getContext(), uri, verticalAlignment);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, @DrawableRes int resourceId) {
+        super(view.getContext(), resourceId);
+        init(view);
+    }
+
+    public AnimatedImageSpan(@NonNull View view, @DrawableRes int resourceId, 
int verticalAlignment) {
+        super(view.getContext(), resourceId, verticalAlignment);
+        init(view);
+    }
+
+    public void start() {
+        AnimationDrawable drawable = getAnimationDrawable();
+        if (drawable != null) {
+            drawable.start();
+        }
+    }
+
+    public void stop() {
+        AnimationDrawable drawable = getAnimationDrawable();
+        if (drawable != null) {
+            drawable.stop();
+        }
+    }
+
+    public void toggle() {
+        if (isRunning()) {
+            stop();
+        } else {
+            start();
+        }
+    }
+
+    public boolean isRunning() {
+        AnimationDrawable drawable = getAnimationDrawable();
+        return drawable != null && drawable.isRunning();
+    }
+
+    @Override
+    public void setDrawable(@Nullable Drawable drawable) {
+        clearCallback(drawable);
+        super.setDrawable(drawable);
+        setCallback(drawable);
+    }
+
+    @Nullable
+    protected AnimationDrawable getAnimationDrawable() {
+        return getDrawable() instanceof AnimationDrawable
+                ? (AnimationDrawable) getDrawable()
+                : null;
+    }
+
+    private void init(@NonNull View view) {
+        // Drawable.setCallback() keeps a weak reference so hold a strong 
reference here.
+        animateCallback = new AlienDrawableCallback(view);
+        setCallback(getDrawable());
+    }
+
+    private void setCallback(@Nullable Drawable drawable) {
+        if (drawable != null) {
+            drawable.setCallback(animateCallback);
+        }
+    }
+
+    private void clearCallback(@Nullable Drawable drawable) {
+        if (drawable != null) {
+            stop();
+            drawable.setCallback(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java 
b/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
new file mode 100644
index 0000000..317cb58
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/richtext/AudioUrlSpan.java
@@ -0,0 +1,114 @@
+package org.wikipedia.richtext;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LevelListDrawable;
+import android.support.annotation.DimenRes;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.widget.TextView;
+
+import org.wikipedia.R;
+import org.wikipedia.drawable.CircularProgressDrawable;
+import org.wikipedia.drawable.DrawableUtil;
+import org.wikipedia.media.AvPlayer;
+
+public class AudioUrlSpan extends AnimatedImageSpan implements ClickSpan {
+    private static final int STOP_ICON_LEVEL = 0;
+    private static final int PLAY_ICON_LEVEL = 1;
+
+    @NonNull
+    private final AvPlayer player;
+
+    public AudioUrlSpan(@NonNull View view, @NonNull AvPlayer player) {
+        super(view, drawable(view.getContext()));
+        this.player = player;
+        view.addOnAttachStateChangeListener(new ViewAttachListener());
+    }
+
+    @Override
+    public void onClick(TextView textView) {
+        toggle();
+    }
+
+    @Override
+    public void start() {
+        showIcon(PLAY_ICON_LEVEL);
+        super.start();
+        player.play();
+    }
+
+    @Override
+    public void stop() {
+        showIcon(STOP_ICON_LEVEL);
+        super.stop();
+        player.stop();
+
+    }
+
+    @Override
+    public void toggle() {
+        super.toggle();
+        player.togglePlayback();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return getIconShown() == PLAY_ICON_LEVEL;
+    }
+
+    @NonNull
+    @Override
+    public Drawable getDrawable() {
+        //noinspection ConstantConditions
+        return super.getDrawable();
+    }
+
+    private void showIcon(int level) {
+        getDrawable().setLevel(level);
+    }
+
+    private int getIconShown() {
+        return getDrawable().getLevel();
+    }
+
+    private static Drawable drawable(Context context) {
+        LevelListDrawable levels = new LevelListDrawable();
+        levels.addLevel(STOP_ICON_LEVEL, STOP_ICON_LEVEL, 
speakerDrawable(context));
+        levels.addLevel(PLAY_ICON_LEVEL, PLAY_ICON_LEVEL, 
spinnerDrawable(context));
+        DrawableUtil.setTint(levels, Color.WHITE);
+        return levels;
+    }
+
+    private static Drawable speakerDrawable(Context context) {
+        return getDrawable(context, R.drawable.ic_volume_up_black_24dp);
+    }
+
+    private static Drawable spinnerDrawable(Context context) {
+        return new CircularProgressDrawable(Color.WHITE,
+                getDimensionPixelSize(context, 
R.dimen.audio_url_span_loading_spinner_border_thickness),
+                getDimensionPixelSize(context, 
R.dimen.audio_url_span_loading_spinner_radius));
+    }
+
+    private static Drawable getDrawable(Context context, @DrawableRes int id) {
+        return context.getResources().getDrawable(id);
+    }
+
+    private static int getDimensionPixelSize(Context context, @DimenRes int 
id) {
+        return context.getResources().getDimensionPixelSize(id);
+    }
+
+    private class ViewAttachListener implements 
View.OnAttachStateChangeListener {
+        @Override
+        public void onViewAttachedToWindow(View view) {
+            player.init();
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {
+            player.deinit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/richtext/ClickSpan.java 
b/app/src/main/java/org/wikipedia/richtext/ClickSpan.java
new file mode 100644
index 0000000..e47cbe4
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/richtext/ClickSpan.java
@@ -0,0 +1,7 @@
+package org.wikipedia.richtext;
+
+import android.widget.TextView;
+
+public interface ClickSpan {
+    void onClick(TextView textView);
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/richtext/DrawableSpan.java 
b/app/src/main/java/org/wikipedia/richtext/DrawableSpan.java
new file mode 100644
index 0000000..9fc8ac1
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/richtext/DrawableSpan.java
@@ -0,0 +1,95 @@
+package org.wikipedia.richtext;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.style.ImageSpan;
+
+/** A more mutable ImageSpan better suited to Drawables. */
+public class DrawableSpan extends ImageSpan {
+    @Nullable
+    private Drawable drawable;
+
+    public DrawableSpan(@NonNull Context context, Bitmap bitmap) {
+        super(context, bitmap);
+        init();
+    }
+
+    public DrawableSpan(@NonNull Context context, Bitmap bitmap, int 
verticalAlignment) {
+        super(context, bitmap, verticalAlignment);
+        init();
+    }
+
+    public DrawableSpan(Drawable drawable) {
+        super(drawable);
+        init();
+    }
+
+    public DrawableSpan(Drawable drawable, int verticalAlignment) {
+        super(drawable, verticalAlignment);
+        init();
+    }
+
+    public DrawableSpan(Drawable drawable, String source) {
+        super(drawable, source);
+        init();
+    }
+
+    public DrawableSpan(Drawable drawable, String source, int 
verticalAlignment) {
+        super(drawable, source, verticalAlignment);
+        init();
+    }
+
+    public DrawableSpan(@NonNull Context context, Uri uri) {
+        super(context, uri);
+        init();
+    }
+
+    public DrawableSpan(@NonNull Context context, Uri uri, int 
verticalAlignment) {
+        super(context, uri, verticalAlignment);
+        init();
+    }
+
+    public DrawableSpan(@NonNull Context context, @DrawableRes int resourceId) 
{
+        super(context, resourceId);
+        init();
+    }
+
+    public DrawableSpan(@NonNull Context context, @DrawableRes int resourceId, 
int verticalAlignment) {
+        super(context, resourceId, verticalAlignment);
+        init();
+    }
+
+    @Override
+    @Nullable
+    public Drawable getDrawable() {
+        return drawable;
+    }
+
+    public void setDrawable(@Nullable Drawable drawable) {
+        this.drawable = drawable;
+    }
+
+    public void setIntrinsicBounds() {
+        if (drawable != null && drawable.getBounds().isEmpty()) {
+            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), 
drawable.getIntrinsicHeight());
+        }
+    }
+
+    private void init() {
+        // super.getDrawable() is convoluted:
+        // * May return an original or a new Drawable; does not keep a 
reference in the latter
+        //   case.
+        // * May set the bounds or not.
+        // * May set the bounds differently.
+        //
+        // This is the only seam to change the Drawable state.
+        drawable = super.getDrawable();
+
+        setIntrinsicBounds();
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/richtext/IntrinsicImageSpan.java 
b/app/src/main/java/org/wikipedia/richtext/IntrinsicImageSpan.java
deleted file mode 100644
index ca0093c..0000000
--- a/app/src/main/java/org/wikipedia/richtext/IntrinsicImageSpan.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package org.wikipedia.richtext;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.text.style.ImageSpan;
-
-public class IntrinsicImageSpan extends ImageSpan {
-    @Nullable private Drawable drawable;
-
-    public IntrinsicImageSpan(@NonNull Context context, Bitmap bitmap) {
-        super(context, bitmap);
-        init();
-    }
-
-    public IntrinsicImageSpan(@NonNull Context context, Bitmap bitmap, int 
verticalAlignment) {
-        super(context, bitmap, verticalAlignment);
-        init();
-    }
-
-    public IntrinsicImageSpan(Drawable d) {
-        super(d);
-        init();
-    }
-
-    public IntrinsicImageSpan(Drawable d, int verticalAlignment) {
-        super(d, verticalAlignment);
-        init();
-    }
-
-    public IntrinsicImageSpan(Drawable d, String source) {
-        super(d, source);
-        init();
-    }
-
-    public IntrinsicImageSpan(Drawable d, String source, int 
verticalAlignment) {
-        super(d, source, verticalAlignment);
-        init();
-    }
-
-    public IntrinsicImageSpan(@NonNull Context context, Uri uri) {
-        super(context, uri);
-        init();
-    }
-
-    public IntrinsicImageSpan(@NonNull Context context, Uri uri, int 
verticalAlignment) {
-        super(context, uri, verticalAlignment);
-        init();
-    }
-
-    public IntrinsicImageSpan(@NonNull Context context, @DrawableRes int 
resourceId) {
-        super(context, resourceId);
-        init();
-    }
-
-    public IntrinsicImageSpan(@NonNull Context context, @DrawableRes int 
resourceId, int verticalAlignment) {
-        super(context, resourceId, verticalAlignment);
-        init();
-    }
-
-    @Override @Nullable public Drawable getDrawable() {
-        return drawable;
-    }
-
-    private void init() {
-        // super.getDrawable() is convoluted:
-        // * May return an original or a new Drawable; does not keep a 
reference in the latter
-        //   case.
-        // * May set the bounds or not.
-        // * May set the bounds differently.
-        //
-        // This is the only seam to change the Drawable state.
-        drawable = super.getDrawable();
-
-        setIntrinsicSize();
-    }
-
-    private void setIntrinsicSize() {
-        if (drawable != null) {
-            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), 
drawable.getIntrinsicHeight());
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/richtext/PronunciationSpan.java 
b/app/src/main/java/org/wikipedia/richtext/PronunciationSpan.java
deleted file mode 100644
index 725b13e..0000000
--- a/app/src/main/java/org/wikipedia/richtext/PronunciationSpan.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.wikipedia.richtext;
-
-import android.text.style.ClickableSpan;
-import android.view.View;
-
-public class PronunciationSpan extends ClickableSpan {
-    @Override  public void onClick(View widget) {
-        // TODO: implementation.
-    }
-}
diff --git 
a/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java 
b/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
new file mode 100644
index 0000000..01f9edc
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/richtext/TextViewSpanOnTouchListener.java
@@ -0,0 +1,116 @@
+package org.wikipedia.richtext;
+
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Layout;
+import android.text.Spanned;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+public class TextViewSpanOnTouchListener implements View.OnTouchListener {
+    @Nullable private TextView textView;
+
+    public TextViewSpanOnTouchListener(@NonNull TextView textView) {
+        this.textView = textView;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent event) {
+        ClickSpan span = getSpanned() == null ? null : 
getEventClickSpan(getSpanned(), event);
+        if (span == null) {
+            return false;
+        }
+
+        int action = event.getAction();
+        if (action == MotionEvent.ACTION_UP) {
+            span.onClick(textView);
+        }
+
+        return action == MotionEvent.ACTION_UP || action == 
MotionEvent.ACTION_DOWN;
+    }
+
+    @Nullable
+    private ClickSpan getEventClickSpan(@NonNull Spanned spanned, @NonNull 
MotionEvent event) {
+        return getEventSpan(spanned, event, ClickSpan.class);
+    }
+
+    @Nullable
+    private <T> T getEventSpan(@NonNull Spanned spanned,
+                               @NonNull MotionEvent event,
+                               @NonNull Class<T> clazz) {
+        Integer offset = getEventCharacterOffset(event);
+        if (offset == null) {
+            return null;
+        }
+
+        T[] spans = spanned.getSpans(offset, offset, clazz);
+        return spans.length > 0 ? spans[0] : null;
+    }
+
+    @Nullable
+    private Integer getEventCharacterOffset(@NonNull MotionEvent event) {
+        int x = getEventX(event);
+        int y = getEventY(event);
+        int line = getLineOffset(y);
+
+        Rect bounds = getLineBounds(line);
+        return bounds.contains(x, y) ? getCharacterOffset(line, x) : null;
+    }
+
+    private int getEventX(@NonNull MotionEvent event) {
+        return Math.round(event.getX()) - getTotalPaddingLeft() + getScrollX();
+    }
+
+    private int getEventY(@NonNull MotionEvent event) {
+        return Math.round(event.getY()) - getTotalPaddingTop() + getScrollY();
+    }
+
+    @Nullable
+    private Spanned getSpanned() {
+        return getText() instanceof Spanned ? (Spanned) getText() : null;
+    }
+
+    private CharSequence getText() {
+        return textView.getText();
+    }
+
+    private Rect getLineBounds(int line) {
+        Rect bounds = new Rect();
+        getLayout().getLineBounds(line, bounds);
+        // Left and right are set to the bounds of the TextView, not the 
bounds of the line. See
+        // Layout.getLineBounds.
+        bounds.left = Math.round(getLayout().getLineLeft(line));
+        bounds.right = Math.round(getLayout().getLineRight(line));
+        return bounds;
+    }
+
+    private int getCharacterOffset(int line, int x) {
+        return getLayout().getOffsetForHorizontal(line, x);
+    }
+
+    private int getLineOffset(int y) {
+        return getLayout().getLineForVertical(y);
+    }
+
+    private Layout getLayout() {
+        return textView.getLayout();
+    }
+
+    private int getScrollX() {
+        return textView.getScrollX();
+    }
+
+    private int getScrollY() {
+        return textView.getScrollY();
+    }
+
+    private int getTotalPaddingLeft() {
+        return textView.getTotalPaddingLeft();
+    }
+
+    private int getTotalPaddingTop() {
+        return textView.getTotalPaddingTop();
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/util/ApiUtil.java 
b/app/src/main/java/org/wikipedia/util/ApiUtil.java
index 611c3be..10d90f4 100644
--- a/app/src/main/java/org/wikipedia/util/ApiUtil.java
+++ b/app/src/main/java/org/wikipedia/util/ApiUtil.java
@@ -3,27 +3,27 @@
 import android.os.Build;
 
 public final class ApiUtil {
-    /** @return True if SDK API level is greater than or equal to 21. */
+    /** @return True if SDK API level is greater than or equal to 21, v5.0.x. 
*/
     public static boolean hasLollipop() {
         return has(Build.VERSION_CODES.LOLLIPOP);
     }
 
-    /** @return True if SDK API level is greater than or equal to 19. */
+    /** @return True if SDK API level is greater than or equal to 19, v4.4.x. 
*/
     public static boolean hasKitKat() {
         return has(Build.VERSION_CODES.KITKAT);
     }
 
-    /** @return True if SDK API level is greater than or equal to 18. */
+    /** @return True if SDK API level is greater than or equal to 18, v4.3.x. 
*/
     public static boolean hasJellyBeanMr2() {
         return has(Build.VERSION_CODES.JELLY_BEAN_MR2);
     }
 
-    /** @return True if SDK API level is greater than or equal to 17. */
+    /** @return True if SDK API level is greater than or equal to 17, v4.2.x. 
*/
     public static boolean hasJellyBeanMr1() {
         return has(Build.VERSION_CODES.JELLY_BEAN_MR1);
     }
 
-    /** @return True if SDK API level is greater than or equal to 16. */
+    /** @return True if SDK API level is greater than or equal to 16, v4.1.x. 
*/
     public static boolean hasJellyBean() {
         return has(Build.VERSION_CODES.JELLY_BEAN);
     }
diff --git a/app/src/main/java/org/wikipedia/views/AlienDrawableCallback.java 
b/app/src/main/java/org/wikipedia/views/AlienDrawableCallback.java
new file mode 100644
index 0000000..6c5c572
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/views/AlienDrawableCallback.java
@@ -0,0 +1,42 @@
+package org.wikipedia.views;
+
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.view.View;
+
+/**
+ * A {@link Drawable.Callback} for any {@link Drawable} on any {@link View}.
+ * {@link View#verifyDrawable} does not permit callbacks on unknown {@link 
Drawable}s so this
+ * wrapper invokes the {@link View}'s {@link Handler} directly when available, 
or its own handler
+ * otherwise.
+ */
+public class AlienDrawableCallback implements Drawable.Callback {
+    @NonNull
+    private final Handler handler;
+
+    @NonNull
+    private final View view;
+
+    public AlienDrawableCallback(@NonNull View view) {
+        handler = view.getHandler() == null ? new Handler() : 
view.getHandler();
+        this.view = view;
+    }
+
+    @Override
+    public void invalidateDrawable(Drawable who) {
+        // view.invalidate(who.getDirtyBounds()) would be more efficient but 
it doesn't seem to
+        // be practical to obtain the relative coordinates of the Drawable.
+        view.invalidate();
+    }
+
+    @Override
+    public void scheduleDrawable(Drawable who, Runnable what, long when) {
+        handler.postAtTime(what, when);
+    }
+
+    @Override
+    public void unscheduleDrawable(Drawable who, Runnable what) {
+        handler.removeCallbacks(what);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/wikipedia/views/AppTextView.java 
b/app/src/main/java/org/wikipedia/views/AppTextView.java
index 5c5a9d3..a10bf55 100644
--- a/app/src/main/java/org/wikipedia/views/AppTextView.java
+++ b/app/src/main/java/org/wikipedia/views/AppTextView.java
@@ -3,9 +3,16 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.os.Build;
+import android.support.annotation.NonNull;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import org.wikipedia.richtext.TextViewSpanOnTouchListener;
 
 public class AppTextView extends ConfigurableTextView {
+    @NonNull
+    private final TextViewSpanOnTouchListener spanTouchListener = new 
TextViewSpanOnTouchListener(this);
+
     public AppTextView(Context context) {
         this(context, null);
     }
@@ -29,6 +36,11 @@
         remeasureForLineSpacing();
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return super.onTouchEvent(event) || spanTouchListener.onTouch(this, 
event);
+    }
+
     // Ensure the descenders of the final line are not truncated. This usually 
happens when
     // lineSpacingMultiplier is less than one.
     private void remeasureForLineSpacing() {
diff --git a/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java 
b/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
index 4c4185d..2e8edf9 100644
--- a/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
+++ b/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
@@ -20,7 +20,6 @@
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.style.AbsoluteSizeSpan;
-import android.text.style.ImageSpan;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
@@ -32,13 +31,12 @@
 
 import org.wikipedia.R;
 import org.wikipedia.Utils;
-import org.wikipedia.drawable.DrawableUtil;
+import org.wikipedia.media.DefaultAvPlayer;
 import org.wikipedia.page.leadimages.ImageViewWithFace;
 import org.wikipedia.page.leadimages.ImageViewWithFace.OnImageLoadListener;
-import org.wikipedia.richtext.IntrinsicImageSpan;
 import org.wikipedia.richtext.LeadingSpan;
 import org.wikipedia.richtext.ParagraphSpan;
-import org.wikipedia.richtext.PronunciationSpan;
+import org.wikipedia.richtext.AudioUrlSpan;
 import org.wikipedia.richtext.RichTextUtil;
 import org.wikipedia.util.DimenUtil;
 import org.wikipedia.util.GradientUtil;
@@ -54,6 +52,7 @@
 
     @NonNull private CharSequence title = "";
     @NonNull private CharSequence subtitle = "";
+    @Nullable private String pronunciationUrl;
 
     public ArticleHeaderView(Context context) {
         super(context);
@@ -178,14 +177,13 @@
         text.setTextSize(unit, size);
     }
 
-    public void setPronunciation() {
-        // TODO: implementation.
+    public void setPronunciation(@Nullable String url) {
+        pronunciationUrl = url;
         updateText();
     }
 
     public boolean hasPronunciation() {
-        // TODO: implementation.
-        return false;
+        return pronunciationUrl != null;
     }
 
     private void updateText() {
@@ -206,18 +204,10 @@
 
     private Spanned pronunciationSpanned() {
         return RichTextUtil.setSpans(new SpannableString(" "),
-                                     0,
-                                     1,
-                                     Spannable.SPAN_INCLUSIVE_EXCLUSIVE,
-                                     pronunciationIconSpan(),
-                                     new PronunciationSpan());
-    }
-
-    private Object pronunciationIconSpan() {
-        ImageSpan span = new IntrinsicImageSpan(getContext(), 
R.drawable.ic_volume_up_black_24dp,
-                ImageSpan.ALIGN_BASELINE);
-        DrawableUtil.setTint(span.getDrawable(), Color.WHITE);
-        return span;
+                0,
+                1,
+                Spannable.SPAN_INCLUSIVE_EXCLUSIVE,
+                new AudioUrlSpan(text, new DefaultAvPlayer()));
     }
 
     private Spanned subtitleSpanned() {
@@ -227,8 +217,7 @@
                                      0,
                                      subtitle.length(),
                                      Spannable.SPAN_INCLUSIVE_EXCLUSIVE,
-                                     new 
AbsoluteSizeSpan(getDimensionPixelSize(R.dimen.descriptionTextSize),
-                                             false),
+                                     new 
AbsoluteSizeSpan(getDimensionPixelSize(R.dimen.descriptionTextSize), false),
                                      new LeadingSpan(leadingScalar),
                                      new ParagraphSpan(paragraphScalar));
     }
diff --git a/app/src/main/res/values/dimens.xml 
b/app/src/main/res/values/dimens.xml
index 4be1008..5e8cbaf 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -70,11 +70,13 @@
     <item name="lead_subtitle_leading_scalar" format="float" 
type="dimen">1.35</item>
     <item name="lead_subtitle_paragraph_scalar" format="float" 
type="dimen">1.15</item>
 
+    <dimen name="audio_url_span_loading_spinner_border_thickness">3dp</dimen>
+    <dimen name="audio_url_span_loading_spinner_radius">12dp</dimen>
+
     <!-- Maps -->
     <integer name="map_default_zoom">2</integer>
     <dimen name="map_marker_icon_size">32dp</dimen>
 
     <!-- Crash report -->
     <item name="crash_report_icon_alpha" format="float" type="dimen">.5</item>
-
 </resources>

-- 
To view, visit https://gerrit.wikimedia.org/r/250448
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Icfb00a4f0453a10fc3b492f777f5d9e52404f0dd
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to