|
|
|
|
@ -29,6 +29,7 @@ import android.content.res.Resources;
|
|
|
|
|
import android.graphics.Bitmap;
|
|
|
|
|
import android.graphics.BitmapFactory;
|
|
|
|
|
import android.graphics.Canvas;
|
|
|
|
|
import android.graphics.Point;
|
|
|
|
|
import android.graphics.Rect;
|
|
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
|
@ -36,6 +37,7 @@ import android.net.Uri;
|
|
|
|
|
import android.os.Build;
|
|
|
|
|
import android.provider.ContactsContract;
|
|
|
|
|
import android.text.Editable;
|
|
|
|
|
import android.text.Layout;
|
|
|
|
|
import android.text.Spanned;
|
|
|
|
|
import android.text.TextDirectionHeuristics;
|
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
@ -43,6 +45,8 @@ import android.text.TextWatcher;
|
|
|
|
|
import android.text.style.DynamicDrawableSpan;
|
|
|
|
|
import android.text.style.ImageSpan;
|
|
|
|
|
import android.util.AttributeSet;
|
|
|
|
|
import android.view.DragEvent;
|
|
|
|
|
import android.view.HapticFeedbackConstants;
|
|
|
|
|
import android.view.MotionEvent;
|
|
|
|
|
import android.view.View;
|
|
|
|
|
|
|
|
|
|
@ -71,6 +75,8 @@ public class EditTextMultiAutoComplete extends AppCompatMultiAutoCompleteTextVie
|
|
|
|
|
private boolean dark;
|
|
|
|
|
private int colorAccent;
|
|
|
|
|
private Tokenizer tokenizer;
|
|
|
|
|
private float dragDownX;
|
|
|
|
|
private float dragDownY;
|
|
|
|
|
private Map<String, Integer> encryption = new ConcurrentHashMap<>();
|
|
|
|
|
|
|
|
|
|
private static int[] icons = new int[]{
|
|
|
|
|
@ -100,6 +106,39 @@ public class EditTextMultiAutoComplete extends AppCompatMultiAutoCompleteTextVie
|
|
|
|
|
tokenizer = new CommaTokenizer();
|
|
|
|
|
setTokenizer(tokenizer);
|
|
|
|
|
|
|
|
|
|
setOnDragListener(new OnDragListener() {
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onDrag(View v, DragEvent event) {
|
|
|
|
|
try {
|
|
|
|
|
if (!(event.getLocalState() instanceof DragState))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
switch (event.getAction()) {
|
|
|
|
|
case DragEvent.ACTION_DRAG_STARTED:
|
|
|
|
|
return (event.getClipDescription() != null &&
|
|
|
|
|
event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN));
|
|
|
|
|
|
|
|
|
|
case DragEvent.ACTION_DRAG_ENTERED:
|
|
|
|
|
case DragEvent.ACTION_DRAG_LOCATION:
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case DragEvent.ACTION_DROP:
|
|
|
|
|
return dropClip((DragState) event.getLocalState(), event.getX(), event.getY());
|
|
|
|
|
|
|
|
|
|
case DragEvent.ACTION_DRAG_EXITED:
|
|
|
|
|
case DragEvent.ACTION_DRAG_ENDED:
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
|
Log.e(ex);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
|
dark = Helper.isDarkTheme(context);
|
|
|
|
|
colorAccent = Helper.resolveColor(context, androidx.appcompat.R.attr.colorAccent);
|
|
|
|
|
@ -195,6 +234,18 @@ public class EditTextMultiAutoComplete extends AppCompatMultiAutoCompleteTextVie
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
|
|
|
try {
|
|
|
|
|
switch (event.getActionMasked()) {
|
|
|
|
|
case MotionEvent.ACTION_DOWN:
|
|
|
|
|
dragDownX = event.getX();
|
|
|
|
|
dragDownY = event.getY();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
|
|
|
dragDownX = 0;
|
|
|
|
|
dragDownY = 0;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return super.onTouchEvent(event);
|
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
|
Log.w(ex);
|
|
|
|
|
@ -202,6 +253,49 @@ public class EditTextMultiAutoComplete extends AppCompatMultiAutoCompleteTextVie
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean performLongClick() {
|
|
|
|
|
try {
|
|
|
|
|
Editable edit = getText();
|
|
|
|
|
Layout layout = getLayout();
|
|
|
|
|
if (edit == null || layout == null)
|
|
|
|
|
return super.performLongClick();
|
|
|
|
|
|
|
|
|
|
int line = layout.getLineForVertical((int) (dragDownY - getTotalPaddingTop() + getScrollY()));
|
|
|
|
|
float x = dragDownX - getTotalPaddingLeft() + getScrollX();
|
|
|
|
|
int offset = layout.getOffsetForHorizontal(line, x);
|
|
|
|
|
|
|
|
|
|
ClipImageSpan[] spans = edit.getSpans(offset, offset, ClipImageSpan.class);
|
|
|
|
|
if (spans.length != 1)
|
|
|
|
|
return super.performLongClick();
|
|
|
|
|
|
|
|
|
|
ClipImageSpan span = spans[0];
|
|
|
|
|
|
|
|
|
|
int start = edit.getSpanStart(span);
|
|
|
|
|
int end = edit.getSpanEnd(span);
|
|
|
|
|
|
|
|
|
|
if (start < 0 || end <= start)
|
|
|
|
|
return super.performLongClick();
|
|
|
|
|
|
|
|
|
|
Helper.performHapticFeedback(this, HapticFeedbackConstants.CONFIRM);
|
|
|
|
|
|
|
|
|
|
String text = edit.subSequence(start, end).toString();
|
|
|
|
|
DragState state = new DragState(this, span, start, end, text);
|
|
|
|
|
ClipData data = ClipData.newPlainText("FairEmail recipient", text);
|
|
|
|
|
View.DragShadowBuilder shadow = new ChipDragShadowBuilder(span);
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
|
|
|
|
return startDragAndDrop(data, shadow, state, View.DRAG_FLAG_OPAQUE);
|
|
|
|
|
else {
|
|
|
|
|
startDrag(data, shadow, state, 0);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
|
Log.e(ex);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void replaceText(CharSequence text) {
|
|
|
|
|
HtmlHelper.clearComposingText(this);
|
|
|
|
|
@ -424,6 +518,82 @@ public class EditTextMultiAutoComplete extends AppCompatMultiAutoCompleteTextVie
|
|
|
|
|
return Math.max(start, selStart) <= Math.min(end, selEnd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean dropClip(DragState state, float x, float y) {
|
|
|
|
|
try {
|
|
|
|
|
Editable edit = getText();
|
|
|
|
|
Layout layout = getLayout();
|
|
|
|
|
if (edit == null || layout == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
int target = getDropOffset(layout, x, y);
|
|
|
|
|
if (target < 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
target = resolveDropBoundary(edit, target, state.span);
|
|
|
|
|
|
|
|
|
|
int sourceStart = state.start;
|
|
|
|
|
int sourceEnd = state.end;
|
|
|
|
|
if (this == state.source && target >= sourceStart && target <= sourceEnd)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
String text = state.text;
|
|
|
|
|
int insertLength = text.length();
|
|
|
|
|
|
|
|
|
|
Editable source = state.source.getText();
|
|
|
|
|
source.removeSpan(state.span);
|
|
|
|
|
source.delete(sourceStart, sourceEnd);
|
|
|
|
|
state.source.invalidate();
|
|
|
|
|
state.source.post(state.source.update);
|
|
|
|
|
if (this == state.source && target > sourceEnd)
|
|
|
|
|
target -= sourceEnd - sourceStart;
|
|
|
|
|
|
|
|
|
|
target = Math.max(0, Math.min(target, edit.length()));
|
|
|
|
|
target = resolveDropBoundary(edit, target, null);
|
|
|
|
|
edit.insert(target, text);
|
|
|
|
|
edit.setSpan(state.span, target, target + insertLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
setSelection(target + insertLength);
|
|
|
|
|
invalidate();
|
|
|
|
|
post(update);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
|
Log.e(ex);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int getDropOffset(Layout layout, float x, float y) {
|
|
|
|
|
int line = layout.getLineForVertical((int) (y - getTotalPaddingTop() + getScrollY()));
|
|
|
|
|
line = Math.max(0, Math.min(line, layout.getLineCount() - 1));
|
|
|
|
|
float horizontal = x - getTotalPaddingLeft() + getScrollX();
|
|
|
|
|
horizontal = Math.max(layout.getLineLeft(line), Math.min(horizontal, layout.getLineRight(line)));
|
|
|
|
|
return layout.getOffsetForHorizontal(line, horizontal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int resolveDropBoundary(Editable edit, int target, @Nullable ClipImageSpan ignore) {
|
|
|
|
|
target = Math.max(0, Math.min(target, edit.length()));
|
|
|
|
|
ClipImageSpan[] spans = edit.getSpans(0, edit.length(), ClipImageSpan.class);
|
|
|
|
|
for (ClipImageSpan span : spans) {
|
|
|
|
|
if (span == ignore)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
int start = edit.getSpanStart(span);
|
|
|
|
|
int end = edit.getSpanEnd(span);
|
|
|
|
|
if (start < 0 || end <= start)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (target > start && target < end) {
|
|
|
|
|
int middle = start + (end - start) / 2;
|
|
|
|
|
return (target < middle ? start : end);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (target == start || target == end)
|
|
|
|
|
return target;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return target;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class ClipImageSpan extends ImageSpan {
|
|
|
|
|
private boolean update;
|
|
|
|
|
|
|
|
|
|
@ -439,4 +609,45 @@ public class EditTextMultiAutoComplete extends AppCompatMultiAutoCompleteTextVie
|
|
|
|
|
return update;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class DragState {
|
|
|
|
|
final EditTextMultiAutoComplete source;
|
|
|
|
|
final ClipImageSpan span;
|
|
|
|
|
final int start;
|
|
|
|
|
final int end;
|
|
|
|
|
final String text;
|
|
|
|
|
|
|
|
|
|
DragState(EditTextMultiAutoComplete source, ClipImageSpan span, int start, int end, String text) {
|
|
|
|
|
this.source = source;
|
|
|
|
|
this.span = span;
|
|
|
|
|
this.start = start;
|
|
|
|
|
this.end = end;
|
|
|
|
|
this.text = text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class ChipDragShadowBuilder extends View.DragShadowBuilder {
|
|
|
|
|
private final Drawable drawable;
|
|
|
|
|
|
|
|
|
|
ChipDragShadowBuilder(ClipImageSpan span) {
|
|
|
|
|
super();
|
|
|
|
|
drawable = span.getDrawable();
|
|
|
|
|
if (drawable != null)
|
|
|
|
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
|
|
|
|
|
int width = Math.max(1, drawable.getBounds().width());
|
|
|
|
|
int height = Math.max(1, drawable.getBounds().height());
|
|
|
|
|
outShadowSize.set(width, height);
|
|
|
|
|
outShadowTouchPoint.set(width / 2, height / 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onDrawShadow(Canvas canvas) {
|
|
|
|
|
if (drawable != null)
|
|
|
|
|
drawable.draw(canvas);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|