diff --git a/app/build.gradle b/app/build.gradle index c1e0787a..913d8f47 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/assets/licenses.html b/app/src/main/assets/licenses.html index d35ed425..bcf59354 100644 --- a/app/src/main/assets/licenses.html +++ b/app/src/main/assets/licenses.html @@ -10,7 +10,7 @@

Notices for libraries:

             Copyright 2018 nuclearfog
diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/utils/LinkAndScrollMovement.java b/app/src/main/java/org/nuclearfog/twidda/backend/utils/LinkAndScrollMovement.java
new file mode 100644
index 00000000..31157b14
--- /dev/null
+++ b/app/src/main/java/org/nuclearfog/twidda/backend/utils/LinkAndScrollMovement.java
@@ -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;
+	}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tagger.java b/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tagger.java
new file mode 100644
index 00000000..5a2df8d5
--- /dev/null
+++ b/app/src/main/java/org/nuclearfog/twidda/backend/utils/Tagger.java
@@ -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 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 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);
+	}
+}
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ImageViewer.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ImageViewer.java
index d4628720..6682baf9 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ImageViewer.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ImageViewer.java
@@ -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;
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java
index 773dfc7a..29aa9f11 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/ProfileActivity.java
@@ -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;
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusActivity.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusActivity.java
index 62879397..bc66991e 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusActivity.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusActivity.java
@@ -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;
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/FieldAdapter.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/FieldAdapter.java
index d5e7fed4..dc7ac248 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/FieldAdapter.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/FieldAdapter.java
@@ -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;
 
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/EditHistoryHolder.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/EditHistoryHolder.java
index cd3e20b1..39ff9f71 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/EditHistoryHolder.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/EditHistoryHolder.java
@@ -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;
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/FieldHolder.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/FieldHolder.java
index 3ddc9b35..edf8fb59 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/FieldHolder.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/FieldHolder.java
@@ -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;
 
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/ScheduleHolder.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/ScheduleHolder.java
index 77069ca6..83a6da27 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/ScheduleHolder.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/ScheduleHolder.java
@@ -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;
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/StatusHolder.java b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/StatusHolder.java
index a7d29918..5dcaf940 100644
--- a/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/StatusHolder.java
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/adapter/recyclerview/holder/StatusHolder.java
@@ -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;
diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/views/ZoomView.java b/app/src/main/java/org/nuclearfog/twidda/ui/views/ZoomView.java
new file mode 100644
index 00000000..eeca48d2
--- /dev/null
+++ b/app/src/main/java/org/nuclearfog/twidda/ui/views/ZoomView.java
@@ -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
+	}
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/attr.xml b/app/src/main/res/values/attr.xml
index 20ae9449..c2798535 100644
--- a/app/src/main/res/values/attr.xml
+++ b/app/src/main/res/values/attr.xml
@@ -9,4 +9,10 @@
 		
 	
 
+	
+		
+		
+		
+	
+
 
\ No newline at end of file