diff --git a/photoview/CircleImageView.kt b/photoview/CircleImageView.kt new file mode 100644 index 00000000..6f43c8e2 --- /dev/null +++ b/photoview/CircleImageView.kt @@ -0,0 +1,427 @@ +package com.github.chrisbanes.photoview + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatImageView +import com.armando.raonimotores.testezoom.R + +class CircleImageView : AppCompatImageView { + + private val mDrawableRect = RectF() + private val mBorderRect = RectF() + + private val mShaderMatrix = Matrix() + private val mBitmapPaint = Paint() + private val mBorderPaint = Paint() + private val mCircleBackgroundPaint = Paint() + + private var mBorderColor = DEFAULT_BORDER_COLOR + private var mBorderWidth = DEFAULT_BORDER_WIDTH + private var mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR + + private var mBitmap: Bitmap? = null + private var mBitmapShader: BitmapShader? = null + private var mBitmapWidth: Int = 0 + private var mBitmapHeight: Int = 0 + + private var mDrawableRadius: Float = 0.toFloat() + private var mBorderRadius: Float = 0.toFloat() + + private var mColorFilter: ColorFilter? = null + + private var mReady: Boolean = false + private var mSetupPending: Boolean = false + private var mBorderOverlay: Boolean = false + var isDisableCircularTransformation: Boolean = false + set(disableCircularTransformation) { + if (isDisableCircularTransformation == disableCircularTransformation) { + return + } + + field = disableCircularTransformation + initializeBitmap() + } + + var borderColor: Int + get() = mBorderColor + set(@ColorInt borderColor) { + if (borderColor == mBorderColor) { + return + } + + mBorderColor = borderColor + mBorderPaint.color = mBorderColor + invalidate() + } + + var circleBackgroundColor: Int + get() = mCircleBackgroundColor + set(@ColorInt circleBackgroundColor) { + if (circleBackgroundColor == mCircleBackgroundColor) { + return + } + + mCircleBackgroundColor = circleBackgroundColor + mCircleBackgroundPaint.color = circleBackgroundColor + invalidate() + } + + /** + * Return the color drawn behind the circle-shaped drawable. + * + * @return The color drawn behind the drawable + */ + /** + * Set a color to be drawn behind the circle-shaped drawable. Note that + * this has no effect if the drawable is opaque or no drawable is set. + */ + var fillColor: Int + @Deprecated("Use {@link #getCircleBackgroundColor()} instead.") + get() = circleBackgroundColor + @Deprecated("Use {@link #setCircleBackgroundColor(int)} instead.") + set(@ColorInt fillColor) { + circleBackgroundColor = fillColor + } + + var borderWidth: Int + get() = mBorderWidth + set(borderWidth) { + if (borderWidth == mBorderWidth) { + return + } + + mBorderWidth = borderWidth + setup() + } + + var isBorderOverlay: Boolean + get() = mBorderOverlay + set(borderOverlay) { + if (borderOverlay == mBorderOverlay) { + return + } + + mBorderOverlay = borderOverlay + setup() + } + + constructor(context: Context) : super(context) { + + init() + } + + @JvmOverloads + constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super(context, attrs, defStyle) { + + /* + val a = context.obtainStyledAttributes(attrs, R.styleable.CirleImageView, defStyle, 0) + + mBorderWidth = a.getDimensionPixelSize(R.styleable.CirleImageView_civ_border_width, DEFAULT_BORDER_WIDTH) + mBorderColor = a.getColor(R.styleable.CirleImageView_civ_border_color, DEFAULT_BORDER_COLOR) + mBorderOverlay = a.getBoolean(R.styleable.CirleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY) + + // Look for deprecated civ_fill_color if civ_circle_background_color is not set + if (a.hasValue(R.styleable.CirleImageView_civ_circle_background_color)) { + mCircleBackgroundColor = a.getColor( + R.styleable.CirleImageView_civ_circle_background_color, + DEFAULT_CIRCLE_BACKGROUND_COLOR + ) + } else if (a.hasValue(R.styleable.CirleImageView_civ_fill_color)) { + mCircleBackgroundColor = a.getColor( + R.styleable.CirleImageView_civ_fill_color, + DEFAULT_CIRCLE_BACKGROUND_COLOR + ) + } + + a.recycle() + */ + init() + } + + private fun init() { + super.setScaleType(SCALE_TYPE) + mReady = true + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + outlineProvider = OutlineProvider() + } + + if (mSetupPending) { + setup() + mSetupPending = false + } + } + + override fun getScaleType(): ImageView.ScaleType { + return SCALE_TYPE + } + + override fun setScaleType(scaleType: ImageView.ScaleType) { + if (scaleType != SCALE_TYPE) { + throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)) + } + } + + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + if (adjustViewBounds) { + throw IllegalArgumentException("adjustViewBounds not supported.") + } + } + + override fun onDraw(canvas: Canvas) { + if (isDisableCircularTransformation) { + super.onDraw(canvas) + return + } + + if (mBitmap == null) { + return + } + + if (mCircleBackgroundColor != Color.TRANSPARENT) { + canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint) + } + canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint) + if (mBorderWidth > 0) { + canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint) + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + setup() + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(left, top, right, bottom) + setup() + } + + override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { + super.setPaddingRelative(start, top, end, bottom) + setup() + } + + @Suppress("MemberVisibilityCanBePrivate") + @Deprecated( + "Use {@link #setBorderColor(int)} instead", + ReplaceWith("borderColor = context.resources.getColor(borderColorRes)") + ) + fun setBorderColorResource(@ColorRes borderColorRes: Int) { + borderColor = context.resources.getColor(borderColorRes) + } + + @Suppress("MemberVisibilityCanBePrivate") + fun setCircleBackgroundColorResource(@ColorRes circleBackgroundRes: Int) { + circleBackgroundColor = context.resources.getColor(circleBackgroundRes) + } + + /** + * Set a color to be drawn behind the circle-shaped drawable. Note that + * this has no effect if the drawable is opaque or no drawable is set. + * + * @param fillColorRes The color resource to be resolved to a color and + * drawn behind the drawable + */ + @Deprecated( + "Use {@link #setCircleBackgroundColorResource(int)} instead.", + ReplaceWith("setCircleBackgroundColorResource(fillColorRes)") + ) + fun setFillColorResource(@ColorRes fillColorRes: Int) { + setCircleBackgroundColorResource(fillColorRes) + } + + override fun setImageBitmap(bm: Bitmap) { + super.setImageBitmap(bm) + initializeBitmap() + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + initializeBitmap() + } + + override fun setImageResource(@DrawableRes resId: Int) { + super.setImageResource(resId) + initializeBitmap() + } + + override fun setImageURI(uri: Uri?) { + super.setImageURI(uri) + initializeBitmap() + } + + override fun setColorFilter(cf: ColorFilter) { + if (cf === mColorFilter) { + return + } + + mColorFilter = cf + applyColorFilter() + invalidate() + } + + override fun getColorFilter(): ColorFilter? { + return mColorFilter + } + + private fun applyColorFilter() { + mBitmapPaint.colorFilter = mColorFilter + } + + private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? { + if (drawable == null) { + return null + } + + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + try { + val bitmap: Bitmap + + if (drawable is ColorDrawable) { + bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG) + } else { + bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, BITMAP_CONFIG) + } + + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } catch (e: Exception) { + e.printStackTrace() + return null + } + + } + + private fun initializeBitmap() { + mBitmap = if (isDisableCircularTransformation) { + null + } else { + getBitmapFromDrawable(drawable) + } + setup() + } + + private fun setup() { + if (!mReady) { + mSetupPending = true + return + } + + if (width == 0 && height == 0) { + return + } + + if (mBitmap == null) { + invalidate() + return + } + + mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + + mBitmapPaint.isAntiAlias = true + mBitmapPaint.shader = mBitmapShader + + mBorderPaint.style = Paint.Style.STROKE + mBorderPaint.isAntiAlias = true + mBorderPaint.color = mBorderColor + mBorderPaint.strokeWidth = mBorderWidth.toFloat() + + mCircleBackgroundPaint.style = Paint.Style.FILL + mCircleBackgroundPaint.isAntiAlias = true + mCircleBackgroundPaint.color = mCircleBackgroundColor + + mBitmapHeight = mBitmap!!.height + mBitmapWidth = mBitmap!!.width + + mBorderRect.set(calculateBounds()) + mBorderRadius = + Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f) + + mDrawableRect.set(mBorderRect) + if (!mBorderOverlay && mBorderWidth > 0) { + mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f) + } + mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f) + + applyColorFilter() + updateShaderMatrix() + invalidate() + } + + private fun calculateBounds(): RectF { + val availableWidth = width - paddingLeft - paddingRight + val availableHeight = height - paddingTop - paddingBottom + + val sideLength = Math.min(availableWidth, availableHeight) + + val left = paddingLeft + (availableWidth - sideLength) / 2f + val top = paddingTop + (availableHeight - sideLength) / 2f + + return RectF(left, top, left + sideLength, top + sideLength) + } + + private fun updateShaderMatrix() { + val scale: Float + var dx = 0f + var dy = 0f + + mShaderMatrix.set(null) + + if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { + scale = mDrawableRect.height() / mBitmapHeight.toFloat() + dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f + } else { + scale = mDrawableRect.width() / mBitmapWidth.toFloat() + dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f + } + + mShaderMatrix.setScale(scale, scale) + mShaderMatrix.postTranslate((dx + 0.5f).toInt() + mDrawableRect.left, (dy + 0.5f).toInt() + mDrawableRect.top) + + mBitmapShader!!.setLocalMatrix(mShaderMatrix) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private inner class OutlineProvider : ViewOutlineProvider() { + + override fun getOutline(view: View, outline: Outline) { + val bounds = Rect() + mBorderRect.roundOut(bounds) + outline.setRoundRect(bounds, bounds.width() / 2.0f) + } + + } + + companion object { + + private val SCALE_TYPE = ImageView.ScaleType.CENTER_CROP + + private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 + private const val COLORDRAWABLE_DIMENSION = 2 + + private const val DEFAULT_BORDER_WIDTH = 0 + private const val DEFAULT_BORDER_COLOR = Color.BLACK + private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT + private const val DEFAULT_BORDER_OVERLAY = false + } + +} \ No newline at end of file diff --git a/photoview/Compat.kt b/photoview/Compat.kt new file mode 100644 index 00000000..b5434c25 --- /dev/null +++ b/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 com.github.chrisbanes.photoview + +import android.annotation.TargetApi +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.view.View + +internal object Compat { + private const val SIXTY_FPS_INTERVAL = 1000 / 60 + fun postOnAnimation(view: View, runnable: Runnable) { + if (VERSION.SDK_INT >= 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) + } +} diff --git a/photoview/CustomGestureDetector.kt b/photoview/CustomGestureDetector.kt new file mode 100644 index 00000000..eaef7546 --- /dev/null +++ b/photoview/CustomGestureDetector.kt @@ -0,0 +1,186 @@ +/* + 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 com.github.chrisbanes.photoview + +import android.content.Context +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.ScaleGestureDetector.OnScaleGestureListener +import android.view.VelocityTracker +import android.view.ViewConfiguration + +/** + * Does a whole lot of gesture detecting. + */ +internal class CustomGestureDetector() { + private var mActivePointerId = INVALID_POINTER_ID + private var mActivePointerIndex = 0 + private var mDetector: ScaleGestureDetector? = null + private var mVelocityTracker: VelocityTracker? = null + var isDragging = false + private set + private var mLastTouchX = 0f + private var mLastTouchY = 0f + private var mTouchSlop: Float = 0.0f + private var mMinimumVelocity: Float = 0.0f + private lateinit var mListener: OnGestureListener + 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 + } + + constructor(context: Context?, listener: OnGestureListener) : this() { + val configuration = ViewConfiguration.get(context) + mMinimumVelocity = configuration.scaledMinimumFlingVelocity.toFloat() + mTouchSlop = configuration.scaledTouchSlop.toFloat() + mListener = listener + val mScaleListener: OnScaleGestureListener = object : OnScaleGestureListener { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scaleFactor = detector.scaleFactor + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false + if (scaleFactor >= 0) { + 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) + } +} diff --git a/photoview/OnGestureListener.kt b/photoview/OnGestureListener.kt new file mode 100644 index 00000000..33a059e4 --- /dev/null +++ b/photoview/OnGestureListener.kt @@ -0,0 +1,24 @@ +/* + 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 com.github.chrisbanes.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) +} diff --git a/photoview/OnMatrixChangedListener.kt b/photoview/OnMatrixChangedListener.kt new file mode 100644 index 00000000..f9e2f496 --- /dev/null +++ b/photoview/OnMatrixChangedListener.kt @@ -0,0 +1,17 @@ +package com.github.chrisbanes.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) +} diff --git a/photoview/OnOutsidePhotoTapListener.kt b/photoview/OnOutsidePhotoTapListener.kt new file mode 100644 index 00000000..16df0c57 --- /dev/null +++ b/photoview/OnOutsidePhotoTapListener.kt @@ -0,0 +1,13 @@ +package com.github.chrisbanes.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?) +} diff --git a/photoview/OnPhotoTapListener.kt b/photoview/OnPhotoTapListener.kt new file mode 100644 index 00000000..3321107a --- /dev/null +++ b/photoview/OnPhotoTapListener.kt @@ -0,0 +1,21 @@ +package com.github.chrisbanes.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) +} diff --git a/photoview/OnScaleChangedListener.kt b/photoview/OnScaleChangedListener.kt new file mode 100644 index 00000000..da18d775 --- /dev/null +++ b/photoview/OnScaleChangedListener.kt @@ -0,0 +1,15 @@ +package com.github.chrisbanes.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) +} diff --git a/photoview/OnSingleFlingListener.kt b/photoview/OnSingleFlingListener.kt new file mode 100644 index 00000000..eceed1aa --- /dev/null +++ b/photoview/OnSingleFlingListener.kt @@ -0,0 +1,20 @@ +package com.github.chrisbanes.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 +} diff --git a/photoview/OnViewDragListener.kt b/photoview/OnViewDragListener.kt new file mode 100644 index 00000000..1cf40834 --- /dev/null +++ b/photoview/OnViewDragListener.kt @@ -0,0 +1,15 @@ +package com.github.chrisbanes.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) +} diff --git a/photoview/OnViewTapListener.kt b/photoview/OnViewTapListener.kt new file mode 100644 index 00000000..d826cfa5 --- /dev/null +++ b/photoview/OnViewTapListener.kt @@ -0,0 +1,15 @@ +package com.github.chrisbanes.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) +} diff --git a/photoview/PhotoView.kt b/photoview/PhotoView.kt new file mode 100644 index 00000000..2824c82e --- /dev/null +++ b/photoview/PhotoView.kt @@ -0,0 +1,228 @@ +/* + 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 com.github.chrisbanes.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.OnDoubleTapListener +import androidx.appcompat.widget.AppCompatImageView + + +class PhotoView : AppCompatImageView { + /** + * 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 + + + constructor(context: Context) : super(context) { + + init() + } + + @JvmOverloads + constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super(context, attrs, defStyle) { + 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(ScaleType.MATRIX) + //apply the previously applied scale type + if (pendingScaleType != null) { + scaleType = pendingScaleType!! + pendingScaleType = null + } + } + + + override fun getScaleType(): ScaleType { + return attacher!!.scaleType + } + + override fun getImageMatrix(): Matrix { + return attacher!!.imageMatrix + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + attacher!!.setOnLongClickListener(l) + } + + override fun setOnClickListener(l: OnClickListener?) { + attacher!!.setOnClickListener(l) + } + + override fun setScaleType(scaleType: ScaleType) { + if (attacher == null) { + pendingScaleType = scaleType + } else { + attacher!!.scaleType = scaleType + } + } + + 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() + } + } + + override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean { + val changed = 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) + } + + var isZoomable: Boolean + get() = attacher!!.isZoomable + set(zoomable) { + attacher!!.isZoomable = zoomable + } + + val displayRect: RectF? + get() = attacher!!.displayRect + + fun getDisplayMatrix(matrix: Matrix?) { + attacher!!.getDisplayMatrix(matrix!!) + } + + fun setDisplayMatrix(finalRectangle: Matrix?): Boolean { + return attacher!!.setDisplayMatrix(finalRectangle) + } + + fun getSuppMatrix(matrix: Matrix?) { + 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.toFloat() + set(scale) { + attacher!!.scale = scale.toDouble() + } + + 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.toDouble(), animate) + } + + fun setScale(scale: Float, focalX: Float, focalY: Float, animate: Boolean) { + attacher!!.setScale(scale.toDouble(), focalX, focalY, animate) + } + + fun setZoomTransitionDuration(milliseconds: Int) { + attacher!!.setZoomTransitionDuration(milliseconds) + } + + fun setOnDoubleTapListener(onDoubleTapListener: OnDoubleTapListener?) { + attacher!!.setOnDoubleTapListener(onDoubleTapListener) + } + + fun setOnScaleChangeListener(onScaleChangedListener: OnScaleChangedListener?) { + attacher!!.setOnScaleChangeListener(onScaleChangedListener) + } + + fun setOnSingleFlingListener(onSingleFlingListener: OnSingleFlingListener?) { + attacher!!.setOnSingleFlingListener(onSingleFlingListener) + } + + +} diff --git a/photoview/PhotoViewAttacher.kt b/photoview/PhotoViewAttacher.kt new file mode 100644 index 00000000..02ec7d57 --- /dev/null +++ b/photoview/PhotoViewAttacher.kt @@ -0,0 +1,738 @@ +/* + 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 com.github.chrisbanes.photoview + +import android.content.Context +import android.graphics.Matrix +import android.graphics.Matrix.ScaleToFit +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.view.GestureDetector +import android.view.GestureDetector.OnDoubleTapListener +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import android.view.View +import android.view.View.OnLongClickListener +import android.view.View.OnTouchListener +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Interpolator +import android.widget.ImageView +import android.widget.ImageView.ScaleType +import android.widget.OverScroller +import kotlin.math.pow +import kotlin.math.sqrt + +/** + * 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) : 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 lateinit var mGestureDetector: GestureDetector + private lateinit var mScaleDragDetector: CustomGestureDetector + + // 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: OnLongClickListener? = null + private var mScaleChangeListener: OnScaleChangedListener? = null + private var mSingleFlingListener: OnSingleFlingListener? = null + private var mOnViewDragListener: OnViewDragListener? = null + private var mCurrentFlingRunnable: FlingRunnable? = null + private var mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH + private var mVerticalScrollEdge = VERTICAL_EDGE_BOTH + private var mBaseRotation: Float = 0f + + @get:Deprecated("") + var isZoomEnabled = true + private set + private var mScaleType = 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 (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f + || mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f + || mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f + || mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -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) { + if (mScaleChangeListener != null) { + mScaleChangeListener!!.onScaleChange(scaleFactor, focusX, focusY) + } + mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY) + checkAndDisplayMatrix() + } + } + } + + fun setOnDoubleTapListener(newOnDoubleTapListener: 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) { + Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale) + mMinScale = minimumScale + } + + var mediumScale: Float + get() = mMidScale + set(mediumScale) { + Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale) + mMidScale = mediumScale + } + + var maximumScale: Float + get() = mMaxScale + set(maximumScale) { + Util.checkZoomLevels(mMinScale, mMidScale, maximumScale) + mMaxScale = maximumScale + } + + var scale: Double + get() = sqrt(getValue(mSuppMatrix, Matrix.MSCALE_X).toDouble().pow(2.0).toFloat() + getValue(mSuppMatrix, Matrix.MSKEW_Y).toDouble().pow(2.0) as Double) + set(scale) { + setScale(scale, false) + } + + var scaleType: ScaleType + get() = mScaleType + set(scaleType) { + if (Util.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 && Util.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.toDouble(), + rect.centerX(), rect.centerY())) + handled = true + } + } else if (scale > mMaxScale) { + val rect = displayRect + if (rect != null) { + v.post(AnimatedZoomRunnable(scale, mMaxScale.toDouble(), + rect.centerX(), rect.centerY())) + handled = true + } + } + } + // Try the Scale/Drag detector + 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) { + Util.checkZoomLevels(minimumScale, mediumScale, maximumScale) + mMinScale = minimumScale + mMidScale = mediumScale + mMaxScale = maximumScale + } + + fun setOnLongClickListener(listener: 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: Double, animate: Boolean) { + setScale(scale, + mImageView.right / 2.toFloat(), + mImageView.bottom / 2.toFloat(), + animate) + } + + fun setScale( + scale: Double, 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.toFloat(), scale.toFloat(), 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 + */ + 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 == ScaleType.CENTER) { + mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2f, + (viewHeight - drawableHeight) / 2f) + } else if (mScaleType == 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 == 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) { + ScaleType.FIT_CENTER -> mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER) + ScaleType.FIT_START -> mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START) + ScaleType.FIT_END -> mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END) + ScaleType.FIT_XY -> mBaseMatrix.setRectToRect(mTempSrc, mTempDst, 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) { + ScaleType.FIT_START -> -rect.top + ScaleType.FIT_END -> viewHeight - height - rect.top + else -> (viewHeight - height) / 2 - rect.top + } + mVerticalScrollEdge = VERTICAL_EDGE_BOTH + } else if (rect.top > 0) { + mVerticalScrollEdge = VERTICAL_EDGE_TOP + deltaY = -rect.top + } else if (rect.bottom < viewHeight) { + mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM + deltaY = viewHeight - rect.bottom + } else { + mVerticalScrollEdge = VERTICAL_EDGE_NONE + } + val viewWidth = getImageViewWidth(mImageView) + if (width <= viewWidth) { + deltaX = when (mScaleType) { + ScaleType.FIT_START -> -rect.left + ScaleType.FIT_END -> viewWidth - width - rect.left + else -> (viewWidth - width) / 2 - rect.left + } + mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH + } else if (rect.left > 0) { + mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT + deltaX = -rect.left + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right + mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT + } else { + mHorizontalScrollEdge = HORIZONTAL_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 + } + } + + fun getImageView(): ImageView { + return mImageView + } + + private inner class AnimatedZoomRunnable( + currentZoom: Double, targetZoom: Double, + private val mFocalX: Float, private val mFocalY: Float) : Runnable { + private val mStartTime: Long = System.currentTimeMillis() + private val mZoomStart: Float = currentZoom.toFloat() + private val mZoomEnd: Float = targetZoom.toFloat() + 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) { + Compat.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 + 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 + Compat.postOnAnimation(mImageView, this) + } + } + + init { + mScroller = OverScroller(context) + } + } + + 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 HORIZONTAL_EDGE_NONE = -1 + private const val HORIZONTAL_EDGE_LEFT = 0 + private const val HORIZONTAL_EDGE_RIGHT = 1 + private const val HORIZONTAL_EDGE_BOTH = 2 + private const val VERTICAL_EDGE_NONE = -1 + private const val VERTICAL_EDGE_TOP = 0 + private const val VERTICAL_EDGE_BOTTOM = 1 + private const val VERTICAL_EDGE_BOTH = 2 + private const val SINGLE_TOUCH = 1 + } + + 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 : 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 + || e2.pointerCount > SINGLE_TOUCH) { + false + } else mSingleFlingListener!!.onFling(e1, e2, velocityX, velocityY) + } + return false + } + }) + mGestureDetector.setOnDoubleTapListener(object : 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.toDouble(), x, y, true) + } else if (scale >= mediumScale && scale < maximumScale) { + setScale(maximumScale.toDouble(), x, y, true) + } else { + setScale(minimumScale.toDouble(), 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 + } + // + }) + } + } +} diff --git a/photoview/Util.kt b/photoview/Util.kt new file mode 100644 index 00000000..68eec095 --- /dev/null +++ b/photoview/Util.kt @@ -0,0 +1,31 @@ +package com.github.chrisbanes.photoview + +import android.view.MotionEvent +import android.widget.ImageView +import android.widget.ImageView.ScaleType + +internal object Util { + 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" } + } + + fun hasDrawable(imageView: ImageView): Boolean { + return imageView.drawable != null + } + + fun isSupportedScaleType(scaleType: ScaleType?): Boolean { + if (scaleType == null) { + return false + } + when (scaleType) { + ScaleType.MATRIX -> throw IllegalStateException("Matrix scale type is not supported") + } + return true + } + + fun getPointerIndex(action: Int): Int { + return action and MotionEvent.ACTION_POINTER_INDEX_MASK shr MotionEvent.ACTION_POINTER_INDEX_SHIFT + } +}