integrated ZoomView, Tagger & LinkAndScrollMovement library into package, removed gradle dependencies

This commit is contained in:
nuclearfog 2023-11-21 22:05:02 +01:00
parent 20718465b6
commit 64740ea1ba
No known key found for this signature in database
GPG Key ID: 43E45B82006BC9D5
14 changed files with 547 additions and 19 deletions

View File

@ -61,9 +61,6 @@ dependencies {
//noinspection GradleDependency
implementation 'com.squareup.picasso:picasso:2.8'
implementation 'com.github.QuadFlask:colorpicker:0.0.15'
implementation 'com.github.nuclearfog:ZoomView:1.0.4'
implementation 'com.github.nuclearfog:Tagger:2.4'
implementation 'com.github.nuclearfog:LinkAndScrollMovement:1.4.1'
implementation 'com.github.kyleduo:SwitchButton:2.0.3-SNAPSHOT'
implementation 'com.github.UnifiedPush:android-connector:2.1.1'
implementation 'com.google.android.material:material:1.9.0'

View File

@ -10,7 +10,7 @@
<body>
<h3>Notices for libraries:</h3>
<ul>
<li>Shitter, ZoomView, Tagger, LinkAndScrollMovement</li>
<li>Shitter</li>
</ul>
<pre>
Copyright 2018 nuclearfog

View File

@ -0,0 +1,94 @@
package org.nuclearfog.twidda.backend.utils;
import android.text.Layout;
import android.text.Spannable;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.view.ViewParent;
import android.widget.TextView;
/**
* @author nuclearfog
*/
public class LinkAndScrollMovement extends ScrollingMovementMethod {
private static final LinkAndScrollMovement instance = new LinkAndScrollMovement();
/**
* setup the x axis threshold to disable click events.
*/
private static final int THRESHOLD_WIDTH_DIVIDER = 6;
/**
* setup the y axis threshold to disable click events.
*/
private static final int THRESHOLD_HEIGHT_DIVIDER = 3;
private int xScroll = 0;
private int yScroll = 0;
/**
*/
private LinkAndScrollMovement() {
super();
}
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
lockParentScrolling(widget, true);
xScroll = widget.getScrollX();
yScroll = widget.getScrollY();
break;
case MotionEvent.ACTION_UP:
lockParentScrolling(widget, false);
int deltaX = Math.abs(widget.getScrollX() - xScroll);
int deltaY = Math.abs(widget.getScrollY() - yScroll);
if (deltaY <= widget.getTextSize() / THRESHOLD_HEIGHT_DIVIDER && deltaX <= widget.getWidth() / THRESHOLD_WIDTH_DIVIDER) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length > 0) {
link[0].onClick(widget);
return true;
}
}
break;
}
return super.onTouchEvent(widget, buffer, event);
}
/**
* lock parent view scrolling
*
* @param widget interacting TextView
* @param lock true if parent views scrolling should be locked
*/
private void lockParentScrolling(TextView widget, boolean lock) {
ViewParent parent = widget.getParent();
int lineCount = widget.getLineCount();
int maxLines = widget.getMaxLines();
if ( parent != null && maxLines > 0 && lineCount > maxLines ) {
parent.requestDisallowInterceptTouchEvent(lock);
}
}
/**
* Get singleton instance of the movement method
*
* @return LinkAndScrollingMovementMethod object
*/
public static LinkAndScrollMovement getInstance() {
return instance;
}
}

View File

@ -0,0 +1,212 @@
package org.nuclearfog.twidda.backend.utils;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.util.Patterns;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author nuclearfog
*/
public class Tagger {
/**
* regex patterns used to get @usernames and #hashtags
*/
private static final Pattern[] PATTERNS = {
Pattern.compile("@[^#\"“”‘’«»„"⹂‟`*'~,;:<>|^!/§%&()=?´°{}+\\-\\[\\]\\s]+"),
Pattern.compile("#[^@#\"“”‘’«»„"⹂‟`*'~,;.:<>|^!/§%&()=?´°{}+\\-\\[\\]\\s]+")
};
/**
* default span type
*/
private static final int SPAN_TYPE = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
/**
* maximum link url length before truncating
*/
private static final int MAX_LINK_LENGTH = 30;
/**
* Make a spannable colored String with click listener
*
* @param text String that should be spannable
* @param color Text Color
* @param l click listener
* @return Spannable String
*/
public static Spannable makeText(@Nullable String text, final int color, @NonNull final OnTagClickListener l) {
SpannableStringBuilder spannable = new SpannableStringBuilder();
/// Add '@' & '#' highlighting + listener
if (text != null && text.length() > 0) {
spannable.append(text);
for (Pattern pattern : PATTERNS) {
Matcher m = pattern.matcher(spannable);
while (m.find()) {
int end = m.end();
int start = m.start();
final String tag = spannable.subSequence(start, end).toString();
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
l.onTagClick(tag);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setColor(color);
ds.setUnderlineText(false);
}
}, start, end, SPAN_TYPE);
}
}
}
return spannable;
}
/**
* Make a spannable colored String with click listener
* http(s) links included
*
* @param text String that should be spannable
* @param color Text Color
* @param l click listener
* @return Spannable String
*/
public static Spannable makeTextWithLinks(@Nullable String text, final int color, @NonNull final OnTagClickListener l) {
SpannableStringBuilder spannable = new SpannableStringBuilder(makeText(text, color, l));
// Add link highlight + listener
if (spannable.length() > 0) {
Stack<Integer> indexStack = new Stack<>();
Matcher m = Patterns.WEB_URL.matcher(spannable.toString());
while (m.find()) {
indexStack.push(m.start());
indexStack.push(m.end());
}
while (!indexStack.empty()) {
int end = indexStack.pop();
int start = indexStack.pop();
final String link = spannable.subSequence(start, end).toString();
if (link.startsWith("https://")) {
spannable = spannable.delete(start, start + 8);
end -= 8;
} else if (link.startsWith("http://")) {
spannable = spannable.delete(start, start + 7);
end -= 7;
}
if (start + MAX_LINK_LENGTH < end) {
spannable.replace(start + MAX_LINK_LENGTH, end, "...");
end = start + MAX_LINK_LENGTH + 3;
}
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
l.onLinkClick(link);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setColor(color);
ds.setUnderlineText(false);
}
}, start, end, SPAN_TYPE);
}
}
return spannable;
}
/**
* Make a spannable String without listener
*
* @param text String that should be spannable
* @param color Text Color
* @return Spannable String
*/
public static Spannable makeText(@Nullable String text, int color) {
SpannableStringBuilder spannable = new SpannableStringBuilder();
// Add '@' & '#' highlighting
if (text != null && text.length() > 0) {
spannable.append(text);
for (Pattern pattern : PATTERNS) {
Matcher m = pattern.matcher(spannable.toString());
while (m.find()) {
int end = m.end();
int start = m.start();
ForegroundColorSpan colorSpan = new ForegroundColorSpan(color);
spannable.setSpan(colorSpan, start, end, SPAN_TYPE);
}
}
}
return spannable;
}
/**
* Make a spannable String without listener
* http(s) links included will be shorted
*
* @param text String that should be spannable
* @param color Text Color
* @return Spannable String
*/
public static Spannable makeTextWithLinks(@Nullable String text, int color) {
SpannableStringBuilder spannable = new SpannableStringBuilder(makeText(text, color));
// Add link highlighting
if (spannable.length() > 0) {
Stack<Integer> indexStack = new Stack<>();
Matcher m = Patterns.WEB_URL.matcher(spannable.toString());
while (m.find()) {
indexStack.push(m.start());
indexStack.push(m.end());
}
while (!indexStack.empty()) {
int end = indexStack.pop();
int start = indexStack.pop();
final String link = spannable.subSequence(start, end).toString();
if (link.startsWith("https://")) {
spannable = spannable.delete(start, start + 8);
end -= 8;
} else if (link.startsWith("http://")) {
spannable = spannable.delete(start, start + 7);
end -= 7;
}
if (start + MAX_LINK_LENGTH < end) {
spannable.replace(start + MAX_LINK_LENGTH, end, "...");
end = start + MAX_LINK_LENGTH + 3;
}
ForegroundColorSpan colorSpan = new ForegroundColorSpan(color);
spannable.setSpan(colorSpan, start, end, SPAN_TYPE);
}
}
return spannable;
}
/**
* Listener for clickable spans
*/
public interface OnTagClickListener {
/**
* Called when user clicks on a tag
*
* @param tag Tag string (starting with '@', '#')
*/
void onTagClick(String tag);
/**
* Called when user clicks on link
*
* @param link http(s) link
*/
void onLinkClick(String link);
}
}

View File

@ -30,7 +30,7 @@ import org.nuclearfog.twidda.ui.dialogs.DescriptionDialog.DescriptionCallback;
import org.nuclearfog.twidda.ui.dialogs.MetaDialog;
import org.nuclearfog.twidda.ui.views.AnimatedImageView;
import org.nuclearfog.twidda.ui.views.DescriptionView;
import org.nuclearfog.zoomview.ZoomView;
import org.nuclearfog.twidda.ui.views.ZoomView;
import java.io.File;
import java.io.Serializable;

View File

@ -31,9 +31,6 @@ import com.squareup.picasso.Callback;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Transformation;
import org.nuclearfog.tag.Tagger;
import org.nuclearfog.tag.Tagger.OnTagClickListener;
import org.nuclearfog.textviewtool.LinkAndScrollMovement;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
@ -45,8 +42,11 @@ import org.nuclearfog.twidda.backend.image.PicassoBuilder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.EmojiUtils;
import org.nuclearfog.twidda.backend.utils.ErrorUtils;
import org.nuclearfog.twidda.backend.utils.LinkAndScrollMovement;
import org.nuclearfog.twidda.backend.utils.LinkUtils;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.backend.utils.Tagger;
import org.nuclearfog.twidda.backend.utils.Tagger.OnTagClickListener;
import org.nuclearfog.twidda.backend.utils.ToolbarUpdater;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.Relation;

View File

@ -38,9 +38,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Transformation;
import org.nuclearfog.tag.Tagger;
import org.nuclearfog.tag.Tagger.OnTagClickListener;
import org.nuclearfog.textviewtool.LinkAndScrollMovement;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
@ -53,8 +50,11 @@ import org.nuclearfog.twidda.backend.image.PicassoBuilder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.EmojiUtils;
import org.nuclearfog.twidda.backend.utils.ErrorUtils;
import org.nuclearfog.twidda.backend.utils.LinkAndScrollMovement;
import org.nuclearfog.twidda.backend.utils.LinkUtils;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.backend.utils.Tagger;
import org.nuclearfog.twidda.backend.utils.Tagger.OnTagClickListener;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.Card;
import org.nuclearfog.twidda.model.Location;

View File

@ -5,7 +5,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import org.nuclearfog.tag.Tagger.OnTagClickListener;
import org.nuclearfog.twidda.backend.utils.Tagger.OnTagClickListener;
import org.nuclearfog.twidda.model.lists.Fields;
import org.nuclearfog.twidda.ui.adapter.recyclerview.holder.FieldHolder;

View File

@ -20,15 +20,15 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Transformation;
import org.nuclearfog.tag.Tagger;
import org.nuclearfog.textviewtool.LinkAndScrollMovement;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.AsyncExecutor;
import org.nuclearfog.twidda.backend.async.TextEmojiLoader;
import org.nuclearfog.twidda.backend.image.PicassoBuilder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.EmojiUtils;
import org.nuclearfog.twidda.backend.utils.LinkAndScrollMovement;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.backend.utils.Tagger;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.EditedStatus;
import org.nuclearfog.twidda.model.User;

View File

@ -10,11 +10,11 @@ import android.widget.TextView;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.nuclearfog.tag.Tagger;
import org.nuclearfog.tag.Tagger.OnTagClickListener;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.backend.utils.Tagger;
import org.nuclearfog.twidda.backend.utils.Tagger.OnTagClickListener;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.User.Field;

View File

@ -12,10 +12,10 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.nuclearfog.tag.Tagger;
import org.nuclearfog.textviewtool.LinkAndScrollMovement;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.LinkAndScrollMovement;
import org.nuclearfog.twidda.backend.utils.Tagger;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.ScheduledStatus;
import org.nuclearfog.twidda.model.Status;

View File

@ -23,7 +23,6 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Transformation;
import org.nuclearfog.tag.Tagger;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.async.AsyncExecutor.AsyncCallback;
import org.nuclearfog.twidda.backend.async.TextEmojiLoader;
@ -33,6 +32,7 @@ import org.nuclearfog.twidda.backend.image.PicassoBuilder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.backend.utils.EmojiUtils;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.backend.utils.Tagger;
import org.nuclearfog.twidda.config.GlobalSettings;
import org.nuclearfog.twidda.model.Notification;
import org.nuclearfog.twidda.model.Status;

View File

@ -0,0 +1,219 @@
package org.nuclearfog.twidda.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RemoteViews.RemoteView;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import static android.view.MotionEvent.*;
import org.nuclearfog.twidda.R;
/**
* Zoomable image view
*
* @author nuclearfog
*/
@RemoteView
public class ZoomView extends AppCompatImageView {
// Default values
private static final float DEF_MAX_ZOOM_IN = 3.0f;
private static final float DEF_MAX_ZOOM_OUT = 0.5f;
private static final boolean DEF_ENABLE_MOVE = true;
private static final ScaleType DEF_SCALE_TYPE = ScaleType.FIT_CENTER;
// Layout Attributes
private float max_zoom_in = DEF_MAX_ZOOM_IN;
private float max_zoom_out = DEF_MAX_ZOOM_OUT;
private boolean enableMove = DEF_ENABLE_MOVE;
private ScaleType scaleType = DEF_SCALE_TYPE;
// intern flags
private final PointF pos = new PointF(0.0f, 0.0f);
private final PointF dist = new PointF(0.0f, 0.0f);
private boolean moveLock = false;
/**
*
*/
public ZoomView(Context context) {
super(context);
}
/**
*
*/
public ZoomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
/**
*
*/
public ZoomView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
scaleType = getScaleType();
if (attrs != null) {
TypedArray attrArray = context.obtainStyledAttributes(attrs, R.styleable.ZoomView);
setMaxZoomIn(attrArray.getFloat(R.styleable.ZoomView_max_zoom_in, DEF_MAX_ZOOM_IN));
setMaxZoomOut(attrArray.getFloat(R.styleable.ZoomView_max_zoom_out, DEF_MAX_ZOOM_OUT));
setMovable(attrArray.getBoolean(R.styleable.ZoomView_enable_move, DEF_ENABLE_MOVE));
attrArray.recycle();
}
}
@Override
public boolean performClick() {
return super.performClick();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (getScaleType() != ScaleType.MATRIX)
setScaleType(ScaleType.MATRIX);
if (event.getPointerCount() == 1) {
switch (event.getAction()) {
case ACTION_UP:
pos.set(event.getX(), event.getY());
moveLock = false;
break;
case ACTION_DOWN:
pos.set(event.getX(), event.getY());
break;
case ACTION_MOVE:
if (moveLock || !enableMove)
return super.performClick();
float posX = event.getX() - pos.x;
float posY = event.getY() - pos.y;
pos.set(event.getX(), event.getY());
Matrix m = new Matrix(getImageMatrix());
m.postTranslate(posX, posY);
apply(m);
break;
}
} else if (event.getPointerCount() == 2) {
float distX, distY, scale;
switch (event.getActionMasked()) {
case ACTION_POINTER_UP:
case ACTION_POINTER_DOWN:
distX = event.getX(0) - event.getX(1);
distY = event.getY(0) - event.getY(1);
dist.set(distX, distY); // Distance vector
moveLock = true;
break;
case ACTION_MOVE:
distX = event.getX(0) - event.getX(1);
distY = event.getY(0) - event.getY(1);
PointF current = new PointF(distX, distY);
scale = current.length() / dist.length();
Matrix m = new Matrix(getImageMatrix());
m.postScale(scale, scale, getWidth() / 2.0f, getHeight() / 2.0f);
dist.set(distX, distY);
apply(m);
break;
}
}
return true;
}
/**
* Reset Image position/zoom to default
*/
public void reset() {
setScaleType(scaleType);
}
/**
* set Image movable
*
* @param enableMove set image movable
*/
public void setMovable(boolean enableMove) {
this.enableMove = enableMove;
}
/**
* set maximum zoom in
*
* @param max_zoom_in maximum zoom value
*/
public void setMaxZoomIn(float max_zoom_in) {
if (max_zoom_in < 1.0f)
throw new AssertionError("value should be more 1.0!");
this.max_zoom_in = max_zoom_in;
}
/**
* set maximum zoom in
*
* @param max_zoom_out maximum zoom value
*/
public void setMaxZoomOut(float max_zoom_out) {
if (max_zoom_out > 1.0f)
throw new AssertionError("value should be less 1.0!");
this.max_zoom_out = max_zoom_out;
}
/**
*
*/
private void apply(Matrix m) {
Drawable d = getDrawable();
if (d == null) return;
float[] val = new float[9];
m.getValues(val);
float scale = (val[Matrix.MSCALE_X] + val[Matrix.MSCALE_Y]) / 2; // Scale factor
float width = d.getIntrinsicWidth() * scale; // image width
float height = d.getIntrinsicHeight() * scale; // image height
float leftBorder = val[Matrix.MTRANS_X]; // distance to left border
float rightBorder = -(val[Matrix.MTRANS_X] + width - getWidth()); // distance to right border
float bottomBorder = val[Matrix.MTRANS_Y]; // distance to bottom border
float topBorder = -(val[Matrix.MTRANS_Y] + height - getHeight()); // distance to top border
if (width > getWidth()) { // is image width bigger than screen width?
if (rightBorder > 0) // is image on the right border?
m.postTranslate(rightBorder, 0); // clamp to right border
else if (leftBorder > 0)
m.postTranslate(-leftBorder, 0); // clamp to left order
} else if (leftBorder < 0 ^ rightBorder < 0) { // does image clash with one border?
if (rightBorder < 0)
m.postTranslate(rightBorder, 0); // clamp to right border
else
m.postTranslate(-leftBorder, 0); // clamp to left border
}
if (height > getHeight()) { // is image height bigger than screen height?
if (bottomBorder > 0) // is image on the bottom border?
m.postTranslate(0, -bottomBorder); // clamp to bottom border
else if (topBorder > 0) // is image on the top border?
m.postTranslate(0, topBorder); // clamp to top border
} else if (topBorder < 0 ^ bottomBorder < 0) { // does image clash with one border?
if (bottomBorder < 0)
m.postTranslate(0, -bottomBorder); // clamp to bottom border
else
m.postTranslate(0, topBorder); // clamp to top border
}
if (scale > max_zoom_in) { // scale limit exceeded?
float undoScale = max_zoom_in / scale; // undo scale setting
m.postScale(undoScale, undoScale, getWidth() / 2.0f, getHeight() / 2.0f);
} else if (scale < max_zoom_out) { // scale limit exceeded?
float undoScale = max_zoom_out / scale; // undo scale setting
m.postScale(undoScale, undoScale, getWidth() / 2.0f, getHeight() / 2.0f);
}
setImageMatrix(m); // set Image matrix
}
}

View File

@ -9,4 +9,10 @@
<attr name="viewpager" format="reference" />
</declare-styleable>
<declare-styleable name="ZoomView">
<attr name="max_zoom_in" format="float" />
<attr name="max_zoom_out" format="float" />
<attr name="enable_move" format="boolean" />
</declare-styleable>
</resources>