diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 482a7eacf..e7b219940 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -1,36 +1,83 @@ package org.joinmastodon.android.ui.text; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.graphics.Canvas; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Build; import android.text.Layout; import android.text.Spanned; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import org.joinmastodon.android.R; import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - private Paint hlPaint; + private final Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - private TextView view; - + private final TextView view; + + private final GestureDetector gestureDetector; + public ClickableLinksDelegate(TextView view) { this.view=view; hlPaint=new Paint(); hlPaint.setAntiAlias(true); hlPaint.setPathEffect(new CornerPathEffect(V.dp(3))); // view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light)); + gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler()); } public boolean onTouch(MotionEvent event) { - if(event.getAction()==MotionEvent.ACTION_DOWN){ + if(event.getAction()==MotionEvent.ACTION_CANCEL){ + // the gestureDetector does not provide a callback for CANCEL, therefore: + // remove background color of view before passing event to gestureDetector + resetAndInvalidate(); + } + return gestureDetector.onTouchEvent(event); + } + + /** + * remove highlighting from span and let the system redraw the view + */ + private void resetAndInvalidate() { + hlPath=null; + selectedSpan=null; + view.invalidate(); + } + + public void onDraw(Canvas canvas){ + if(hlPath!=null){ + canvas.save(); + canvas.translate(0, view.getPaddingTop()); + canvas.drawPath(hlPath, hlPaint); + canvas.restore(); + } + } + + /** + * GestureListener for spans that represent URLs. + * onDown: on start of touch event, set highlighting + * onSingleTapUp: when there was a (short) tap, call onClick and reset highlighting + * onLongPress: copy URL to clipboard, let user know, reset highlighting + */ + private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent event) { int line=-1; Rect rect=new Rect(); Layout l=view.getLayout(); @@ -45,8 +92,7 @@ public class ClickableLinksDelegate { return false; } CharSequence text=view.getText(); - if(text instanceof Spanned){ - Spanned s=(Spanned)text; + if(text instanceof Spanned s){ LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class); if(spans.length>0){ for(LinkSpan span:spans){ @@ -88,31 +134,35 @@ public class ClickableLinksDelegate { } } } + return super.onDown(event); } - if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - view.playSoundEffect(SoundEffectConstants.CLICK); - selectedSpan.onClick(view.getContext()); - hlPath=null; - selectedSpan=null; - view.invalidate(); - return false; - } - if(event.getAction()==MotionEvent.ACTION_CANCEL){ - hlPath=null; - selectedSpan=null; - view.invalidate(); - return false; - } - return false; - } - - public void onDraw(Canvas canvas){ - if(hlPath!=null){ - canvas.save(); - canvas.translate(0, view.getPaddingTop()); - canvas.drawPath(hlPath, hlPaint); - canvas.restore(); - } - } + @Override + public boolean onSingleTapUp(@NonNull MotionEvent event) { + if(selectedSpan!=null){ + view.playSoundEffect(SoundEffectConstants.CLICK); + selectedSpan.onClick(view.getContext()); + resetAndInvalidate(); + return true; + } + return false; + } + + @Override + public void onLongPress(@NonNull MotionEvent event) { + //if target is not a link, don't copy + if (selectedSpan == null) return; + if (selectedSpan.getType() != LinkSpan.Type.URL) return; + //copy link text to clipboard + ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("", selectedSpan.getLink())); + //show toast, android from S_V2 on has built-in popup, as documented in + //https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(view.getContext(), R.string.text_copied, Toast.LENGTH_SHORT).show(); + } + //reset view + resetAndInvalidate(); + } + } }