commit c2907459cd94b02b60830dac320c3c3d4e79e21b Author: aminecmi Date: Sat Jan 14 22:50:03 2023 +0100 Fork and kotlin port. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..e3f7dbe --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..360e6d4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PhotoView/.gitignore b/PhotoView/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/PhotoView/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/PhotoView/build.gradle b/PhotoView/build.gradle new file mode 100644 index 0000000..0e0419e --- /dev/null +++ b/PhotoView/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'bou.amine.apps.photoview' + compileSdk rootProject.ext.sdkVersion + + defaultConfig { + minSdk 18 + targetSdk rootProject.ext.sdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + // Flag to enable support for the new language APIs + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + // For Kotlin projects + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'com.google.android.material:material:1.7.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + +} \ No newline at end of file diff --git a/PhotoView/consumer-rules.pro b/PhotoView/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/PhotoView/proguard-rules.pro b/PhotoView/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/PhotoView/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/PhotoView/src/main/AndroidManifest.xml b/PhotoView/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/PhotoView/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/Compat.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/Compat.kt new file mode 100644 index 0000000..2cca7eb --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/Compat.kt @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bou.amine.apps.photoview + +import android.annotation.TargetApi +import android.os.Build +import android.view.View + +internal object Compat { + private const val SIXTY_FPS_INTERVAL = 1000 / 60 + @JvmStatic + fun postOnAnimation(view: View, runnable: Runnable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + postOnAnimationJellyBean(view, runnable) + } else { + view.postDelayed(runnable, SIXTY_FPS_INTERVAL.toLong()) + } + } + + @TargetApi(16) + private fun postOnAnimationJellyBean(view: View, runnable: Runnable) { + view.postOnAnimation(runnable) + } +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/CustomGestureDetector.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/CustomGestureDetector.kt new file mode 100755 index 0000000..3396b21 --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/CustomGestureDetector.kt @@ -0,0 +1,196 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bou.amine.apps.photoview + +import android.content.Context +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.VelocityTracker +import android.view.ViewConfiguration +import bou.amine.apps.photoview.CustomGestureDetector +import java.lang.Exception +import java.lang.IllegalArgumentException + +/** + * Does a whole lot of gesture detecting. + */ +internal class CustomGestureDetector(context: Context?, listener: OnGestureListener) { + private var mActivePointerId = INVALID_POINTER_ID + private var mActivePointerIndex = 0 + private val mDetector: ScaleGestureDetector + private var mVelocityTracker: VelocityTracker? = null + var isDragging = false + private set + private var mLastTouchX = 0f + private var mLastTouchY = 0f + private val mTouchSlop: Float + private val mMinimumVelocity: Float + private val mListener: OnGestureListener + + init { + val configuration = ViewConfiguration + .get(context!!) + mMinimumVelocity = configuration.scaledMinimumFlingVelocity.toFloat() + mTouchSlop = configuration.scaledTouchSlop.toFloat() + mListener = listener + val mScaleListener: ScaleGestureDetector.OnScaleGestureListener = + object : ScaleGestureDetector.OnScaleGestureListener { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scaleFactor = detector.scaleFactor + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false + mListener.onScale( + scaleFactor, + detector.focusX, detector.focusY + ) + return true + } + + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) { + // NO-OP + } + } + mDetector = ScaleGestureDetector(context, mScaleListener) + } + + private fun getActiveX(ev: MotionEvent): Float { + return try { + ev.getX(mActivePointerIndex) + } catch (e: Exception) { + ev.x + } + } + + private fun getActiveY(ev: MotionEvent): Float { + return try { + ev.getY(mActivePointerIndex) + } catch (e: Exception) { + ev.y + } + } + + val isScaling: Boolean + get() = mDetector.isInProgress + + fun onTouchEvent(ev: MotionEvent): Boolean { + return try { + mDetector.onTouchEvent(ev) + processTouchEvent(ev) + } catch (e: IllegalArgumentException) { + // Fix for support lib bug, happening when onDestroy is called + true + } + } + + private fun processTouchEvent(ev: MotionEvent): Boolean { + val action = ev.action + when (action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN -> { + mActivePointerId = ev.getPointerId(0) + mVelocityTracker = VelocityTracker.obtain() + if (null != mVelocityTracker) { + mVelocityTracker!!.addMovement(ev) + } + mLastTouchX = getActiveX(ev) + mLastTouchY = getActiveY(ev) + isDragging = false + } + MotionEvent.ACTION_MOVE -> { + val x = getActiveX(ev) + val y = getActiveY(ev) + val dx = x - mLastTouchX + val dy = y - mLastTouchY + if (!isDragging) { + // Use Pythagoras to see if drag length is larger than + // touch slop + isDragging = Math.sqrt((dx * dx + dy * dy).toDouble()) >= mTouchSlop + } + if (isDragging) { + mListener.onDrag(dx, dy) + mLastTouchX = x + mLastTouchY = y + if (null != mVelocityTracker) { + mVelocityTracker!!.addMovement(ev) + } + } + } + MotionEvent.ACTION_CANCEL -> { + mActivePointerId = INVALID_POINTER_ID + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker!!.recycle() + mVelocityTracker = null + } + } + MotionEvent.ACTION_UP -> { + mActivePointerId = INVALID_POINTER_ID + if (isDragging) { + if (null != mVelocityTracker) { + mLastTouchX = getActiveX(ev) + mLastTouchY = getActiveY(ev) + + // Compute velocity within the last 1000ms + mVelocityTracker!!.addMovement(ev) + mVelocityTracker!!.computeCurrentVelocity(1000) + val vX = mVelocityTracker!!.xVelocity + val vY = mVelocityTracker!! + .yVelocity + + // If the velocity is greater than minVelocity, call + // listener + if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { + mListener.onFling( + mLastTouchX, mLastTouchY, -vX, + -vY + ) + } + } + } + + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker!!.recycle() + mVelocityTracker = null + } + } + MotionEvent.ACTION_POINTER_UP -> { + val pointerIndex = Util.getPointerIndex(ev.action) + val pointerId = ev.getPointerId(pointerIndex) + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + val newPointerIndex = if (pointerIndex == 0) 1 else 0 + mActivePointerId = ev.getPointerId(newPointerIndex) + mLastTouchX = ev.getX(newPointerIndex) + mLastTouchY = ev.getY(newPointerIndex) + } + } + } + mActivePointerIndex = ev + .findPointerIndex(if (mActivePointerId != INVALID_POINTER_ID) mActivePointerId else 0) + return true + } + + companion object { + private const val INVALID_POINTER_ID = -1 + } +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnGestureListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnGestureListener.kt new file mode 100644 index 0000000..b0c842a --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnGestureListener.kt @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bou.amine.apps.photoview + +internal interface OnGestureListener { + fun onDrag(dx: Float, dy: Float) + fun onFling( + startX: Float, startY: Float, velocityX: Float, + velocityY: Float + ) + + fun onScale(scaleFactor: Float, focusX: Float, focusY: Float) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnMatrixChangedListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnMatrixChangedListener.kt new file mode 100644 index 0000000..d68323b --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnMatrixChangedListener.kt @@ -0,0 +1,17 @@ +package bou.amine.apps.photoview + +import android.graphics.RectF + +/** + * Interface definition for a callback to be invoked when the internal Matrix has changed for + * this View. + */ +interface OnMatrixChangedListener { + /** + * Callback for when the Matrix displaying the Drawable has changed. This could be because + * the View's bounds have changed, or the user has zoomed. + * + * @param rect - Rectangle displaying the Drawable's new bounds. + */ + fun onMatrixChanged(rect: RectF?) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnOutsidePhotoTapListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnOutsidePhotoTapListener.kt new file mode 100644 index 0000000..86c6edb --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnOutsidePhotoTapListener.kt @@ -0,0 +1,13 @@ +package bou.amine.apps.photoview + +import android.widget.ImageView + +/** + * Callback when the user tapped outside of the photo + */ +interface OnOutsidePhotoTapListener { + /** + * The outside of the photo has been tapped + */ + fun onOutsidePhotoTap(imageView: ImageView?) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnPhotoTapListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnPhotoTapListener.kt new file mode 100644 index 0000000..9ef9105 --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnPhotoTapListener.kt @@ -0,0 +1,21 @@ +package bou.amine.apps.photoview + +import android.widget.ImageView + +/** + * A callback to be invoked when the Photo is tapped with a single + * tap. + */ +interface OnPhotoTapListener { + /** + * A callback to receive where the user taps on a photo. You will only receive a callback if + * the user taps on the actual photo, tapping on 'whitespace' will be ignored. + * + * @param view ImageView the user tapped. + * @param x where the user tapped from the of the Drawable, as percentage of the + * Drawable width. + * @param y where the user tapped from the top of the Drawable, as percentage of the + * Drawable height. + */ + fun onPhotoTap(view: ImageView?, x: Float, y: Float) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnScaleChangedListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnScaleChangedListener.kt new file mode 100644 index 0000000..47f84f3 --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnScaleChangedListener.kt @@ -0,0 +1,15 @@ +package bou.amine.apps.photoview + +/** + * Interface definition for callback to be invoked when attached ImageView scale changes + */ +interface OnScaleChangedListener { + /** + * Callback for when the scale changes + * + * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in) + * @param focusX focal point X position + * @param focusY focal point Y position + */ + fun onScaleChange(scaleFactor: Float, focusX: Float, focusY: Float) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnSingleFlingListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnSingleFlingListener.kt new file mode 100644 index 0000000..0d05581 --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnSingleFlingListener.kt @@ -0,0 +1,20 @@ +package bou.amine.apps.photoview + +import android.view.MotionEvent + +/** + * A callback to be invoked when the ImageView is flung with a single + * touch + */ +interface OnSingleFlingListener { + /** + * A callback to receive where the user flings on a ImageView. You will receive a callback if + * the user flings anywhere on the view. + * + * @param e1 MotionEvent the user first touch. + * @param e2 MotionEvent the user last touch. + * @param velocityX distance of user's horizontal fling. + * @param velocityY distance of user's vertical fling. + */ + fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnViewDragListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnViewDragListener.kt new file mode 100644 index 0000000..ebba2aa --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnViewDragListener.kt @@ -0,0 +1,15 @@ +package bou.amine.apps.photoview + +/** + * Interface definition for a callback to be invoked when the photo is experiencing a drag event + */ +interface OnViewDragListener { + /** + * Callback for when the photo is experiencing a drag event. This cannot be invoked when the + * user is scaling. + * + * @param dx The change of the coordinates in the x-direction + * @param dy The change of the coordinates in the y-direction + */ + fun onDrag(dx: Float, dy: Float) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/OnViewTapListener.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/OnViewTapListener.kt new file mode 100644 index 0000000..40a0267 --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/OnViewTapListener.kt @@ -0,0 +1,15 @@ +package bou.amine.apps.photoview + +import android.view.View + +interface OnViewTapListener { + /** + * A callback to receive where the user taps on a ImageView. You will receive a callback if + * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. + * + * @param view - View the user tapped. + * @param x - where the user tapped from the left of the View. + * @param y - where the user tapped from the top of the View. + */ + fun onViewTap(view: View?, x: Float, y: Float) +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/PhotoView.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/PhotoView.kt new file mode 100644 index 0000000..9f49fe5 --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/PhotoView.kt @@ -0,0 +1,233 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bou.amine.apps.photoview + +import android.content.Context +import android.graphics.Matrix +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.View +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView + +/** + * A zoomable ImageView. See [PhotoViewAttacher] for most of the details on how the zooming + * is accomplished + */ +class PhotoView @JvmOverloads constructor( + context: Context?, + attr: AttributeSet? = null, + defStyle: Int = 0 +) : AppCompatImageView(context!!, attr, defStyle) { + /** + * Get the current [PhotoViewAttacher] for this view. Be wary of holding on to references + * to this attacher, as it has a reference to this view, which, if a reference is held in the + * wrong place, can cause memory leaks. + * + * @return the attacher. + */ + var attacher: PhotoViewAttacher? = null + private set + private var pendingScaleType: ScaleType? = null + + init { + init() + } + + private fun init() { + attacher = PhotoViewAttacher(this) + //We always pose as a Matrix scale type, though we can change to another scale type + //via the attacher + super.setScaleType(ImageView.ScaleType.MATRIX) + //apply the previously applied scale type + if (pendingScaleType != null) { + scaleType = pendingScaleType as ScaleType + pendingScaleType = null + } + } + + override fun getScaleType(): ScaleType { + return attacher!!.scaleType + } + + override fun setScaleType(scaleType: ScaleType?) { + if (attacher == null) { + pendingScaleType = scaleType + } else { + if (scaleType != null) { + attacher!!.scaleType = scaleType + } + } + } + + override fun getImageMatrix(): Matrix = attacher!!.imageMatrix + + override fun setOnLongClickListener(l: View.OnLongClickListener?) { + attacher!!.setOnLongClickListener(l) + } + + override fun setOnClickListener(l: View.OnClickListener?) { + attacher!!.setOnClickListener(l) + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + // setImageBitmap calls through to this method + if (attacher != null) { + attacher!!.update() + } + } + + override fun setImageResource(resId: Int) { + super.setImageResource(resId) + if (attacher != null) { + attacher!!.update() + } + } + + override fun setImageURI(uri: Uri?) { + super.setImageURI(uri) + if (attacher != null) { + attacher!!.update() + } + } + + protected override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean { + val changed: Boolean = super.setFrame(l, t, r, b) + if (changed) { + attacher!!.update() + } + return changed + } + + fun setRotationTo(rotationDegree: Float) { + attacher!!.setRotationTo(rotationDegree) + } + + fun setRotationBy(rotationDegree: Float) { + attacher!!.setRotationBy(rotationDegree) + } + + @get:Deprecated("") + val isZoomEnabled: Boolean + get() = attacher!!.isZoomEnabled + var isZoomable: Boolean + get() = attacher!!.isZoomable + set(zoomable) { + attacher!!.isZoomable = zoomable + } + val displayRect: RectF + get() = attacher!!.displayRect!! + + fun getDisplayMatrix(matrix: Matrix?) { + if (matrix != null) { + attacher!!.getDisplayMatrix(matrix) + } + } + + fun setDisplayMatrix(finalRectangle: Matrix?): Boolean { + return attacher!!.setDisplayMatrix(finalRectangle) + } + + fun getSuppMatrix(matrix: Matrix?) { + if (matrix != null) { + attacher!!.getSuppMatrix(matrix) + } + } + + fun setSuppMatrix(matrix: Matrix?): Boolean { + return attacher!!.setDisplayMatrix(matrix) + } + + var minimumScale: Float + get() = attacher!!.minimumScale + set(minimumScale) { + attacher!!.minimumScale = minimumScale + } + var mediumScale: Float + get() = attacher!!.mediumScale + set(mediumScale) { + attacher!!.mediumScale = mediumScale + } + var maximumScale: Float + get() = attacher!!.maximumScale + set(maximumScale) { + attacher!!.maximumScale = maximumScale + } + var scale: Float + get() = attacher!!.scale + set(scale) { + attacher!!.scale = scale + } + + fun setAllowParentInterceptOnEdge(allow: Boolean) { + attacher!!.setAllowParentInterceptOnEdge(allow) + } + + fun setScaleLevels(minimumScale: Float, mediumScale: Float, maximumScale: Float) { + attacher!!.setScaleLevels(minimumScale, mediumScale, maximumScale) + } + + fun setOnMatrixChangeListener(listener: OnMatrixChangedListener?) { + attacher!!.setOnMatrixChangeListener(listener) + } + + fun setOnPhotoTapListener(listener: OnPhotoTapListener?) { + attacher!!.setOnPhotoTapListener(listener) + } + + fun setOnOutsidePhotoTapListener(listener: OnOutsidePhotoTapListener?) { + attacher!!.setOnOutsidePhotoTapListener(listener) + } + + fun setOnViewTapListener(listener: OnViewTapListener?) { + attacher!!.setOnViewTapListener(listener) + } + + fun setOnViewDragListener(listener: OnViewDragListener?) { + attacher!!.setOnViewDragListener(listener) + } + + fun setScale(scale: Float, animate: Boolean) { + attacher!!.setScale(scale, animate) + } + + fun setScale(scale: Float, focalX: Float, focalY: Float, animate: Boolean) { + attacher!!.setScale(scale, focalX, focalY, animate) + } + + fun setZoomTransitionDuration(milliseconds: Int) { + attacher!!.setZoomTransitionDuration(milliseconds) + } + + fun setOnDoubleTapListener(onDoubleTapListener: GestureDetector.OnDoubleTapListener?) { + attacher!!.setOnDoubleTapListener(onDoubleTapListener) + } + + fun setOnScaleChangeListener(onScaleChangedListener: OnScaleChangedListener?) { + attacher!!.setOnScaleChangeListener(onScaleChangedListener) + } + + fun setOnSingleFlingListener(onSingleFlingListener: OnSingleFlingListener?) { + attacher!!.setOnSingleFlingListener(onSingleFlingListener) + } +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/PhotoViewAttacher.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/PhotoViewAttacher.kt new file mode 100644 index 0000000..204b6ee --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/PhotoViewAttacher.kt @@ -0,0 +1,802 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package bou.amine.apps.photoview + +import android.content.Context +import android.graphics.Matrix +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Interpolator +import android.widget.ImageView +import android.widget.OverScroller +import bou.amine.apps.photoview.Util.hasDrawable +import bou.amine.apps.photoview.Util.checkZoomLevels +import bou.amine.apps.photoview.Util.isSupportedScaleType +import bou.amine.apps.photoview.Compat.postOnAnimation + +/** + * The component of [PhotoView] which does the work allowing for zooming, scaling, panning, etc. + * It is made public in case you need to subclass something other than AppCompatImageView and still + * gain the functionality that [PhotoView] offers + */ +class PhotoViewAttacher(private val mImageView: ImageView) : View.OnTouchListener, + View.OnLayoutChangeListener { + private var mInterpolator: Interpolator = AccelerateDecelerateInterpolator() + private var mZoomDuration = DEFAULT_ZOOM_DURATION + private var mMinScale = DEFAULT_MIN_SCALE + private var mMidScale = DEFAULT_MID_SCALE + private var mMaxScale = DEFAULT_MAX_SCALE + private var mAllowParentInterceptOnEdge = true + private var mBlockParentIntercept = false + + // Gesture Detectors + private var mGestureDetector: GestureDetector? = null + private var mScaleDragDetector: CustomGestureDetector? = null + + // These are set so we don't keep allocating them on the heap + private val mBaseMatrix = Matrix() + val imageMatrix = Matrix() + private val mSuppMatrix = Matrix() + private val mDisplayRect = RectF() + private val mMatrixValues = FloatArray(9) + + // Listeners + private var mMatrixChangeListener: OnMatrixChangedListener? = null + private var mPhotoTapListener: OnPhotoTapListener? = null + private var mOutsidePhotoTapListener: OnOutsidePhotoTapListener? = null + private var mViewTapListener: OnViewTapListener? = null + private var mOnClickListener: View.OnClickListener? = null + private var mLongClickListener: View.OnLongClickListener? = null + private var mScaleChangeListener: OnScaleChangedListener? = null + private var mSingleFlingListener: OnSingleFlingListener? = null + private var mOnViewDragListener: OnViewDragListener? = null + private var mCurrentFlingRunnable: FlingRunnable? = null + private var mScrollEdge = EDGE_BOTH + private var mBaseRotation: Float = 0.0f + + @get:Deprecated("") + var isZoomEnabled = true + private set + private var mScaleType = ImageView.ScaleType.FIT_CENTER + private val onGestureListener: OnGestureListener = object : OnGestureListener { + override fun onDrag(dx: Float, dy: Float) { + if (mScaleDragDetector!!.isScaling) { + return // Do not drag if we are already scaling + } + if (mOnViewDragListener != null) { + mOnViewDragListener!!.onDrag(dx, dy) + } + mSuppMatrix.postTranslate(dx, dy) + checkAndDisplayMatrix() + + /* + * Here we decide whether to let the ImageView's parent to start taking + * over the touch event. + * + * First we check whether this function is enabled. We never want the + * parent to take over if we're scaling. We then check the edge we're + * on, and the direction of the scroll (i.e. if we're pulling against + * the edge, aka 'overscrolling', let the parent take over). + */ + val parent = mImageView.parent + if (mAllowParentInterceptOnEdge && !mScaleDragDetector!!.isScaling && !mBlockParentIntercept) { + if (mScrollEdge == EDGE_BOTH || mScrollEdge == EDGE_LEFT && dx >= 1f + || mScrollEdge == EDGE_RIGHT && dx <= -1f + ) { + parent?.requestDisallowInterceptTouchEvent(false) + } + } else { + parent?.requestDisallowInterceptTouchEvent(true) + } + } + + override fun onFling(startX: Float, startY: Float, velocityX: Float, velocityY: Float) { + mCurrentFlingRunnable = FlingRunnable(mImageView.context) + mCurrentFlingRunnable!!.fling( + getImageViewWidth(mImageView), + getImageViewHeight(mImageView), velocityX.toInt(), velocityY.toInt() + ) + mImageView.post(mCurrentFlingRunnable) + } + + override fun onScale(scaleFactor: Float, focusX: Float, focusY: Float) { + if ((scale < mMaxScale || scaleFactor < 1f) && (scale > mMinScale || scaleFactor > 1f)) { + if (mScaleChangeListener != null) { + mScaleChangeListener!!.onScaleChange(scaleFactor, focusX, focusY) + } + mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY) + checkAndDisplayMatrix() + } + } + } + + init { + mImageView.setOnTouchListener(this) + mImageView.addOnLayoutChangeListener(this) + if (!mImageView.isInEditMode) { + + mBaseRotation = 0.0f + + // Create Gesture Detectors... + mScaleDragDetector = CustomGestureDetector(mImageView.context, onGestureListener) + mGestureDetector = + GestureDetector(mImageView.context, object : GestureDetector.SimpleOnGestureListener() { + // forward long click listener + override fun onLongPress(e: MotionEvent) { + if (mLongClickListener != null) { + mLongClickListener!!.onLongClick(mImageView) + } + } + + override fun onFling( + e1: MotionEvent, e2: MotionEvent, + velocityX: Float, velocityY: Float + ): Boolean { + if (mSingleFlingListener != null) { + if (scale > DEFAULT_MIN_SCALE) { + return false + } + return if (e1.pointerCount > SINGLE_TOUCH + || e1.pointerCount > SINGLE_TOUCH + ) { + false + } else mSingleFlingListener!!.onFling(e1, e2, velocityX, velocityY) + } + return false + } + }) + mGestureDetector!!.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener { + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (mOnClickListener != null) { + mOnClickListener!!.onClick(mImageView) + } + val displayRect = displayRect + val x = e.x + val y = e.y + if (mViewTapListener != null) { + mViewTapListener!!.onViewTap(mImageView, x, y) + } + if (displayRect != null) { + + // Check to see if the user tapped on the photo + if (displayRect.contains(x, y)) { + val xResult = ((x - displayRect.left) + / displayRect.width()) + val yResult = ((y - displayRect.top) + / displayRect.height()) + if (mPhotoTapListener != null) { + mPhotoTapListener!!.onPhotoTap(mImageView, xResult, yResult) + } + return true + } else { + if (mOutsidePhotoTapListener != null) { + mOutsidePhotoTapListener!!.onOutsidePhotoTap(mImageView) + } + } + } + return false + } + + override fun onDoubleTap(ev: MotionEvent): Boolean { + try { + val scale = scale + val x = ev.x + val y = ev.y + if (scale < mediumScale) { + setScale(mediumScale, x, y, true) + } else if (scale >= mediumScale && scale < maximumScale) { + setScale(maximumScale, x, y, true) + } else { + setScale(minimumScale, x, y, true) + } + } catch (e: ArrayIndexOutOfBoundsException) { + // Can sometimes happen when getX() and getY() is called + } + return true + } + + override fun onDoubleTapEvent(e: MotionEvent): Boolean { + // Wait for the confirmed onDoubleTap() instead + return false + } + }) + } + } + + fun setOnDoubleTapListener(newOnDoubleTapListener: GestureDetector.OnDoubleTapListener?) { + mGestureDetector!!.setOnDoubleTapListener(newOnDoubleTapListener) + } + + fun setOnScaleChangeListener(onScaleChangeListener: OnScaleChangedListener?) { + mScaleChangeListener = onScaleChangeListener + } + + fun setOnSingleFlingListener(onSingleFlingListener: OnSingleFlingListener?) { + mSingleFlingListener = onSingleFlingListener + } + + val displayRect: RectF? + get() { + checkMatrixBounds() + return getDisplayRect(drawMatrix) + } + + fun setDisplayMatrix(finalMatrix: Matrix?): Boolean { + requireNotNull(finalMatrix) { "Matrix cannot be null" } + if (mImageView.drawable == null) { + return false + } + mSuppMatrix.set(finalMatrix) + checkAndDisplayMatrix() + return true + } + + fun setBaseRotation(degrees: Float) { + mBaseRotation = degrees % 360 + update() + setRotationBy(mBaseRotation) + checkAndDisplayMatrix() + } + + fun setRotationTo(degrees: Float) { + mSuppMatrix.setRotate(degrees % 360) + checkAndDisplayMatrix() + } + + fun setRotationBy(degrees: Float) { + mSuppMatrix.postRotate(degrees % 360) + checkAndDisplayMatrix() + } + + var minimumScale: Float + get() = mMinScale + set(minimumScale) { + checkZoomLevels(minimumScale, mMidScale, mMaxScale) + mMinScale = minimumScale + } + var mediumScale: Float + get() = mMidScale + set(mediumScale) { + checkZoomLevels(mMinScale, mediumScale, mMaxScale) + mMidScale = mediumScale + } + var maximumScale: Float + get() = mMaxScale + set(maximumScale) { + checkZoomLevels(mMinScale, mMidScale, maximumScale) + mMaxScale = maximumScale + } + var scale: Float + get() = Math.sqrt( + (Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X).toDouble(), 2.0).toFloat() + Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y).toDouble(), + 2.0 + ).toFloat()).toDouble() + ).toFloat() + set(scale) { + setScale(scale, false) + } + var scaleType: ImageView.ScaleType + get() = mScaleType + set(scaleType) { + if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { + mScaleType = scaleType + update() + } + } + + override fun onLayoutChange( + v: View, + left: Int, + top: Int, + right: Int, + bottom: Int, + oldLeft: Int, + oldTop: Int, + oldRight: Int, + oldBottom: Int + ) { + // Update our base matrix, as the bounds have changed + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { + updateBaseMatrix(mImageView.drawable) + } + } + + override fun onTouch(v: View, ev: MotionEvent): Boolean { + var handled = false + if (isZoomEnabled && hasDrawable((v as ImageView))) { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + val parent = v.getParent() + // First, disable the Parent from intercepting the touch + // event + parent?.requestDisallowInterceptTouchEvent(true) + + // If we're flinging, and the user presses down, cancel + // fling + cancelFling() + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> // If the user has zoomed less than min scale, zoom back + // to min scale + if (scale < mMinScale) { + val rect = displayRect + if (rect != null) { + v.post( + AnimatedZoomRunnable( + scale, mMinScale, + rect.centerX(), rect.centerY() + ) + ) + handled = true + } + } else if (scale > mMaxScale) { + val rect = displayRect + if (rect != null) { + v.post( + AnimatedZoomRunnable( + scale, mMaxScale, + rect.centerX(), rect.centerY() + ) + ) + handled = true + } + } + } + + // Try the Scale/Drag detector + if (mScaleDragDetector != null) { + val wasScaling = mScaleDragDetector!!.isScaling + val wasDragging = mScaleDragDetector!!.isDragging + handled = mScaleDragDetector!!.onTouchEvent(ev) + val didntScale = !wasScaling && !mScaleDragDetector!!.isScaling + val didntDrag = !wasDragging && !mScaleDragDetector!!.isDragging + mBlockParentIntercept = didntScale && didntDrag + } + + // Check to see if the user double tapped + if (mGestureDetector != null && mGestureDetector!!.onTouchEvent(ev)) { + handled = true + } + } + return handled + } + + fun setAllowParentInterceptOnEdge(allow: Boolean) { + mAllowParentInterceptOnEdge = allow + } + + fun setScaleLevels(minimumScale: Float, mediumScale: Float, maximumScale: Float) { + checkZoomLevels(minimumScale, mediumScale, maximumScale) + mMinScale = minimumScale + mMidScale = mediumScale + mMaxScale = maximumScale + } + + fun setOnLongClickListener(listener: View.OnLongClickListener?) { + mLongClickListener = listener + } + + fun setOnClickListener(listener: View.OnClickListener?) { + mOnClickListener = listener + } + + fun setOnMatrixChangeListener(listener: OnMatrixChangedListener?) { + mMatrixChangeListener = listener + } + + fun setOnPhotoTapListener(listener: OnPhotoTapListener?) { + mPhotoTapListener = listener + } + + fun setOnOutsidePhotoTapListener(mOutsidePhotoTapListener: OnOutsidePhotoTapListener?) { + this.mOutsidePhotoTapListener = mOutsidePhotoTapListener + } + + fun setOnViewTapListener(listener: OnViewTapListener?) { + mViewTapListener = listener + } + + fun setOnViewDragListener(listener: OnViewDragListener?) { + mOnViewDragListener = listener + } + + fun setScale(scale: Float, animate: Boolean) { + setScale( + scale, ( + mImageView.right / 2).toFloat(), ( + mImageView.bottom / 2).toFloat(), + animate + ) + } + + fun setScale( + scale: Float, focalX: Float, focalY: Float, + animate: Boolean + ) { + // Check to see if the scale is within bounds + require(!(scale < mMinScale || scale > mMaxScale)) { "Scale must be within the range of minScale and maxScale" } + if (animate) { + mImageView.post( + AnimatedZoomRunnable( + scale, scale, + focalX, focalY + ) + ) + } else { + mSuppMatrix.setScale(scale, scale, focalX, focalY) + checkAndDisplayMatrix() + } + } + + /** + * Set the zoom interpolator + * + * @param interpolator the zoom interpolator + */ + fun setZoomInterpolator(interpolator: Interpolator) { + mInterpolator = interpolator + } + + var isZoomable: Boolean + get() = isZoomEnabled + set(zoomable) { + isZoomEnabled = zoomable + update() + } + + fun update() { + if (isZoomEnabled) { + // Update the base matrix using the current drawable + updateBaseMatrix(mImageView.drawable) + } else { + // Reset the Matrix... + resetMatrix() + } + } + + /** + * Get the display matrix + * + * @param matrix target matrix to copy to + */ + fun getDisplayMatrix(matrix: Matrix) { + matrix.set(drawMatrix) + } + + /** + * Get the current support matrix + */ + fun getSuppMatrix(matrix: Matrix) { + matrix.set(mSuppMatrix) + } + + private val drawMatrix: Matrix + private get() { + imageMatrix.set(mBaseMatrix) + imageMatrix.postConcat(mSuppMatrix) + return imageMatrix + } + + fun setZoomTransitionDuration(milliseconds: Int) { + mZoomDuration = milliseconds + } + + /** + * Helper method that 'unpacks' a Matrix and returns the required value + * + * @param matrix Matrix to unpack + * @param whichValue Which value from Matrix.M* to return + * @return returned value + */ + private fun getValue(matrix: Matrix, whichValue: Int): Float { + matrix.getValues(mMatrixValues) + return mMatrixValues[whichValue] + } + + /** + * Resets the Matrix back to FIT_CENTER, and then displays its contents + */ + private fun resetMatrix() { + mSuppMatrix.reset() + setRotationBy(mBaseRotation) + setImageViewMatrix(drawMatrix) + checkMatrixBounds() + } + + private fun setImageViewMatrix(matrix: Matrix) { + mImageView.imageMatrix = matrix + + // Call MatrixChangedListener if needed + if (mMatrixChangeListener != null) { + val displayRect = getDisplayRect(matrix) + if (displayRect != null) { + mMatrixChangeListener!!.onMatrixChanged(displayRect) + } + } + } + + /** + * Helper method that simply checks the Matrix, and then displays the result + */ + private fun checkAndDisplayMatrix() { + if (checkMatrixBounds()) { + setImageViewMatrix(drawMatrix) + } + } + + /** + * Helper method that maps the supplied Matrix to the current Drawable + * + * @param matrix - Matrix to map Drawable against + * @return RectF - Displayed Rectangle + */ + private fun getDisplayRect(matrix: Matrix): RectF? { + val d = mImageView.drawable + if (d != null) { + mDisplayRect[0f, 0f, d.intrinsicWidth.toFloat()] = d.intrinsicHeight.toFloat() + matrix.mapRect(mDisplayRect) + return mDisplayRect + } + return null + } + + /** + * Calculate Matrix for FIT_CENTER + * + * @param drawable - Drawable being displayed + */ + private fun updateBaseMatrix(drawable: Drawable?) { + if (drawable == null) { + return + } + val viewWidth = getImageViewWidth(mImageView).toFloat() + val viewHeight = getImageViewHeight(mImageView).toFloat() + val drawableWidth = drawable.intrinsicWidth + val drawableHeight = drawable.intrinsicHeight + mBaseMatrix.reset() + val widthScale = viewWidth / drawableWidth + val heightScale = viewHeight / drawableHeight + if (mScaleType == ImageView.ScaleType.CENTER) { + mBaseMatrix.postTranslate( + (viewWidth - drawableWidth) / 2f, + (viewHeight - drawableHeight) / 2f + ) + } else if (mScaleType == ImageView.ScaleType.CENTER_CROP) { + val scale = Math.max(widthScale, heightScale) + mBaseMatrix.postScale(scale, scale) + mBaseMatrix.postTranslate( + (viewWidth - drawableWidth * scale) / 2f, + (viewHeight - drawableHeight * scale) / 2f + ) + } else if (mScaleType == ImageView.ScaleType.CENTER_INSIDE) { + val scale = Math.min(1.0f, Math.min(widthScale, heightScale)) + mBaseMatrix.postScale(scale, scale) + mBaseMatrix.postTranslate( + (viewWidth - drawableWidth * scale) / 2f, + (viewHeight - drawableHeight * scale) / 2f + ) + } else { + var mTempSrc = RectF(0F, 0F, drawableWidth.toFloat(), drawableHeight.toFloat()) + val mTempDst = RectF(0F, 0F, viewWidth, viewHeight) + if (mBaseRotation.toInt() % 180 != 0) { + mTempSrc = RectF(0F, 0F, drawableHeight.toFloat(), drawableWidth.toFloat()) + } + when (mScaleType) { + ImageView.ScaleType.FIT_CENTER -> mBaseMatrix.setRectToRect( + mTempSrc, + mTempDst, + Matrix.ScaleToFit.CENTER + ) + ImageView.ScaleType.FIT_START -> mBaseMatrix.setRectToRect( + mTempSrc, + mTempDst, + Matrix.ScaleToFit.START + ) + ImageView.ScaleType.FIT_END -> mBaseMatrix.setRectToRect( + mTempSrc, + mTempDst, + Matrix.ScaleToFit.END + ) + ImageView.ScaleType.FIT_XY -> mBaseMatrix.setRectToRect( + mTempSrc, + mTempDst, + Matrix.ScaleToFit.FILL + ) + else -> {} + } + } + resetMatrix() + } + + private fun checkMatrixBounds(): Boolean { + val rect = getDisplayRect(drawMatrix) ?: return false + val height = rect.height() + val width = rect.width() + var deltaX = 0f + var deltaY = 0f + val viewHeight = getImageViewHeight(mImageView) + if (height <= viewHeight) { + deltaY = when (mScaleType) { + ImageView.ScaleType.FIT_START -> -rect.top + ImageView.ScaleType.FIT_END -> viewHeight - height - rect.top + else -> (viewHeight - height) / 2 - rect.top + } + } else if (rect.top > 0) { + deltaY = -rect.top + } else if (rect.bottom < viewHeight) { + deltaY = viewHeight - rect.bottom + } + val viewWidth = getImageViewWidth(mImageView) + if (width <= viewWidth) { + deltaX = when (mScaleType) { + ImageView.ScaleType.FIT_START -> -rect.left + ImageView.ScaleType.FIT_END -> viewWidth - width - rect.left + else -> (viewWidth - width) / 2 - rect.left + } + mScrollEdge = EDGE_BOTH + } else if (rect.left > 0) { + mScrollEdge = EDGE_LEFT + deltaX = -rect.left + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right + mScrollEdge = EDGE_RIGHT + } else { + mScrollEdge = EDGE_NONE + } + + // Finally actually translate the matrix + mSuppMatrix.postTranslate(deltaX, deltaY) + return true + } + + private fun getImageViewWidth(imageView: ImageView): Int { + return imageView.width - imageView.paddingLeft - imageView.paddingRight + } + + private fun getImageViewHeight(imageView: ImageView): Int { + return imageView.height - imageView.paddingTop - imageView.paddingBottom + } + + private fun cancelFling() { + if (mCurrentFlingRunnable != null) { + mCurrentFlingRunnable!!.cancelFling() + mCurrentFlingRunnable = null + } + } + + private inner class AnimatedZoomRunnable( + currentZoom: Float, targetZoom: Float, + private val mFocalX: Float, private val mFocalY: Float + ) : Runnable { + private val mStartTime: Long + private val mZoomStart: Float + private val mZoomEnd: Float + + init { + mStartTime = System.currentTimeMillis() + mZoomStart = currentZoom + mZoomEnd = targetZoom + } + + override fun run() { + val t = interpolate() + val scale = mZoomStart + t * (mZoomEnd - mZoomStart) + val deltaScale = scale / scale + onGestureListener.onScale(deltaScale, mFocalX, mFocalY) + + // We haven't hit our target scale yet, so post ourselves again + if (t < 1f) { + postOnAnimation(mImageView, this) + } + } + + private fun interpolate(): Float { + var t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration + t = Math.min(1f, t) + t = mInterpolator.getInterpolation(t) + return t + } + } + + private inner class FlingRunnable(context: Context?) : Runnable { + private val mScroller: OverScroller + private var mCurrentX = 0 + private var mCurrentY = 0 + + init { + mScroller = OverScroller(context) + } + + fun cancelFling() { + mScroller.forceFinished(true) + } + + fun fling( + viewWidth: Int, viewHeight: Int, velocityX: Int, + velocityY: Int + ) { + val rect = displayRect ?: return + val startX = Math.round(-rect.left) + val minX: Int + val maxX: Int + val minY: Int + val maxY: Int + if (viewWidth < rect.width()) { + minX = 0 + maxX = Math.round(rect.width() - viewWidth) + } else { + maxX = startX + minX = maxX + } + val startY = Math.round(-rect.top) + if (viewHeight < rect.height()) { + minY = 0 + maxY = Math.round(rect.height() - viewHeight) + } else { + maxY = startY + minY = maxY + } + mCurrentX = startX + mCurrentY = startY + + // If we actually can move, fling the scroller + if (startX != maxX || startY != maxY) { + mScroller.fling( + startX, startY, velocityX, velocityY, minX, + maxX, minY, maxY, 0, 0 + ) + } + } + + override fun run() { + if (mScroller.isFinished) { + return // remaining post that should not be handled + } + if (mScroller.computeScrollOffset()) { + val newX = mScroller.currX + val newY = mScroller.currY + mSuppMatrix.postTranslate( + (mCurrentX - newX).toFloat(), + (mCurrentY - newY).toFloat() + ) + checkAndDisplayMatrix() + mCurrentX = newX + mCurrentY = newY + + // Post On animation + postOnAnimation(mImageView, this) + } + } + } + + companion object { + private const val DEFAULT_MAX_SCALE = 3.0f + private const val DEFAULT_MID_SCALE = 1.75f + private const val DEFAULT_MIN_SCALE = 1.0f + private const val DEFAULT_ZOOM_DURATION = 200 + private const val EDGE_NONE = -1 + private const val EDGE_LEFT = 0 + private const val EDGE_RIGHT = 1 + private const val EDGE_BOTH = 2 + private const val SINGLE_TOUCH = 1 + } +} \ No newline at end of file diff --git a/PhotoView/src/main/java/bou/amine/apps/photoview/Util.kt b/PhotoView/src/main/java/bou/amine/apps/photoview/Util.kt new file mode 100644 index 0000000..4eaefff --- /dev/null +++ b/PhotoView/src/main/java/bou/amine/apps/photoview/Util.kt @@ -0,0 +1,37 @@ +package bou.amine.apps.photoview + +import android.view.MotionEvent +import android.widget.ImageView +import java.lang.IllegalStateException + +internal object Util { + @JvmStatic + fun checkZoomLevels( + minZoom: Float, midZoom: Float, + maxZoom: Float + ) { + require(minZoom < midZoom) { "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value" } + require(midZoom < maxZoom) { "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value" } + } + + @JvmStatic + fun hasDrawable(imageView: ImageView): Boolean { + return imageView.drawable != null + } + + @JvmStatic + fun isSupportedScaleType(scaleType: ImageView.ScaleType?): Boolean { + if (scaleType == null) { + return false + } + when (scaleType) { + ImageView.ScaleType.MATRIX -> throw IllegalStateException("Matrix scale type is not supported") + else -> {} + } + return true + } + + fun getPointerIndex(action: Int): Int { + return action and MotionEvent.ACTION_POINTER_INDEX_MASK shr MotionEvent.ACTION_POINTER_INDEX_SHIFT + } +} \ No newline at end of file diff --git a/PhotoView/src/test/java/bou/amine/apps/photoview/ExampleUnitTest.kt b/PhotoView/src/test/java/bou/amine/apps/photoview/ExampleUnitTest.kt new file mode 100644 index 0000000..281639a --- /dev/null +++ b/PhotoView/src/test/java/bou/amine/apps/photoview/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package bou.amine.apps.photoview + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..902a37d --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# PhotoView (Fork from https://github.com/fanhonest/chrisbanes_PhotoView) +PhotoView aims to help produce an easily usable implementation of a zooming Android ImageView. + +## Dependency + + +Add the library to your module `build.gradle` +```gradle +dependencies { + implementation 'bou.amine.apps:PhotoView:latest.release.here' +} +``` + +## Features +- Out of the box zooming, using multi-touch and double-tap. +- Scrolling, with smooth scrolling fling. +- Works perfectly when used in a scrolling parent (such as ViewPager). +- Allows the application to be notified when the displayed Matrix has changed. Useful for when you need to update your UI based on the current zoom/scroll position. +- Allows the application to be notified when the user taps on the Photo. + +## Usage +There is a [sample](https://github.com/chrisbanes/PhotoView/tree/master/sample) provided which shows how to use the library in a more advanced way, but for completeness, here is all that is required to get PhotoView working: +```xml + +``` +```java +PhotoView photoView = (PhotoView) findViewById(R.id.photo_view); +photoView.setImageResource(R.drawable.image); +``` +That's it! + +## Issues With ViewGroups +There are some ViewGroups (ones that utilize onInterceptTouchEvent) that throw exceptions when a PhotoView is placed within them, most notably [ViewPager](http://developer.android.com/reference/android/support/v4/view/ViewPager.html) and [DrawerLayout](https://developer.android.com/reference/android/support/v4/widget/DrawerLayout.html). This is a framework issue that has not been resolved. In order to prevent this exception (which typically occurs when you zoom out), take a look at [HackyDrawerLayout](https://github.com/chrisbanes/PhotoView/blob/master/sample/src/main/java/com/github/chrisbanes/photoview/sample/HackyDrawerLayout.java) and you can see the solution is to simply catch the exception. Any ViewGroup which uses onInterceptTouchEvent will also need to be extended and exceptions caught. Use the [HackyDrawerLayout](https://github.com/chrisbanes/PhotoView/blob/master/sample/src/main/java/com/github/chrisbanes/photoview/sample/HackyDrawerLayout.java) as a template of how to do so. The basic implementation is: +```java +public class HackyProblematicViewGroup extends ProblematicViewGroup { + + public HackyProblematicViewGroup(Context context) { + super(context); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException e) { + //uncomment if you really want to see these errors + //e.printStackTrace(); + return false; + } + } +} +``` + +## Usage with Fresco +Due to the complex nature of Fresco, this library does not currently support Fresco. See [this project](https://github.com/ongakuer/PhotoDraweeView) as an alternative solution. + +## Subsampling Support +This library aims to keep the zooming implementation simple. If you are looking for an implementation that supports subsampling, check out [this project](https://github.com/davemorrissey/subsampling-scale-image-view) + +License +-------- + + Copyright 2017 Chris Banes + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..31f7ea7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,11 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.3.1' apply false + id 'com.android.library' version '7.3.1' apply false + id 'org.jetbrains.kotlin.android' version '1.7.20' apply false +} + +ext { + sdkVersion = 33 + supportLibVersion = '33.0.0' +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8de9f00 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,41 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true + + +VERSION_NAME=1.0.0 +VERSION_CODE=100 +GROUP=bou.amine.apps.photoview + +POM_DESCRIPTION=A simple ImageView that support zooming, both by Multi-touch gestures and double-tap (Fork from https://github.com/fanhonest/chrisbanes_PhotoView). + +POM_URL=https://github.com/chrisbanes/PhotoView +POM_SCM_URL=https://github.com/chrisbanes/PhotoView +POM_SCM_CONNECTION=scm:git@github.com/chrisbanes/PhotoView.git +POM_SCM_DEV_CONNECTION=scm:git@github.com/chrisbanes/PhotoView.git + +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=AmineLou +POM_DEVELOPER_NAME=Amine Louveau diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..216b489 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jan 14 21:46:08 CET 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..35d8fb0 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'bou.amine.apps.photoviewproject' + compileSdk 33 + + defaultConfig { + applicationId "bou.amine.apps.photoviewproject" + minSdk 18 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + // Flag to enable support for the new language APIs + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + // For Kotlin projects + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'com.google.android.material:material:1.7.0' + implementation project(path: ':PhotoView') + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'com.squareup.picasso:picasso:2.5.2' + implementation project(':PhotoView') +} \ No newline at end of file diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sample/src/androidTest/java/bou/amine/apps/photoviewproject/ExampleInstrumentedTest.kt b/sample/src/androidTest/java/bou/amine/apps/photoviewproject/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..df8d548 --- /dev/null +++ b/sample/src/androidTest/java/bou/amine/apps/photoviewproject/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package bou.amine.apps.photoviewproject + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("bou.amine.apps.photoviewproject", appContext.packageName) + } +} \ No newline at end of file diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..589ab3f --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/ActivityTransitionActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/ActivityTransitionActivity.java new file mode 100755 index 0000000..51c0f78 --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/ActivityTransitionActivity.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package bou.amine.apps.photoviewproject; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + + + + +import android.view.View; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityOptionsCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public class ActivityTransitionActivity extends AppCompatActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_transition); + + RecyclerView list = findViewById(R.id.list); + list.setLayoutManager(new GridLayoutManager(this, 2)); + ImageAdapter imageAdapter = new ImageAdapter(new ImageAdapter.Listener() { + @Override + public void onImageClicked(View view) { + transition(view); + } + }); + list.setAdapter(imageAdapter); + } + + private void transition(View view) { + if (Build.VERSION.SDK_INT < 21) { + Toast.makeText(ActivityTransitionActivity.this, "21+ only, keep out", Toast.LENGTH_SHORT).show(); + } else { + Intent intent = new Intent(ActivityTransitionActivity.this, ActivityTransitionToActivity.class); + ActivityOptionsCompat options = ActivityOptionsCompat. + makeSceneTransitionAnimation(ActivityTransitionActivity.this, view, getString(R.string.transition_test)); + startActivity(intent, options.toBundle()); + } + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/ActivityTransitionToActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/ActivityTransitionToActivity.java new file mode 100644 index 0000000..92c1eeb --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/ActivityTransitionToActivity.java @@ -0,0 +1,18 @@ +package bou.amine.apps.photoviewproject; + +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + + +/** + * Activity that gets transitioned to + */ +public class ActivityTransitionToActivity extends AppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_transition_to); + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/HackyDrawerLayout.java b/sample/src/main/java/bou/amine/apps/photoviewproject/HackyDrawerLayout.java new file mode 100644 index 0000000..d64188e --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/HackyDrawerLayout.java @@ -0,0 +1,34 @@ +package bou.amine.apps.photoviewproject; + +import android.content.Context; + +import android.view.MotionEvent; +import androidx.drawerlayout.widget.DrawerLayout; + +/** + * Hacky fix for Issue #4 and + * http://code.google.com/p/android/issues/detail?id=18990 + *

+ * ScaleGestureDetector seems to mess up the touch events, which means that + * ViewGroups which make use of onInterceptTouchEvent throw a lot of + * IllegalArgumentException: pointerIndex out of range. + *

+ * There's not much I can do in my code for now, but we can mask the result by + * just catching the problem and ignoring it. + */ +public class HackyDrawerLayout extends DrawerLayout { + + public HackyDrawerLayout(Context context) { + super(context); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/HackyViewPager.java b/sample/src/main/java/bou/amine/apps/photoviewproject/HackyViewPager.java new file mode 100755 index 0000000..bedd905 --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/HackyViewPager.java @@ -0,0 +1,40 @@ +package bou.amine.apps.photoviewproject; + +import android.content.Context; + +import android.util.AttributeSet; +import android.view.MotionEvent; +import androidx.viewpager.widget.ViewPager; + +/** + * Hacky fix for Issue #4 and + * http://code.google.com/p/android/issues/detail?id=18990 + *

+ * ScaleGestureDetector seems to mess up the touch events, which means that + * ViewGroups which make use of onInterceptTouchEvent throw a lot of + * IllegalArgumentException: pointerIndex out of range. + *

+ * There's not much I can do in my code for now, but we can mask the result by + * just catching the problem and ignoring it. + * + * @author Chris Banes + */ +public class HackyViewPager extends ViewPager { + + public HackyViewPager(Context context) { + super(context); + } + + public HackyViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/ImageAdapter.java b/sample/src/main/java/bou/amine/apps/photoviewproject/ImageAdapter.java new file mode 100644 index 0000000..90d257a --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/ImageAdapter.java @@ -0,0 +1,44 @@ +package bou.amine.apps.photoviewproject; + + +import android.view.View; +import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Image adapter + */ +public class ImageAdapter extends RecyclerView.Adapter { + + Listener mListener; + + public ImageAdapter(Listener listener) { + mListener = listener; + } + + @Override + public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + ImageViewHolder holder = ImageViewHolder.inflate(parent); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mListener.onImageClicked(view); + } + }); + return holder; + } + + @Override + public void onBindViewHolder(ImageViewHolder holder, int position) { + + } + + @Override + public int getItemCount() { + return 20; + } + + public interface Listener { + void onImageClicked(View view); + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/ImageViewHolder.java b/sample/src/main/java/bou/amine/apps/photoviewproject/ImageViewHolder.java new file mode 100644 index 0000000..8843dfa --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/ImageViewHolder.java @@ -0,0 +1,30 @@ +package bou.amine.apps.photoviewproject; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Image in recyclerview + */ +public class ImageViewHolder extends RecyclerView.ViewHolder { + + public static ImageViewHolder inflate(ViewGroup parent) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_image, parent, false); + return new ImageViewHolder(view); + } + + public TextView mTextTitle; + + public ImageViewHolder(View view) { + super(view); + mTextTitle = view.findViewById(R.id.title); + } + + private void bind(String title) { + mTextTitle.setText(title); + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/ImmersiveActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/ImmersiveActivity.java new file mode 100644 index 0000000..074e9a6 --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/ImmersiveActivity.java @@ -0,0 +1,88 @@ +package bou.amine.apps.photoviewproject; + +import android.os.Build; +import android.os.Bundle; + + +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import bou.amine.apps.photoview.OnPhotoTapListener; +import bou.amine.apps.photoview.PhotoView; +import com.squareup.picasso.Picasso; + +import static android.R.attr.uiOptions; + +/** + * Shows immersive image viewer + */ + +public class ImmersiveActivity extends AppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_immersive); + + PhotoView photoView = findViewById(R.id.photo_view); + Picasso.with(this) + .load("http://pbs.twimg.com/media/Bist9mvIYAAeAyQ.jpg") + .into(photoView); + photoView.setOnPhotoTapListener(new OnPhotoTapListener() { + @Override + public void onPhotoTap(ImageView view, float x, float y) { + //fullScreen(); + } + }); + fullScreen(); + } + + public void fullScreen() { + + // BEGIN_INCLUDE (get_current_ui_flags) + // The UI options currently enabled are represented by a bitfield. + // getSystemUiVisibility() gives us that bitfield. + int uiOptions = getWindow().getDecorView().getSystemUiVisibility(); + int newUiOptions = uiOptions; + // END_INCLUDE (get_current_ui_flags) + // BEGIN_INCLUDE (toggle_ui_flags) + boolean isImmersiveModeEnabled = isImmersiveModeEnabled(); + if (isImmersiveModeEnabled) { + Log.i("TEST", "Turning immersive mode mode off. "); + } else { + Log.i("TEST", "Turning immersive mode mode on."); + } + + // Navigation bar hiding: Backwards compatible to ICS. + if (Build.VERSION.SDK_INT >= 14) { + newUiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + } + + // Status bar hiding: Backwards compatible to Jellybean + if (Build.VERSION.SDK_INT >= 16) { + newUiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN; + } + + // Immersive mode: Backward compatible to KitKat. + // Note that this flag doesn't do anything by itself, it only augments the behavior + // of HIDE_NAVIGATION and FLAG_FULLSCREEN. For the purposes of this sample + // all three flags are being toggled together. + // Note that there are two immersive mode UI flags, one of which is referred to as "sticky". + // Sticky immersive mode differs in that it makes the navigation and status bars + // semi-transparent, and the UI flag does not get cleared when the user interacts with + // the screen. + if (Build.VERSION.SDK_INT >= 18) { + newUiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + } + + getWindow().getDecorView().setSystemUiVisibility(newUiOptions); + //END_INCLUDE (set_ui_flags) + } + + private boolean isImmersiveModeEnabled() { + return ((uiOptions | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) == uiOptions); + } + +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/LauncherActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/LauncherActivity.java new file mode 100755 index 0000000..bb981b6 --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/LauncherActivity.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package bou.amine.apps.photoviewproject; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + + + + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public class LauncherActivity extends AppCompatActivity { + + public static final String[] options = { + "Simple Sample", + "ViewPager Sample", + "Rotation Sample", + "Picasso Sample", + "Activity Transition Sample", + "Immersive Sample" + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_launcher); + Toolbar toolbar = findViewById(R.id.toolbar); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setTitle(R.string.app_name); + } + RecyclerView recyclerView = findViewById(R.id.list); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(new ItemAdapter()); + } + + + private static class ItemAdapter extends RecyclerView.Adapter { + @Override + public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final ItemViewHolder holder = ItemViewHolder.newInstance(parent); + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Class clazz; + + switch (holder.getAdapterPosition()) { + default: + case 0: + clazz = SimpleSampleActivity.class; + break; + case 1: + clazz = ViewPagerActivity.class; + break; + case 2: + clazz = RotationSampleActivity.class; + break; + case 3: + clazz = PicassoSampleActivity.class; + break; + case 4: + clazz = ActivityTransitionActivity.class; + break; + case 5: + clazz = ImmersiveActivity.class; + } + + Context context = holder.itemView.getContext(); + context.startActivity(new Intent(context, clazz)); + } + }); + return holder; + } + + @Override + public void onBindViewHolder(final ItemViewHolder holder, int position) { + holder.bind(options[position]); + } + + @Override + public int getItemCount() { + return options.length; + } + } + + private static class ItemViewHolder extends RecyclerView.ViewHolder { + + public static ItemViewHolder newInstance(ViewGroup parent) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_sample, parent, false); + return new ItemViewHolder(view); + } + + public TextView mTextTitle; + + public ItemViewHolder(View view) { + super(view); + mTextTitle = view.findViewById(R.id.title); + } + + private void bind(String title) { + mTextTitle.setText(title); + } + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/PicassoSampleActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/PicassoSampleActivity.java new file mode 100644 index 0000000..2181520 --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/PicassoSampleActivity.java @@ -0,0 +1,23 @@ +package bou.amine.apps.photoviewproject; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import bou.amine.apps.photoview.PhotoView; +import com.squareup.picasso.Picasso; + + +public class PicassoSampleActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_simple); + + final PhotoView photoView = findViewById(R.id.iv_photo); + + Picasso.with(this) + .load("http://pbs.twimg.com/media/Bist9mvIYAAeAyQ.jpg") + .into(photoView); + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/RotationSampleActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/RotationSampleActivity.java new file mode 100755 index 0000000..8839eed --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/RotationSampleActivity.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package bou.amine.apps.photoviewproject; + +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; + + +import android.view.MenuItem; +import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.app.AppCompatActivity; +import bou.amine.apps.photoview.PhotoView; + + +public class RotationSampleActivity extends AppCompatActivity { + + private PhotoView photo; + private final Handler handler = new Handler(); + private boolean rotating = false; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_rotation_sample); + Toolbar toolbar = findViewById(R.id.toolbar); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.inflateMenu(R.menu.rotation); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_rotate_10_right: + photo.setRotationBy(10); + return true; + case R.id.action_rotate_10_left: + photo.setRotationBy(-10); + return true; + case R.id.action_toggle_automatic_rotation: + toggleRotation(); + return true; + case R.id.action_reset_to_0: + photo.setRotationTo(0); + return true; + case R.id.action_reset_to_90: + photo.setRotationTo(90); + return true; + case R.id.action_reset_to_180: + photo.setRotationTo(180); + return true; + case R.id.action_reset_to_270: + photo.setRotationTo(270); + return true; + } + return false; + } + }); + } + photo = findViewById(R.id.iv_photo); + photo.setImageResource(R.drawable.wallpaper); + } + + @Override + protected void onPause() { + super.onPause(); + handler.removeCallbacksAndMessages(null); + } + + private void toggleRotation() { + if (rotating) { + handler.removeCallbacksAndMessages(null); + } else { + rotateLoop(); + } + rotating = !rotating; + } + + private void rotateLoop() { + handler.postDelayed(new Runnable() { + @Override + public void run() { + photo.setRotationBy(1); + rotateLoop(); + } + }, 15); + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/SimpleSampleActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/SimpleSampleActivity.java new file mode 100755 index 0000000..ffe58d0 --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/SimpleSampleActivity.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package bou.amine.apps.photoviewproject; + +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; + + +import android.util.Log; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import bou.amine.apps.photoview.OnMatrixChangedListener; +import bou.amine.apps.photoview.OnPhotoTapListener; +import bou.amine.apps.photoview.OnSingleFlingListener; +import bou.amine.apps.photoview.PhotoView; + + +import java.util.Random; + + +public class SimpleSampleActivity extends AppCompatActivity { + + static final String PHOTO_TAP_TOAST_STRING = "Photo Tap! X: %.2f %% Y:%.2f %% ID: %d"; + static final String SCALE_TOAST_STRING = "Scaled to: %.2ff"; + static final String FLING_LOG_STRING = "Fling velocityX: %.2f, velocityY: %.2f"; + + private PhotoView mPhotoView; + private TextView mCurrMatrixTv; + + private Toast mCurrentToast; + + private Matrix mCurrentDisplayMatrix = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_simple_sample); + + Toolbar toolbar = findViewById(R.id.toolbar); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setTitle("Simple Sample"); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onBackPressed(); + } + }); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.inflateMenu(R.menu.main_menu); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_zoom_toggle: + mPhotoView.setZoomable(!mPhotoView.isZoomEnabled()); + item.setTitle(mPhotoView.isZoomEnabled() ? R.string.menu_zoom_disable : R.string.menu_zoom_enable); + return true; + + case R.id.menu_scale_fit_center: + mPhotoView.setScaleType(ImageView.ScaleType.CENTER); + return true; + + case R.id.menu_scale_fit_start: + mPhotoView.setScaleType(ImageView.ScaleType.FIT_START); + return true; + + case R.id.menu_scale_fit_end: + mPhotoView.setScaleType(ImageView.ScaleType.FIT_END); + return true; + + case R.id.menu_scale_fit_xy: + mPhotoView.setScaleType(ImageView.ScaleType.FIT_XY); + return true; + + case R.id.menu_scale_scale_center: + mPhotoView.setScaleType(ImageView.ScaleType.CENTER); + return true; + + case R.id.menu_scale_scale_center_crop: + mPhotoView.setScaleType(ImageView.ScaleType.CENTER_CROP); + return true; + + case R.id.menu_scale_scale_center_inside: + mPhotoView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + return true; + + case R.id.menu_scale_random_animate: + case R.id.menu_scale_random: + Random r = new Random(); + + float minScale = mPhotoView.getMinimumScale(); + float maxScale = mPhotoView.getMaximumScale(); + float randomScale = minScale + (r.nextFloat() * (maxScale - minScale)); + mPhotoView.setScale(randomScale, item.getItemId() == R.id.menu_scale_random_animate); + + showToast(String.format(SCALE_TOAST_STRING, randomScale)); + + return true; + case R.id.menu_matrix_restore: + if (mCurrentDisplayMatrix == null) + showToast("You need to capture display matrix first"); + else + mPhotoView.setDisplayMatrix(mCurrentDisplayMatrix); + return true; + case R.id.menu_matrix_capture: + mCurrentDisplayMatrix = new Matrix(); + mPhotoView.getDisplayMatrix(mCurrentDisplayMatrix); + return true; + } + return false; + } + }); + } + mPhotoView = findViewById(R.id.iv_photo); + mCurrMatrixTv = findViewById(R.id.tv_current_matrix); + + Drawable bitmap = ContextCompat.getDrawable(this, R.drawable.wallpaper); + mPhotoView.setImageDrawable(bitmap); + + // Lets attach some listeners, not required though! + mPhotoView.setOnMatrixChangeListener(new MatrixChangeListener()); + mPhotoView.setOnPhotoTapListener(new PhotoTapListener()); + mPhotoView.setOnSingleFlingListener(new SingleFlingListener()); + } + + private class PhotoTapListener implements OnPhotoTapListener { + + @Override + public void onPhotoTap(ImageView view, float x, float y) { + float xPercentage = x * 100f; + float yPercentage = y * 100f; + + showToast(String.format(PHOTO_TAP_TOAST_STRING, xPercentage, yPercentage, view == null ? 0 : view.getId())); + } + } + + private void showToast(CharSequence text) { + if (mCurrentToast != null) { + mCurrentToast.cancel(); + } + + mCurrentToast = Toast.makeText(SimpleSampleActivity.this, text, Toast.LENGTH_SHORT); + mCurrentToast.show(); + } + + private class MatrixChangeListener implements OnMatrixChangedListener { + + @Override + public void onMatrixChanged(RectF rect) { + mCurrMatrixTv.setText(rect.toString()); + } + } + + private class SingleFlingListener implements OnSingleFlingListener { + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + Log.d("PhotoView", String.format(FLING_LOG_STRING, velocityX, velocityY)); + return true; + } + } +} diff --git a/sample/src/main/java/bou/amine/apps/photoviewproject/ViewPagerActivity.java b/sample/src/main/java/bou/amine/apps/photoviewproject/ViewPagerActivity.java new file mode 100755 index 0000000..f48c40a --- /dev/null +++ b/sample/src/main/java/bou/amine/apps/photoviewproject/ViewPagerActivity.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package bou.amine.apps.photoviewproject; + +import android.os.Bundle; + + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import androidx.appcompat.app.AppCompatActivity; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; +import bou.amine.apps.photoview.PhotoView; + + +public class ViewPagerActivity extends AppCompatActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_pager); + ViewPager viewPager = findViewById(R.id.view_pager); + + viewPager.setAdapter(new SamplePagerAdapter()); + } + + static class SamplePagerAdapter extends PagerAdapter { + + private static final int[] sDrawables = { R.drawable.wallpaper, R.drawable.wallpaper, R.drawable.wallpaper, + R.drawable.wallpaper, R.drawable.wallpaper, R.drawable.wallpaper }; + + @Override + public int getCount() { + return sDrawables.length; + } + + @Override + public View instantiateItem(ViewGroup container, int position) { + PhotoView photoView = new PhotoView(container.getContext()); + photoView.setImageResource(sDrawables[position]); + + // Now just add PhotoView to ViewPager and return it + container.addView(photoView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + + return photoView; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + } +} diff --git a/sample/src/main/res/drawable-nodpi/wallpaper.jpg b/sample/src/main/res/drawable-nodpi/wallpaper.jpg new file mode 100755 index 0000000..6bc5292 Binary files /dev/null and b/sample/src/main/res/drawable-nodpi/wallpaper.jpg differ diff --git a/sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml b/sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml new file mode 100644 index 0000000..38fbc26 --- /dev/null +++ b/sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/sample/src/main/res/layout/activity_immersive.xml b/sample/src/main/res/layout/activity_immersive.xml new file mode 100644 index 0000000..2cbd313 --- /dev/null +++ b/sample/src/main/res/layout/activity_immersive.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_launcher.xml b/sample/src/main/res/layout/activity_launcher.xml new file mode 100644 index 0000000..6677784 --- /dev/null +++ b/sample/src/main/res/layout/activity_launcher.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_rotation_sample.xml b/sample/src/main/res/layout/activity_rotation_sample.xml new file mode 100755 index 0000000..14c2e0f --- /dev/null +++ b/sample/src/main/res/layout/activity_rotation_sample.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_simple.xml b/sample/src/main/res/layout/activity_simple.xml new file mode 100755 index 0000000..ae26080 --- /dev/null +++ b/sample/src/main/res/layout/activity_simple.xml @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_simple_sample.xml b/sample/src/main/res/layout/activity_simple_sample.xml new file mode 100755 index 0000000..0d088bf --- /dev/null +++ b/sample/src/main/res/layout/activity_simple_sample.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_transition.xml b/sample/src/main/res/layout/activity_transition.xml new file mode 100755 index 0000000..fc0fd0c --- /dev/null +++ b/sample/src/main/res/layout/activity_transition.xml @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_transition_to.xml b/sample/src/main/res/layout/activity_transition_to.xml new file mode 100755 index 0000000..855c540 --- /dev/null +++ b/sample/src/main/res/layout/activity_transition_to.xml @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_view_pager.xml b/sample/src/main/res/layout/activity_view_pager.xml new file mode 100755 index 0000000..09ea3aa --- /dev/null +++ b/sample/src/main/res/layout/activity_view_pager.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/layout/item_image.xml b/sample/src/main/res/layout/item_image.xml new file mode 100644 index 0000000..c6346a5 --- /dev/null +++ b/sample/src/main/res/layout/item_image.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/layout/item_sample.xml b/sample/src/main/res/layout/item_sample.xml new file mode 100644 index 0000000..c16b420 --- /dev/null +++ b/sample/src/main/res/layout/item_sample.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/menu/main_menu.xml b/sample/src/main/res/menu/main_menu.xml new file mode 100755 index 0000000..688f84c --- /dev/null +++ b/sample/src/main/res/menu/main_menu.xml @@ -0,0 +1,43 @@ + +

+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/menu/rotation.xml b/sample/src/main/res/menu/rotation.xml new file mode 100644 index 0000000..da8e846 --- /dev/null +++ b/sample/src/main/res/menu/rotation.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000..ecee8d5 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000..fa0049f Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000..c211c86 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..d7c57be Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..5730727 Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 0000000..3feb43b --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #AACE30 + #142D3E + #001425 + \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100755 index 0000000..25f5e52 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + PhotoView Sample + Enable Zoom + Disable Zoom + Change to FIT_CENTER + Change to FIT_START + Change to FIT_END + Change to FIT_XY + Change to CENTER + Change to CENTER_INSIDE + Change to CENTER_CROP + Animate scale to random value + Set scale to random value + Restore Display Matrix + Capture Display Matrix + Extract visible bitmap + + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 0000000..449b266 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/transitions.xml b/sample/src/main/res/values/transitions.xml new file mode 100644 index 0000000..e0e3c44 --- /dev/null +++ b/sample/src/main/res/values/transitions.xml @@ -0,0 +1,4 @@ + + + test + \ No newline at end of file diff --git a/sample/src/test/java/bou/amine/apps/photoviewproject/ExampleUnitTest.kt b/sample/src/test/java/bou/amine/apps/photoviewproject/ExampleUnitTest.kt new file mode 100644 index 0000000..1820bad --- /dev/null +++ b/sample/src/test/java/bou/amine/apps/photoviewproject/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package bou.amine.apps.photoviewproject + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9b01134 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "PhotoViewProject" +include ':sample' +include ':PhotoView'