diff --git a/app/build.gradle b/app/build.gradle index 235318e..2b17f13 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 23 @@ -24,7 +25,7 @@ dependencies { testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.android.support:design:23.0.1' - compile 'com.github.Lukle:ClickableAreasImages:v0.1' + implementation project(':clickableareasimage') debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // compile project(':clickableareasimage') diff --git a/app/src/main/java/at/lukle/clickableareas/MainActivity.java b/app/src/main/java/at/lukle/clickableareas/MainActivity.java index a5e9af9..8645e09 100644 --- a/app/src/main/java/at/lukle/clickableareas/MainActivity.java +++ b/app/src/main/java/at/lukle/clickableareas/MainActivity.java @@ -3,6 +3,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.ImageView; @@ -11,11 +12,17 @@ import java.util.ArrayList; import java.util.List; +import at.lukle.clickableareasimage.AbstractArea; import at.lukle.clickableareasimage.ClickableArea; import at.lukle.clickableareasimage.ClickableAreasImage; +import at.lukle.clickableareasimage.ClickableCircleArea; +import at.lukle.clickableareasimage.ClickablePolyArea; +import at.lukle.clickableareasimage.ClickableRectangleArea; import at.lukle.clickableareasimage.OnClickableAreaClickedListener; +import at.lukle.clickableareasimage.PixelPosition; import uk.co.senab.photoview.PhotoViewAttacher; +@SuppressWarnings("rawtypes") public class MainActivity extends AppCompatActivity implements OnClickableAreaClickedListener { private final String TAG = getClass().getSimpleName(); @@ -33,8 +40,7 @@ protected void onCreate(Bundle savedInstanceState) { ClickableAreasImage clickableAreasImage = new ClickableAreasImage(new PhotoViewAttacher(image), this); // Define your clickable area (pixel values: x coordinate, y coordinate, width, height) and assign an object to it - List clickableAreas = getClickableAreas(); - clickableAreasImage.setClickableAreas(clickableAreas); + clickableAreasImage.setClickableAreas(getClickableAreas()); } // Listen for touches on your images: @@ -47,21 +53,40 @@ public void onClickableAreaTouched(Object item) { } @NonNull - private List getClickableAreas() { - - List clickableAreas = new ArrayList<>(); - - clickableAreas.add(new ClickableArea(600, 100, 50, 50, new State("Lower Austria"))); - clickableAreas.add(new ClickableArea(440, 125, 50, 50, new State("Upper Austria"))); - clickableAreas.add(new ClickableArea(700, 126, 50, 50, new State("Vienna"))); - - clickableAreas.add(new ClickableArea(685, 270, 50, 50, new State("Burgenland"))); - clickableAreas.add(new ClickableArea(420, 350, 50, 50, new State("Carinthia"))); - clickableAreas.add(new ClickableArea(370, 245, 50, 50, new State("Salzburg"))); - - clickableAreas.add(new ClickableArea(170, 280, 50, 50, new State("Tyrol"))); - clickableAreas.add(new ClickableArea(30, 280, 50, 50, new State("Vorarlberg"))); - clickableAreas.add(new ClickableArea(570, 250, 50, 50, new State("Styria"))); + private List getClickableAreas() { + + List clickableAreas = new ArrayList<>(); + + clickableAreas.add(new ClickableRectangleArea<>(600, 100, 50, 50, new State("Lower Austria"))); + clickableAreas.add(new ClickableRectangleArea<>(440, 125, 50, 50, new State("Upper Austria"))); + clickableAreas.add(new ClickableRectangleArea<>(700, 126, 50, 50, new State("Vienna"))); + + clickableAreas.add(new ClickableCircleArea<>(715, 300, 27, new State("Burgenland"))); + clickableAreas.add(new ClickableCircleArea<>(450, 380, 27, new State("Carinthia"))); + clickableAreas.add(new ClickableCircleArea<>(400, 275, 27, new State("Salzburg"))); + + + clickableAreas.add(new ClickablePolyArea<>(new State("Tyrol"), + new PixelPosition(170,280), + new PixelPosition(160,330), + new PixelPosition(190, 340), + new PixelPosition(220,330), + new PixelPosition(220, 280), + new PixelPosition(195, 270))); + clickableAreas.add(new ClickablePolyArea<>(new State("Vorarlberg"), + new PixelPosition(30, 280), + new PixelPosition(20, 330), + new PixelPosition(50, 340), + new PixelPosition(80, 330), + new PixelPosition(80, 280), + new PixelPosition(55, 270))); + clickableAreas.add(new ClickablePolyArea<>(new State("Styria"), + new PixelPosition(570, 250), + new PixelPosition(560, 300), + new PixelPosition(590, 310), + new PixelPosition(620, 300), + new PixelPosition(620, 250), + new PixelPosition(595, 240))); return clickableAreas; } diff --git a/build.gradle b/build.gradle index be515a8..a05dcc5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,18 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.5.30' + ext.kotlin_version = "1.4.32" repositories { jcenter() + google() + maven { + url "https://maven.google.com" + } } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:4.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,6 +22,10 @@ buildscript { allprojects { repositories { jcenter() + google() + maven { + url "https://maven.google.com" + } } } diff --git a/clickableareasimage/build.gradle b/clickableareasimage/build.gradle index 5d5bc11..53817b3 100644 --- a/clickableareasimage/build.gradle +++ b/clickableareasimage/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { compileSdkVersion 23 @@ -16,11 +17,29 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + testOptions { + unitTests.returnDefaultValues = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } dependencies { - compile 'com.android.support:appcompat-v7:23.+' - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.commit451:PhotoView:1.2.4' - testCompile 'junit:junit:4.12' + api 'com.android.support:appcompat-v7:23.+' + api fileTree(dir: 'libs', include: ['*.jar']) + api 'com.commit451:PhotoView:1.2.4' + testImplementation 'junit:junit:4.13.2' + testImplementation "io.mockk:mockk:1.12.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() } diff --git a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/AbstractArea.kt b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/AbstractArea.kt new file mode 100644 index 0000000..9641109 --- /dev/null +++ b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/AbstractArea.kt @@ -0,0 +1,6 @@ +package at.lukle.clickableareasimage + +abstract class AbstractArea(var item : T) { + + abstract fun isInside(positionX: Int, positionY: Int): Boolean +} \ No newline at end of file diff --git a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableArea.java b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableArea.java index 9d94fbf..9e69b48 100644 --- a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableArea.java +++ b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableArea.java @@ -3,21 +3,20 @@ /** * Created by Lukas on 10/22/2015. */ -public class ClickableArea { +@Deprecated //use ClickableRectangleArea +public class ClickableArea extends AbstractArea { private int x; private int y; private int w; private int h; - private T item; - public ClickableArea(int x, int y, int w, int h, T item){ + super(item); this.x = x; this.y = y; this.w = w; this.h = h; - this.item = item; } public int getX() { @@ -52,11 +51,16 @@ public void setH(int h) { this.h = h; } - public T getItem() { - return item; + public void setLabel(T item) { + super.setItem(item); } - public void setLabel(T item) { - this.item = item; + @Override + public boolean isInside(int positionX, int positionY) { + return isBetween(this.x, this.x + this.w, positionX) && isBetween(this.y, this.y + this.h, positionY); + } + + private boolean isBetween(int start, int end, int actual){ + return (start <= actual && actual <= end); } } diff --git a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableAreasImage.java b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableAreasImage.java index 0c6ba05..a5b1421 100644 --- a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableAreasImage.java +++ b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableAreasImage.java @@ -2,6 +2,7 @@ import android.content.res.Resources; import android.graphics.drawable.BitmapDrawable; +import android.support.annotation.VisibleForTesting; import android.view.View; import android.widget.ImageView; @@ -13,12 +14,13 @@ /** * Created by Lukas on 10/22/2015. */ +@SuppressWarnings("rawtypes") public class ClickableAreasImage implements PhotoViewAttacher.OnPhotoTapListener{ private PhotoViewAttacher attacher; private OnClickableAreaClickedListener listener; - private List clickableAreas; + private List clickableAreas; private int imageWidthInPx; private int imageHeightInPx; @@ -35,51 +37,39 @@ private void init(OnClickableAreaClickedListener listener) { } + @VisibleForTesting private void getImageDimensions(ImageView imageView){ BitmapDrawable drawable2 = (BitmapDrawable) imageView.getDrawable(); - //After SDK 28 (Android Pie), getBitmap() returns the actual size of the image on the screen - if (Build.VERSION.SDK_INT > 27) { - imageWidthInPx = (int) (drawable2.getBitmap().getWidth()); - imageHeightInPx = (int) (drawable2.getBitmap().getHeight()); - } else { - imageWidthInPx = (int) (drawable2.getBitmap().getWidth() / Resources.getSystem().getDisplayMetrics().density); - imageHeightInPx = (int) (drawable2.getBitmap().getHeight() / Resources.getSystem().getDisplayMetrics().density); - } - + imageWidthInPx = (int) (drawable2.getBitmap().getWidth() / Resources.getSystem().getDisplayMetrics().density); + imageHeightInPx = (int) (drawable2.getBitmap().getHeight() / Resources.getSystem().getDisplayMetrics().density); } @Override public void onPhotoTap(View view, float x, float y) { PixelPosition pixel = ImageUtils.getPixelPosition(x, y, imageWidthInPx, imageHeightInPx); - List clickableAreas = getClickAbleAreas(pixel.getX(), pixel.getY()); - for(ClickableArea ca : clickableAreas){ + List clickableAreas = getClickAbleAreas(pixel.getX(), pixel.getY()); + for(AbstractArea ca : clickableAreas){ listener.onClickableAreaTouched(ca.getItem()); } } - private List getClickAbleAreas(int x, int y){ - List clickableAreas= new ArrayList<>(); - for(ClickableArea ca : getClickableAreas()){ - if(isBetween(ca.getX(),(ca.getX()+ca.getW()),x)){ - if(isBetween(ca.getY(),(ca.getY()+ca.getH()),y)){ - clickableAreas.add(ca); - } + private List getClickAbleAreas(int x, int y){ + List clickableAreas= new ArrayList<>(); + for(AbstractArea ca : getClickableAreas()){ + if (ca.isInside(x, y)) { + clickableAreas.add(ca); } } return clickableAreas; } - private boolean isBetween(int start, int end, int actual){ - return (start <= actual && actual <= end); - } - - public void setClickableAreas(List clickableAreas) { + public void setClickableAreas(List clickableAreas) { this.clickableAreas = clickableAreas; } - public List getClickableAreas() { + public List getClickableAreas() { return clickableAreas; } } diff --git a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableCircleArea.kt b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableCircleArea.kt new file mode 100644 index 0000000..45f5522 --- /dev/null +++ b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableCircleArea.kt @@ -0,0 +1,25 @@ +package at.lukle.clickableareasimage + +import android.util.Log +import kotlin.math.sqrt + +public class ClickableCircleArea( + private val x: Int, + private val y: Int, + private val radius : Int, + item: T +) : AbstractArea(item) { + + override fun isInside(positionX: Int, positionY: Int): Boolean { + var ret = false + val dx = x - positionX + val dy = y - positionY + + // if tap is less than radius distance from the center + val d = sqrt((dx * dx + dy * dy).toDouble()).toFloat() + if (d <= radius) { + ret = true + } + return ret + } +} \ No newline at end of file diff --git a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickablePolyArea.kt b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickablePolyArea.kt new file mode 100644 index 0000000..d2db1b3 --- /dev/null +++ b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickablePolyArea.kt @@ -0,0 +1,27 @@ +package at.lukle.clickableareasimage + +public class ClickablePolyArea(item: T, + private vararg val points: PixelPosition) + : AbstractArea(item) { + + override fun isInside(positionX: Int, positionY: Int): Boolean { + var c = false + var j = points.size - 1 + for(i in points.indices) { + val pointI = points[i] + val pointJ = points[j] + + //case of point on the line + if ((positionX - pointI.x).toFloat() / (pointJ.x - pointI.x) == (positionY - pointI.y).toFloat() / (pointJ.y - pointI.y)) { + return true + } + + if (pointI.y > positionY != pointJ.y > positionY + && positionX < (pointJ.x - pointI.x) * (positionY - pointI.y)/ (pointJ.y - pointI.y) + pointI.x) { + c = !c + } + j = i + } + return c + } +} \ No newline at end of file diff --git a/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableRectangleArea.kt b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableRectangleArea.kt new file mode 100644 index 0000000..98d26e4 --- /dev/null +++ b/clickableareasimage/src/main/java/at/lukle/clickableareasimage/ClickableRectangleArea.kt @@ -0,0 +1,17 @@ +package at.lukle.clickableareasimage + +public class ClickableRectangleArea( + private val x: Int, + private val y: Int, + private val w: Int, + private val h: Int, + item: T) : AbstractArea(item) { + + override fun isInside(positionX: Int, positionY: Int): Boolean { + return isBetween(this.x, this.x + this.w, positionX) && isBetween(this.y, this.y + this.h, positionY) + } + + private fun isBetween(start: Int, end: Int, actual: Int): Boolean { + return actual in start..end + } +} \ No newline at end of file diff --git a/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableAreasImageTest.kt b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableAreasImageTest.kt new file mode 100644 index 0000000..90c6ef1 --- /dev/null +++ b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableAreasImageTest.kt @@ -0,0 +1,75 @@ +package at.likle.clickableareasimage + +import android.content.res.Resources +import android.graphics.drawable.BitmapDrawable +import android.util.DisplayMetrics +import android.widget.ImageView +import at.lukle.clickableareasimage.AbstractArea +import at.lukle.clickableareasimage.ClickableAreasImage +import at.lukle.clickableareasimage.OnClickableAreaClickedListener +import io.mockk.* +import io.mockk.MockKAnnotations.init +import io.mockk.impl.annotations.MockK +import org.junit.Test +import uk.co.senab.photoview.PhotoViewAttacher +import kotlin.collections.ArrayList + +class ClickableAreasImageTest { + + @MockK + lateinit var resources: Resources + @MockK + lateinit var displayMetrics: DisplayMetrics + + + init { + init(this) + } + + @Test + fun testTwoAreasInOneClick(){ + //before + mockkStatic("android.content.res.Resources") + every { Resources.getSystem() } returns resources + every { resources.displayMetrics } returns displayMetrics + displayMetrics.density = 100f + + val drawableMock = mockkClass(BitmapDrawable::class) + every { drawableMock.bitmap.width } returns 100 + every { drawableMock.bitmap.height } returns 100 + + val imageViewMock = mockkClass(ImageView::class) + every { imageViewMock.drawable } returns drawableMock + + val attacher = mockkClass(PhotoViewAttacher::class) + every { attacher.onPhotoTapListener = any() } returns Unit + every { attacher.imageView } returns imageViewMock + + val listener = spyk(OnClickableAreaClickedListener{}) + val areas = ClickableAreasImage(attacher, listener) + + val mockedArea1 = MockedArea(1, true) + val mockedArea2 = MockedArea(2, false) + val mockedArea3 = MockedArea(3, true) + + areas.clickableAreas = ArrayList>().apply { + add(mockedArea1) + add(mockedArea2) + add(mockedArea3) + } + + //action + areas.onPhotoTap(imageViewMock, 1f, 1f) + + //verify + verify(exactly = 1) { listener.onClickableAreaTouched(1)} + verify(exactly = 1) { listener.onClickableAreaTouched(3) } + verify(exactly = 0) { listener.onClickableAreaTouched(2) } + + } + + + class MockedArea(number: Int, private val isInside: Boolean) : AbstractArea(number) { + override fun isInside(positionX: Int, positionY: Int) = isInside + } +} \ No newline at end of file diff --git a/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableCircleAreaTest.kt b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableCircleAreaTest.kt new file mode 100644 index 0000000..0ac1994 --- /dev/null +++ b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableCircleAreaTest.kt @@ -0,0 +1,29 @@ +package at.likle.clickableareasimage + +import at.lukle.clickableareasimage.ClickableCircleArea +import org.junit.Test + +class ClickableCircleAreaTest { + + @Test + fun check_circle_inside_correct() { + //before + val circle = ClickableCircleArea(50, 50, 20, Object()) + + + //verefy + assert(circle.isInside(50,50)) + assert(circle.isInside(30,50)) + assert(circle.isInside(70,50)) + assert(circle.isInside(50,70)) + assert(circle.isInside(50,30)) + assert(circle.isInside(40,40)) + assert(circle.isInside(35, 63)) + + assert(!circle.isInside(30,30)) + assert(!circle.isInside(30,70)) + assert(!circle.isInside(70,30)) + assert(!circle.isInside(70,70)) + assert(!circle.isInside(35, 64)) + } +} \ No newline at end of file diff --git a/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickablePolyAreaTest.kt b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickablePolyAreaTest.kt new file mode 100644 index 0000000..269f778 --- /dev/null +++ b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickablePolyAreaTest.kt @@ -0,0 +1,30 @@ +package at.likle.clickableareasimage + +import at.lukle.clickableareasimage.ClickablePolyArea +import at.lukle.clickableareasimage.PixelPosition +import org.junit.Test + +class ClickablePolyAreaTest { + + @Test + fun check_poly_inside_correct() { + val poly = ClickablePolyArea(Object(), + PixelPosition(0,0), + PixelPosition(10,-5), + PixelPosition(30,10), + PixelPosition(20,5)) + + //verify + assert(poly.isInside(0,0)) + assert(poly.isInside(10,-5)) + assert(poly.isInside(30,10)) + assert(poly.isInside(20,5)) + + assert(poly.isInside(20,3)) + + assert(!poly.isInside(-1,-1)) + assert(!poly.isInside(5,-5)) + assert(!poly.isInside(30,5)) + assert(!poly.isInside(20,2)) + } +} \ No newline at end of file diff --git a/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableRectangleAreaTest.kt b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableRectangleAreaTest.kt new file mode 100644 index 0000000..e5a79e2 --- /dev/null +++ b/clickableareasimage/src/test/java/at/likle/clickableareasimage/ClickableRectangleAreaTest.kt @@ -0,0 +1,25 @@ +package at.likle.clickableareasimage + +import at.lukle.clickableareasimage.ClickableRectangleArea +import org.junit.Test + +class ClickableRectangleAreaTest { + + @Test + fun check_rectangle_inside_correct() { + val rectangle = ClickableRectangleArea(0,0,50,50, Object()) + + //verify + assert(rectangle.isInside(25,25)) + assert(rectangle.isInside(0,0)) + assert(rectangle.isInside(0,50)) + assert(rectangle.isInside(50,0)) + assert(rectangle.isInside(50,50)) + assert(rectangle.isInside(50,25)) + + assert(!rectangle.isInside(-1,-1)) + assert(!rectangle.isInside(20,51)) + assert(!rectangle.isInside(-1,25)) + assert(!rectangle.isInside(1000,1000)) + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e13e226..cbc6823 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 22 15:41:54 CEST 2015 +#Mon Sep 06 10:51:27 OMST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip