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
