Merge branch 'dev_clickable_links_hold_to_copy'
This commit is contained in:
commit
a8ba50e762
|
@ -1,36 +1,83 @@
|
||||||
package org.joinmastodon.android.ui.text;
|
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.Canvas;
|
||||||
import android.graphics.CornerPathEffect;
|
import android.graphics.CornerPathEffect;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.view.GestureDetector;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.SoundEffectConstants;
|
import android.view.SoundEffectConstants;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ClickableLinksDelegate {
|
public class ClickableLinksDelegate {
|
||||||
|
|
||||||
private Paint hlPaint;
|
private final Paint hlPaint;
|
||||||
private Path hlPath;
|
private Path hlPath;
|
||||||
private LinkSpan selectedSpan;
|
private LinkSpan selectedSpan;
|
||||||
private TextView view;
|
private final TextView view;
|
||||||
|
|
||||||
|
private final GestureDetector gestureDetector;
|
||||||
|
|
||||||
public ClickableLinksDelegate(TextView view) {
|
public ClickableLinksDelegate(TextView view) {
|
||||||
this.view=view;
|
this.view=view;
|
||||||
hlPaint=new Paint();
|
hlPaint=new Paint();
|
||||||
hlPaint.setAntiAlias(true);
|
hlPaint.setAntiAlias(true);
|
||||||
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
||||||
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
|
// 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) {
|
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;
|
int line=-1;
|
||||||
Rect rect=new Rect();
|
Rect rect=new Rect();
|
||||||
Layout l=view.getLayout();
|
Layout l=view.getLayout();
|
||||||
|
@ -45,8 +92,7 @@ public class ClickableLinksDelegate {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CharSequence text=view.getText();
|
CharSequence text=view.getText();
|
||||||
if(text instanceof Spanned){
|
if(text instanceof Spanned s){
|
||||||
Spanned s=(Spanned)text;
|
|
||||||
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
||||||
if(spans.length>0){
|
if(spans.length>0){
|
||||||
for(LinkSpan span:spans){
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue