From 8c92979a90812979474d6f52aba68d1d278129fa Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 13:45:56 +0100 Subject: [PATCH 01/11] Versions --- build.gradle | 4 ++-- library/build.gradle | 2 +- library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 5e7b2a4..31f74e8 100644 --- a/build.gradle +++ b/build.gradle @@ -10,9 +10,9 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:+" + classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.17" } } diff --git a/library/build.gradle b/library/build.gradle index 47f5523..430aaa3 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -32,7 +32,7 @@ android { dependencies { api "androidx.annotation:annotation:1.0.1" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt index 77f8c90..7cccae0 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt @@ -114,7 +114,7 @@ interface ZoomApi { annotation class ZoomType /** - * Defines the available transvormation types + * Defines the available transformation types */ @Retention(AnnotationRetention.SOURCE) @IntDef(TRANSFORMATION_CENTER_INSIDE, TRANSFORMATION_CENTER_CROP, TRANSFORMATION_NONE) From 6b10f4ba10a2dd04bdfa5fc975d407b0b37448d6 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 14:04:46 +0100 Subject: [PATCH 02/11] Add TRANSFORMATION_GRAVITY_AUTO --- README.md | 8 +++- .../java/com/otaliastudios/zoom/ZoomApi.kt | 37 +++++++++++++++++++ .../java/com/otaliastudios/zoom/ZoomEngine.kt | 24 ++++++++---- .../com/otaliastudios/zoom/ZoomImageView.kt | 14 +++---- .../java/com/otaliastudios/zoom/ZoomLayout.kt | 14 +++---- library/src/main/res/values/attrs.xml | 1 + 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 3305619..c73af1e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,9 @@ A container for view hierarchies that can be panned or zoomed. 0) { // Got to change sign to have a negative result. - result[0] = -applyGravity(mTransformationGravity, extraWidth, true) + result[0] = -applyGravity(gravity, extraWidth, true) } if (extraHeight > 0) { - result[1] = -applyGravity(mTransformationGravity, extraHeight, false) + result[1] = -applyGravity(gravity, extraHeight, false) } return result } + private fun computeTransformationGravity(input: Int): Int { + return if (input != ZoomApi.TRANSFORMATION_GRAVITY_AUTO) { + input + } else { + // TODO get from alignment if possible, then fallback to center. + input + } + } + /** * Returns 0 for 'start' gravities, [extraSpace] for 'end' gravities, and half of it * for 'center' gravities. diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt index 89ac09d..7973b89 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt @@ -47,12 +47,12 @@ private constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAtt val zoomEnabled = a.getBoolean(R.styleable.ZoomEngine_zoomEnabled, true) val flingEnabled = a.getBoolean(R.styleable.ZoomEngine_flingEnabled, true) val allowFlingInOverscroll = a.getBoolean(R.styleable.ZoomEngine_allowFlingInOverscroll, true) - val minZoom = a.getFloat(R.styleable.ZoomEngine_minZoom, -1f) - val maxZoom = a.getFloat(R.styleable.ZoomEngine_maxZoom, -1f) - @ZoomType val minZoomMode = a.getInteger(R.styleable.ZoomEngine_minZoomType, ZoomApi.TYPE_ZOOM) - @ZoomType val maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, ZoomApi.TYPE_ZOOM) + val minZoom = a.getFloat(R.styleable.ZoomEngine_minZoom, ZoomApi.MIN_ZOOM_DEFAULT) + val maxZoom = a.getFloat(R.styleable.ZoomEngine_maxZoom, ZoomApi.MAX_ZOOM_DEFAULT) + @ZoomType val minZoomMode = a.getInteger(R.styleable.ZoomEngine_minZoomType, ZoomApi.MIN_ZOOM_DEFAULT_TYPE) + @ZoomType val maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, ZoomApi.MAX_ZOOM_DEFAULT_TYPE) val transformation = a.getInteger(R.styleable.ZoomEngine_transformation, ZoomApi.TRANSFORMATION_CENTER_INSIDE) - val transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, Gravity.CENTER) + val transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, ZoomApi.TRANSFORMATION_GRAVITY_AUTO) val smallerPolicy = a.getInt(R.styleable.ZoomEngine_smallerPolicy, ZoomApi.SMALLER_POLICY_CENTER) val animationDuration = a.getInt(R.styleable.ZoomEngine_animationDuration, ZoomEngine.DEFAULT_ANIMATION_DURATION.toInt()).toLong() a.recycle() @@ -70,8 +70,8 @@ private constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAtt setFlingEnabled(flingEnabled) setAllowFlingInOverscroll(allowFlingInOverscroll) setAnimationDuration(animationDuration) - if (minZoom > -1) setMinZoom(minZoom, minZoomMode) - if (maxZoom > -1) setMaxZoom(maxZoom, maxZoomMode) + setMinZoom(minZoom, minZoomMode) + setMaxZoom(maxZoom, maxZoomMode) imageMatrix = mMatrix scaleType = ImageView.ScaleType.MATRIX diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt index fb1f7ed..0a0c4c0 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt @@ -53,12 +53,12 @@ private constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAtt val flingEnabled = a.getBoolean(R.styleable.ZoomEngine_flingEnabled, true) val allowFlingInOverscroll = a.getBoolean(R.styleable.ZoomEngine_allowFlingInOverscroll, true) val hasChildren = a.getBoolean(R.styleable.ZoomEngine_hasClickableChildren, false) - val minZoom = a.getFloat(R.styleable.ZoomEngine_minZoom, -1f) - val maxZoom = a.getFloat(R.styleable.ZoomEngine_maxZoom, -1f) - @ZoomType val minZoomMode = a.getInteger(R.styleable.ZoomEngine_minZoomType, ZoomApi.TYPE_ZOOM) - @ZoomType val maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, ZoomApi.TYPE_ZOOM) + val minZoom = a.getFloat(R.styleable.ZoomEngine_minZoom, ZoomApi.MIN_ZOOM_DEFAULT) + val maxZoom = a.getFloat(R.styleable.ZoomEngine_maxZoom, ZoomApi.MAX_ZOOM_DEFAULT) + @ZoomType val minZoomMode = a.getInteger(R.styleable.ZoomEngine_minZoomType, ZoomApi.MIN_ZOOM_DEFAULT_TYPE) + @ZoomType val maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, ZoomApi.MAX_ZOOM_DEFAULT_TYPE) val transformation = a.getInteger(R.styleable.ZoomEngine_transformation, ZoomApi.TRANSFORMATION_CENTER_INSIDE) - val transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, Gravity.CENTER) + val transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, ZoomApi.TRANSFORMATION_GRAVITY_AUTO) val smallerPolicy = a.getInt(R.styleable.ZoomEngine_smallerPolicy, ZoomApi.SMALLER_POLICY_CENTER) val animationDuration = a.getInt(R.styleable.ZoomEngine_animationDuration, ZoomEngine.DEFAULT_ANIMATION_DURATION.toInt()).toLong() a.recycle() @@ -76,8 +76,8 @@ private constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAtt setFlingEnabled(flingEnabled) setAllowFlingInOverscroll(allowFlingInOverscroll) setAnimationDuration(animationDuration) - if (minZoom > -1) setMinZoom(minZoom, minZoomMode) - if (maxZoom > -1) setMaxZoom(maxZoom, maxZoomMode) + setMinZoom(minZoom, minZoomMode) + setMaxZoom(maxZoom, maxZoomMode) setHasClickableChildren(hasChildren) setWillNotDraw(false) diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 0d8038d..6379450 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -26,6 +26,7 @@ + From 0be73741f5e1f103a9e0d722ff799f2e0062002a Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 14:49:18 +0100 Subject: [PATCH 03/11] Create Alignment class --- .../java/com/otaliastudios/zoom/Alignment.kt | 51 +++++++++++++++++++ .../java/com/otaliastudios/zoom/ZoomEngine.kt | 5 ++ 2 files changed, 56 insertions(+) create mode 100644 library/src/main/java/com/otaliastudios/zoom/Alignment.kt diff --git a/library/src/main/java/com/otaliastudios/zoom/Alignment.kt b/library/src/main/java/com/otaliastudios/zoom/Alignment.kt new file mode 100644 index 0000000..35c2cde --- /dev/null +++ b/library/src/main/java/com/otaliastudios/zoom/Alignment.kt @@ -0,0 +1,51 @@ +package com.otaliastudios.zoom + +object Alignment { + + // A shift of 4 means each axis can have 16 values. + internal const val SHIFT = 4 + internal const val MASK = 0xF0 // 11110000 + + // A special value meaning that the flag for some axis was not set. + internal const val NO_VALUE = 0x0 + + // Vertical + const val TOP = 0x1 // 0000 0001 + const val BOTTOM = 0x2 // 0000 0010 + const val CENTER_VERTICAL = 0x3 // 0000 0011 + const val NONE_VERTICAL = 0x4 // 0000 0100 + + // Horizontal + const val LEFT = 0x1 shl SHIFT // 0001 0000 + const val RIGHT = 0x2 shl SHIFT // 0010 0000 + const val CENTER_HORIZONTAL = 0x3 shl SHIFT // 0011 0000 + const val NONE_HORIZONTAL = 0x4 shl SHIFT // 0100 0000 + + // TODO support START and END + + /** + * Shorthand for [CENTER_HORIZONTAL] and [CENTER_VERTICAL] together. + */ + const val CENTER = CENTER_VERTICAL or CENTER_HORIZONTAL + + /** + * Shorthand for [NONE_HORIZONTAL] and [NONE_VERTICAL] together. + */ + const val NONE = NONE_VERTICAL or NONE_HORIZONTAL + + /** + * Returns the horizontal alignment for this alignment, + * or [NO_VALUE] if no value was set. + */ + internal fun getHorizontal(alignment: Int): Int { + return alignment and MASK + } + + /** + * Returns the vertical alignment for this alignment, + * or [NO_VALUE] if no value was set. + */ + internal fun getVertical(alignment: Int): Int { + return alignment and MASK.inv() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index 20b931d..86273c3 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -634,6 +634,11 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener return result } + /** + * Computes an actual [Gravity] value from the input gravity, + * which might also be [ZoomApi.TRANSFORMATION_GRAVITY_AUTO]. In this case we should + * try to infer a [Gravity] from the alignment, then fallback to center. + */ private fun computeTransformationGravity(input: Int): Int { return if (input != ZoomApi.TRANSFORMATION_GRAVITY_AUTO) { input From 06e618c5e23f86abd979b83f305c52638311e5a5 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 16:04:56 +0100 Subject: [PATCH 04/11] Create Alignment attrs and update Views --- .../java/com/otaliastudios/zoom/Alignment.kt | 21 ++++--- .../java/com/otaliastudios/zoom/ZoomApi.kt | 56 +++++++++---------- .../com/otaliastudios/zoom/ZoomImageView.kt | 4 +- .../java/com/otaliastudios/zoom/ZoomLayout.kt | 4 +- library/src/main/res/values/attrs.xml | 15 +++-- 5 files changed, 53 insertions(+), 47 deletions(-) diff --git a/library/src/main/java/com/otaliastudios/zoom/Alignment.kt b/library/src/main/java/com/otaliastudios/zoom/Alignment.kt index 35c2cde..669e184 100644 --- a/library/src/main/java/com/otaliastudios/zoom/Alignment.kt +++ b/library/src/main/java/com/otaliastudios/zoom/Alignment.kt @@ -2,24 +2,23 @@ package com.otaliastudios.zoom object Alignment { - // A shift of 4 means each axis can have 16 values. - internal const val SHIFT = 4 - internal const val MASK = 0xF0 // 11110000 + // Will use one hexadecimal value for each axis, so 16 possible values. + internal const val MASK = 0xF0 // 1111 0000 // A special value meaning that the flag for some axis was not set. internal const val NO_VALUE = 0x0 // Vertical - const val TOP = 0x1 // 0000 0001 - const val BOTTOM = 0x2 // 0000 0010 - const val CENTER_VERTICAL = 0x3 // 0000 0011 - const val NONE_VERTICAL = 0x4 // 0000 0100 + const val TOP = 0x01 // 0000 0001 + const val BOTTOM = 0x02 // 0000 0010 + const val CENTER_VERTICAL = 0x03 // 0000 0011 + const val NONE_VERTICAL = 0x04 // 0000 0100 // Horizontal - const val LEFT = 0x1 shl SHIFT // 0001 0000 - const val RIGHT = 0x2 shl SHIFT // 0010 0000 - const val CENTER_HORIZONTAL = 0x3 shl SHIFT // 0011 0000 - const val NONE_HORIZONTAL = 0x4 shl SHIFT // 0100 0000 + const val LEFT = 0x10 // 0001 0000 + const val RIGHT = 0x20 // 0010 0000 + const val CENTER_HORIZONTAL = 0x30 // 0011 0000 + const val NONE_HORIZONTAL = 0x40 // 0100 0000 // TODO support START and END diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt index 4454eb6..781a3a7 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt @@ -121,11 +121,23 @@ interface ZoomApi { annotation class Transformation /** - * Defines the available smaller policies + * Defines the available alignments */ @Retention(AnnotationRetention.SOURCE) - @IntDef(SMALLER_POLICY_CENTER, SMALLER_POLICY_FROM_TRANSFORMATION, SMALLER_POLICY_NONE) - annotation class SmallerPolicy + @IntDef( + com.otaliastudios.zoom.Alignment.BOTTOM, + com.otaliastudios.zoom.Alignment.CENTER_VERTICAL, + com.otaliastudios.zoom.Alignment.NONE_VERTICAL, + com.otaliastudios.zoom.Alignment.TOP, + com.otaliastudios.zoom.Alignment.LEFT, + com.otaliastudios.zoom.Alignment.CENTER_HORIZONTAL, + com.otaliastudios.zoom.Alignment.NONE_HORIZONTAL, + com.otaliastudios.zoom.Alignment.RIGHT, + com.otaliastudios.zoom.Alignment.CENTER, + com.otaliastudios.zoom.Alignment.NONE, + flag = true + ) + annotation class Alignment /** * Controls whether the content should be over-scrollable horizontally. @@ -210,13 +222,16 @@ interface ZoomApi { fun setTransformation(@Transformation transformation: Int, gravity: Int) /** - * Sets the policy to use when content is smaller than the container. - * Defaults to [SMALLER_POLICY_CENTER] - * which means that the content will be centered. + * Sets the content alignment. Can be any of the constants defined in [com.otaliastudios.zoom.Alignment]. + * The content will be aligned and forced to the specified side of the container. + * Defaults to [com.otaliastudios.zoom.Alignment.CENTER]. * - * @param policy the policy + * Of course, this is disabled when the content is larger than the container, + * because a forced alignment would mean making part of the content unreachable. + * + * @param alignment the new alignment */ - fun setSmallerPolicy(@SmallerPolicy policy: Int) + fun setAlignment(@Alignment alignment: Int) /** * A low level API that can animate both zoom and pan at the same time. @@ -370,26 +385,6 @@ interface ZoomApi { */ const val TRANSFORMATION_GRAVITY_AUTO = 0 - /** - * Constant for [ZoomApi.setSmallerPolicy]. - * When smaller than its container, the content will be centered. - */ - const val SMALLER_POLICY_CENTER = 0 - - /** - * Constant for [ZoomApi.setSmallerPolicy]. - * When smaller than its container, the content will be bound to the container - * according to the transformation gravity. - */ - const val SMALLER_POLICY_FROM_TRANSFORMATION = 1 - - /** - * Constant for [ZoomApi.setSmallerPolicy]. - * When smaller than its container, the content is free to be moved around. - * It will just stay inside the container bounds. - */ - const val SMALLER_POLICY_NONE = 2 - /** * The default [setMinZoom] applied by the engine if none is specified. */ @@ -409,5 +404,10 @@ interface ZoomApi { * The default [setMaxZoom] type applied by the engine if none is specified. */ const val MAX_ZOOM_DEFAULT_TYPE = TYPE_ZOOM + + /** + * The default value for [setAlignment]. + */ + const val ALIGNMENT_DEFAULT = com.otaliastudios.zoom.Alignment.CENTER } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt index 7973b89..d809210 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomImageView.kt @@ -53,14 +53,14 @@ private constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAtt @ZoomType val maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, ZoomApi.MAX_ZOOM_DEFAULT_TYPE) val transformation = a.getInteger(R.styleable.ZoomEngine_transformation, ZoomApi.TRANSFORMATION_CENTER_INSIDE) val transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, ZoomApi.TRANSFORMATION_GRAVITY_AUTO) - val smallerPolicy = a.getInt(R.styleable.ZoomEngine_smallerPolicy, ZoomApi.SMALLER_POLICY_CENTER) + val alignment = a.getInt(R.styleable.ZoomEngine_alignment, ZoomApi.ALIGNMENT_DEFAULT) val animationDuration = a.getInt(R.styleable.ZoomEngine_animationDuration, ZoomEngine.DEFAULT_ANIMATION_DURATION.toInt()).toLong() a.recycle() engine.setContainer(this) engine.addListener(this) setTransformation(transformation, transformationGravity) - setSmallerPolicy(smallerPolicy) + setAlignment(alignment) setOverScrollHorizontal(overScrollHorizontal) setOverScrollVertical(overScrollVertical) setHorizontalPanEnabled(horizontalPanEnabled) diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt index 0a0c4c0..a43a24a 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomLayout.kt @@ -59,14 +59,14 @@ private constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAtt @ZoomType val maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, ZoomApi.MAX_ZOOM_DEFAULT_TYPE) val transformation = a.getInteger(R.styleable.ZoomEngine_transformation, ZoomApi.TRANSFORMATION_CENTER_INSIDE) val transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, ZoomApi.TRANSFORMATION_GRAVITY_AUTO) - val smallerPolicy = a.getInt(R.styleable.ZoomEngine_smallerPolicy, ZoomApi.SMALLER_POLICY_CENTER) + val alignment = a.getInt(R.styleable.ZoomEngine_alignment, ZoomApi.ALIGNMENT_DEFAULT) val animationDuration = a.getInt(R.styleable.ZoomEngine_animationDuration, ZoomEngine.DEFAULT_ANIMATION_DURATION.toInt()).toLong() a.recycle() engine.setContainer(this) engine.addListener(this) setTransformation(transformation, transformationGravity) - setSmallerPolicy(smallerPolicy) + setAlignment(alignment) setOverScrollHorizontal(overScrollHorizontal) setOverScrollVertical(overScrollVertical) setHorizontalPanEnabled(horizontalPanEnabled) diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 6379450..c879d93 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -35,10 +35,17 @@ - - - - + + + + + + + + + + + From c7a154a367b7f0dfc19e6e9506754d98c2086aed Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 16:20:17 +0100 Subject: [PATCH 05/11] Implement Alignment in checkPanBounds --- .../java/com/otaliastudios/zoom/ZoomEngine.kt | 74 ++++++++++++------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index 86273c3..10cd71d 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -72,7 +72,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener private var mAllowFlingInOverscroll = false private var mTransformation = ZoomApi.TRANSFORMATION_CENTER_INSIDE private var mTransformationGravity = ZoomApi.TRANSFORMATION_GRAVITY_AUTO - private var mSmallerPolicy = ZoomApi.SMALLER_POLICY_CENTER + private var mAlignment = ZoomApi.ALIGNMENT_DEFAULT // Internal private val mListeners = mutableListOf() @@ -428,8 +428,18 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener mTransformationGravity = gravity } - override fun setSmallerPolicy(@SmallerPolicy policy: Int) { - mSmallerPolicy = policy + /** + * Sets the content alignment. Can be any of the constants defined in [Alignment]. + * The content will be aligned and forced to the specified side of the container. + * Defaults to [Alignment.CENTER]. + * + * Of course, this is disabled when the content is larger than the container, + * because a forced alignment would mean making part of the content unreachable. + * + * @param alignment the new alignment + */ + override fun setAlignment(alignment: Int) { + mAlignment = alignment } //endregion @@ -639,12 +649,26 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener * which might also be [ZoomApi.TRANSFORMATION_GRAVITY_AUTO]. In this case we should * try to infer a [Gravity] from the alignment, then fallback to center. */ + @SuppressLint("RtlHardcoded") private fun computeTransformationGravity(input: Int): Int { return if (input != ZoomApi.TRANSFORMATION_GRAVITY_AUTO) { input } else { - // TODO get from alignment if possible, then fallback to center. - input + val horizontalAlignment = Alignment.getHorizontal(mAlignment) + val verticalAlignment = Alignment.getVertical(mAlignment) + val horizontal = when (horizontalAlignment) { + Alignment.LEFT -> Gravity.LEFT + Alignment.RIGHT -> Gravity.RIGHT + Alignment.CENTER_HORIZONTAL, Alignment.NONE_HORIZONTAL -> Gravity.CENTER_HORIZONTAL + else -> Gravity.CENTER_HORIZONTAL + } + val vertical = when (verticalAlignment) { + Alignment.TOP -> Gravity.TOP + Alignment.BOTTOM -> Gravity.BOTTOM + Alignment.CENTER_VERTICAL, Alignment.NONE_VERTICAL -> Gravity.CENTER_VERTICAL + else -> Gravity.CENTER_VERTICAL + } + return horizontal or vertical } } @@ -664,7 +688,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener Gravity.TOP, Gravity.LEFT -> 0F Gravity.BOTTOM, Gravity.RIGHT -> extraSpace Gravity.CENTER_VERTICAL, Gravity.CENTER_HORIZONTAL -> 0.5F * extraSpace - else -> 0F // Can't happen + else -> 0F // Includes Gravity.NO_GRAVITY and unsupported mixes like FILL } } @@ -750,6 +774,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener * * @return the pan correction to be applied to get into a valid state (0 if valid already) */ + @SuppressLint("RtlHardcoded") @ScaledPan private fun checkPanBounds(horizontal: Boolean, allowOverScroll: Boolean): Float { @ScaledPan val value = if (horizontal) scaledPanX else scaledPanY @@ -757,31 +782,30 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener @ScaledPan val contentSize = if (horizontal) mContentScaledWidth else mContentScaledHeight val overScrollable = if (horizontal) mOverScrollHorizontal else mOverScrollVertical @ScaledPan val overScroll = (if (overScrollable && allowOverScroll) maxOverScroll else 0).toFloat() + val alignment = if (horizontal) Alignment.getHorizontal(mAlignment) else Alignment.getVertical(mAlignment) var min: Float var max: Float if (contentSize <= containerSize) { - // If content is smaller than container, act according to the smaller policy. + // If content is smaller than container, act according to the alignment. // Expect the output to be >= 0, we will show part of the container background. val extraSpace = containerSize - contentSize // > 0 - when (mSmallerPolicy) { - ZoomApi.SMALLER_POLICY_FROM_TRANSFORMATION -> { - // Apply the transformation gravity. - val correction = applyGravity(mTransformation, extraSpace, horizontal) - min = correction - max = correction - } - ZoomApi.SMALLER_POLICY_CENTER -> { - // Stay centered. Need a positive translation, that shows some background. - min = extraSpace / 2f - max = extraSpace / 2f - } - ZoomApi.SMALLER_POLICY_NONE -> { - // Everything is fine, just don't exit the container boundaries. - min = 0f - max = extraSpace - } - else -> throw IllegalStateException("Unsupported policy: $mSmallerPolicy") + val correction = when (alignment) { + Alignment.TOP -> applyGravity(Gravity.TOP, extraSpace, horizontal) + Alignment.BOTTOM -> applyGravity(Gravity.BOTTOM, extraSpace, horizontal) + Alignment.LEFT -> applyGravity(Gravity.LEFT, extraSpace, horizontal) + Alignment.RIGHT -> applyGravity(Gravity.RIGHT, extraSpace, horizontal) + Alignment.CENTER_VERTICAL -> applyGravity(Gravity.CENTER_VERTICAL, extraSpace, horizontal) + Alignment.CENTER_HORIZONTAL -> applyGravity(Gravity.CENTER_HORIZONTAL, extraSpace, horizontal) + else -> null + } + if (correction != null) { + min = correction + max = correction + } else { + // This is Alignment.NONE. Don't force a value, just stay in the container boundaries. + min = 0F + max = extraSpace } } else { // If contentSize is bigger, we just don't want to go outside. From c19fe3d5c68e791d298cc0acb4a532673903b817 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 16:45:54 +0100 Subject: [PATCH 06/11] Update docs --- README.md | 78 ++++++++++++------- .../java/com/otaliastudios/zoom/ZoomEngine.kt | 3 - 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index c73af1e..a67fb2a 100644 --- a/README.md +++ b/README.md @@ -43,16 +43,19 @@ A container for view hierarchies that can be panned or zoomed. android:scrollbars="vertical|horizontal" app:transformation="centerInside" app:transformationGravity="auto" + app:alignment="center" app:overScrollHorizontal="true" app:overScrollVertical="true" app:overPinchable="true" app:horizontalPanEnabled="true" app:verticalPanEnabled="true" app:zoomEnabled="true" + app:flingEnabled="true" app:minZoom="0.7" app:minZoomType="zoom" - app:maxZoom="3.0" + app:maxZoom="2.5" app:maxZoomType="zoom" + app:animationDuration="280" app:hasClickableChildren="false"> @@ -93,24 +96,27 @@ An `ImageView` implementation to control pan and zoom over its Drawable or Bitma android:layout_height="match_parent" android:scrollbars="vertical|horizontal" app:transformation="centerInside" - app:transformationGravity="auto" + app:transformationGravity="auto" + app:alignment="center" app:overScrollHorizontal="true" app:overScrollVertical="true" app:overPinchable="true" app:horizontalPanEnabled="true" app:verticalPanEnabled="true" app:zoomEnabled="true" + app:flingEnabled="true" app:minZoom="0.7" app:minZoomType="zoom" - app:maxZoom="3.0" - app:maxZoomType="zoom"/> + app:maxZoom="2.5" + app:maxZoomType="zoom" + app:animationDuration="280"/> ``` There is nothing surprising going on. Just call `setImageDrawable()` and you are done. Presumably ZoomImageView **won't** work if: -- the drawable has no intrinsic dimensions +- the drawable has no intrinsic dimensions (like a ColorDrawable) - the view has wrap_content as a dimension - you change the scaleType (read [later](#zoom) to know more) @@ -146,11 +152,17 @@ There is no strict limit over what you can do with a `Matrix`, ### Zoom -#### Transformations +#### Transformation + +The transformation defines the engine **resting position**. It is a keyframe that is reached at +certain points, like at start-up or when explicitly requested through `setContentSize` or `setContainerSize`. + +The keyframe is defined by two elements: -When the engine becomes aware of the content size, it will apply a base transformation to the content -that can be controlled through `setTransformation(int, int)` or `app:transformation` and `app:transformationGravity`. -By default it is applied only once, and defines the starting viewport over our content. +- a `transformation` value (modifies zoom in a certain way) +- a `transformationGravity` value (modifies pan in a certain way) + +which can be controlled through `setTransformation(int, int)` or `app:transformation` and `app:transformationGravity`. |Transformation|Description| |--------------|-----------| @@ -158,34 +170,42 @@ By default it is applied only once, and defines the starting viewport over our c |`centerCrop`|The content is scaled down or up so that its smaller side fits exactly inside the view bounds. The larger side will be cropped.| |`none`|No transformation is applied.| -The engine applies the given transformation, and any minZoom and maxZoom constraints. - -If, after this process, the content is bigger than the container, the engine will also apply a -translation according to the given transformation gravity. +After transformation is applied, the transformation gravity will reposition the content with +the specified value. Supported values are most of the Android `Gravity` flags, plus `TRANSFORMATION_GRAVITY_AUTO`. |Transformation Gravity|Description| |----------------------|-----------| -|`top`|If the content is taller than the view, translate it so that we see the top part.| -|`bottom`|If the content is taller than the view, translate it so that we see the bottom part.| -|`left`|If the content is wider than the view, translate it so that we see the left part.| -|`right`|If the content is wider than the view, translate it so that we see the right part.| +|`top`, ...|The content is panned so that its *top* side matches teh container *top* side. Same for other values.| +|`auto` (default)|The transformation gravity is taken from the engine [alignment](#alignment), defaults to `center` on both axes.| -If, after this process, the content is smaller than the container, note that the current -[Smaller Policy](#smaller-policy) applies. +**Note: after transformation and gravity are applied, the engine will apply - as always - all the active constraints, +including minZoom, maxZoom, alignment. This means that the final position might be slightly (or completely) different.** -Note: you can always trigger a new transformation to be applied by using the `setContentSize` or `setContainerSize` APIs. +For example, when `maxZoom == 1`, is forced to not be any larger than the container. This means that +a `centerCrop` transformation will not work (will act just like a `centerInside`). -#### Smaller policy +#### Alignment -You can control how the content will be positioned when it is smaller than the container through -the `setSmallerPolicy(int)` method or by using the `smallerPolicy` XML attribute of `ZoomLayout` and `ZoomImageView`. -By default, content is always centered when it is smaller than its container. +You can force the content position with respect to the container using the `setAlignment(int)` method +or the `alignment` XML flag of `ZoomLayout` and `ZoomImageView`. +The default value is `Alignment.CENTER` which will center the content on both directions. -|Policy|Description| -|------|-----------| -|`center`|The content will be centered within the container.| -|`fromTransformation`|The content will respect the gravity parameter of the transformation (which defaults to `Gravity.CENTER` as well).| -|`none`|The content is free to be moved around inside the container bounds.| +**Note: alignment does not make sense when content is larger than the container, because forcing an +alignment (e.g. left) would mean making part of the content unreachable (e.g. the right part).** + +|Alignment|Description| +|---------|-----------| +|`top`, `bottom`, `left`, `right`|Force align the content to the same side of the container.| +|`center_horizontal`, `center_vertical`|Force the content to be centered inside the container on that axis.| +|`none_horizontal`, `none_vertical`|No alignment set: content is free to be moved on that axis.| + +You can use the or operation to mix the vertical and horizontal flags. + +```kotlin +engine.setAlignment(Alignment.TOP or Alignment.LEFT) +engine.setAlignment(Alignment.TOP) // Equals to Aligment.TOP or Alignment.NONE_HORIZONTAL +engine.setAlignment(Alignment.NONE) // Remove any forced alignment +``` #### Zoom Types diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index 10cd71d..6cbef83 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -625,9 +625,6 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener * Computes the starting pan coordinates, given the current content dimensions and container * dimensions. We will start from [0, 0], unless content is bigger than the container, in which * case the transformation gravity should apply. - * - * Note: after this is computed and applied, the pan correction will take care of respecting the - * SmallerPolicy if needed. */ @ScaledPan private fun computeBasePan(): FloatArray { From 9af405017a836aac225161a7b3c9f75b536abc2d Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 16:56:21 +0100 Subject: [PATCH 07/11] Remove checks to be consistent with docs --- .../main/java/com/otaliastudios/zoom/ZoomEngine.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index 6cbef83..d8dd8cc 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -623,8 +623,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener /** * Computes the starting pan coordinates, given the current content dimensions and container - * dimensions. We will start from [0, 0], unless content is bigger than the container, in which - * case the transformation gravity should apply. + * dimensions. This means applying the transformation gravity. */ @ScaledPan private fun computeBasePan(): FloatArray { @@ -632,12 +631,8 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener val extraWidth = mContentScaledWidth - mContainerWidth val extraHeight = mContentScaledHeight - mContainerHeight val gravity = computeTransformationGravity(mTransformationGravity) - if (extraWidth > 0) { // Got to change sign to have a negative result. - result[0] = -applyGravity(gravity, extraWidth, true) - } - if (extraHeight > 0) { - result[1] = -applyGravity(gravity, extraHeight, false) - } + result[0] = -applyGravity(gravity, extraWidth, true) + result[1] = -applyGravity(gravity, extraHeight, false) return result } From 098bd475424b7b49c465c6666d50a341c297e8da Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 17:00:44 +0100 Subject: [PATCH 08/11] Rename methods and fields --- .../java/com/otaliastudios/zoom/ZoomEngine.kt | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index d8dd8cc..1b523cb 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -77,13 +77,13 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener // Internal private val mListeners = mutableListOf() private var mMatrix = Matrix() - private var mBaseZoom = 0F // mZoom * mBaseZoom matches the matrix scale. + private var mTransformationZoom = 0F // mZoom * mTransformationZoom matches the matrix scale. @State private var mState = NONE private lateinit var mContainer: View private var mContainerWidth = 0F private var mContainerHeight = 0F private var mInitialized = false - private var mTransformedRect = RectF() + private var mContentScaledRect = RectF() private var mContentRect = RectF() private var mClearAnimation = false private var mAnimationDuration = DEFAULT_ANIMATION_DURATION @@ -97,11 +97,11 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener @ScaledPan private val mContentScaledWidth: Float - get() = mTransformedRect.width() + get() = mContentScaledRect.width() @ScaledPan private val mContentScaledHeight: Float - get() = mTransformedRect.height() + get() = mContentScaledRect.height() @AbsolutePan private val mContentWidth: Float @@ -170,7 +170,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener */ @RealZoom override val realZoom: Float - get() = zoom * mBaseZoom + get() = zoom * mTransformationZoom /** * The current pan as an [AbsolutePoint]. @@ -224,7 +224,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener */ @ScaledPan private val scaledPanX: Float - get() = mTransformedRect.left + get() = mContentScaledRect.left /** * The current vertical scaled pan, which is the pan position of the content @@ -232,7 +232,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener */ @ScaledPan private val scaledPanY: Float - get() = mTransformedRect.top + get() = mContentScaledRect.top @Retention(AnnotationRetention.SOURCE) @IntDef(NONE, SCROLLING, PINCHING, ANIMATING, FLINGING) @@ -513,7 +513,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener private fun onSizeChanged(applyTransformation: Boolean) { // We will sync them later using matrix.mapRect. - mTransformedRect.set(mContentRect) + mContentScaledRect.set(mContentRect) if (mContentWidth <= 0 || mContentHeight <= 0 @@ -533,17 +533,17 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener LOG.w("onSizeChanged: will apply?", apply, "transformation?", mTransformation) if (apply) { // First time. Apply base zoom, dispatch first event and return. - mBaseZoom = computeBaseZoom() - mMatrix.setScale(mBaseZoom, mBaseZoom) - mMatrix.mapRect(mTransformedRect, mContentRect) + mTransformationZoom = computeTransformationZoom() + mMatrix.setScale(mTransformationZoom, mTransformationZoom) + mMatrix.mapRect(mContentScaledRect, mContentRect) zoom = 1f - LOG.i("onSizeChanged: newBaseZoom:", mBaseZoom, "newZoom:", zoom) + LOG.i("onSizeChanged: newTransformationZoom:", mTransformationZoom, "newZoom:", zoom) @Zoom val newZoom = checkZoomBounds(zoom, false) LOG.i("onSizeChanged: scaleBounds:", "we need a zoom correction of", newZoom - zoom) if (newZoom != zoom) applyZoom(newZoom, allowOverPinch = false) // pan based on transformation gravity. - @ScaledPan val newPan = computeBasePan() + @ScaledPan val newPan = computeTransformationPan() @ScaledPan val deltaX = newPan[0] - scaledPanX @ScaledPan val deltaY = newPan[1] - scaledPanY if (deltaX != 0f || deltaY != 0f) applyScaledPan(deltaX, deltaY, false) @@ -555,19 +555,19 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener } } else { // We were initialized, but some size changed. Since applyTransformation is false, - // we must do extra work: recompute the baseZoom (since size changed, it makes no sense) + // we must do extra work: recompute the transformationZoom (since size changed, it makes no sense) // but also compute a new zoom such that the real zoom is kept unchanged. // So, this method triggers no Matrix updates. LOG.i("onSizeChanged: Trying to keep real zoom to", realZoom) - LOG.i("onSizeChanged: oldBaseZoom:", mBaseZoom, "oldZoom:$zoom") + LOG.i("onSizeChanged: oldTransformationZoom:", mTransformationZoom, "oldZoom:$zoom") @RealZoom val realZoom = realZoom - mBaseZoom = computeBaseZoom() - zoom = realZoom / mBaseZoom - LOG.i("onSizeChanged: newBaseZoom:", mBaseZoom, "newZoom:", zoom) + mTransformationZoom = computeTransformationZoom() + zoom = realZoom / mTransformationZoom + LOG.i("onSizeChanged: newTransformationZoom:", mTransformationZoom, "newZoom:", zoom) // Now sync the content rect with the current matrix since we are trying to keep it. // This is to have consistent values for other calls here. - mMatrix.mapRect(mTransformedRect, mContentRect) + mMatrix.mapRect(mContentScaledRect, mContentRect) // If the new zoom value is invalid, though, we must bring it to the valid place. // This is a possible matrix update. @@ -592,8 +592,8 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener mContainerHeight = 0f mContainerWidth = 0f zoom = 1f - mBaseZoom = 0f - mTransformedRect = RectF() + mTransformationZoom = 0f + mContentScaledRect = RectF() mContentRect = RectF() mMatrix = Matrix() mInitialized = false @@ -602,18 +602,18 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener /** * Computes the starting zoom, which means applying the transformation. */ - private fun computeBaseZoom(): Float { + private fun computeTransformationZoom(): Float { when (mTransformation) { ZoomApi.TRANSFORMATION_CENTER_INSIDE -> { val scaleX = mContainerWidth / mContentScaledWidth val scaleY = mContainerHeight / mContentScaledHeight - LOG.v("computeBaseZoom", "centerInside", "scaleX:", scaleX, "scaleY:", scaleY) + LOG.v("computeTransformationZoom", "centerInside", "scaleX:", scaleX, "scaleY:", scaleY) return Math.min(scaleX, scaleY) } ZoomApi.TRANSFORMATION_CENTER_CROP -> { val scaleX = mContainerWidth / mContentScaledWidth val scaleY = mContainerHeight / mContentScaledHeight - LOG.v("computeBaseZoom", "centerCrop", "scaleX:", scaleX, "scaleY:", scaleY) + LOG.v("computeTransformationZoom", "centerCrop", "scaleX:", scaleX, "scaleY:", scaleY) return Math.max(scaleX, scaleY) } ZoomApi.TRANSFORMATION_NONE -> return 1f @@ -626,7 +626,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener * dimensions. This means applying the transformation gravity. */ @ScaledPan - private fun computeBasePan(): FloatArray { + private fun computeTransformationPan(): FloatArray { val result = floatArrayOf(0f, 0f) val extraWidth = mContentScaledWidth - mContainerWidth val extraHeight = mContentScaledHeight - mContainerHeight @@ -740,7 +740,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener @ScaledPan val fixY = checkPanBounds(false, allowOverScroll) if (fixX != 0f || fixY != 0f) { mMatrix.postTranslate(fixX, fixY) - mMatrix.mapRect(mTransformedRect, mContentRect) + mMatrix.mapRect(mContentScaledRect, mContentRect) } } @@ -1449,7 +1449,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener mMatrix.postScale(scaleFactor, scaleFactor, zoomTargetX ?: mContainerWidth / 2f, zoomTargetY ?: mContainerHeight / 2f) - mMatrix.mapRect(mTransformedRect, mContentRect) + mMatrix.mapRect(mContentScaledRect, mContentRect) this.zoom = newZoom ensurePanBounds(allowOverScroll) if (notifyListeners) { @@ -1484,7 +1484,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener // Translation val delta = AbsolutePoint(x, y) - pan mMatrix.preTranslate(delta.x, delta.y) - mMatrix.mapRect(mTransformedRect, mContentRect) + mMatrix.mapRect(mContentScaledRect, mContentRect) // Scale val newZoom = checkZoomBounds(zoom, allowOverPinch) @@ -1498,7 +1498,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener val pivotY = zoomTargetY ?: 0F mMatrix.postScale(scaleFactor, scaleFactor, pivotX, pivotY) - mMatrix.mapRect(mTransformedRect, mContentRect) + mMatrix.mapRect(mContentScaledRect, mContentRect) this.zoom = newZoom ensurePanBounds(allowOverScroll) @@ -1519,7 +1519,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener */ private fun applyScaledPan(@ScaledPan deltaX: Float, @ScaledPan deltaY: Float, allowOverScroll: Boolean) { mMatrix.postTranslate(deltaX, deltaY) - mMatrix.mapRect(mTransformedRect, mContentRect) + mMatrix.mapRect(mContentScaledRect, mContentRect) ensurePanBounds(allowOverScroll) dispatchOnMatrix() } @@ -1675,7 +1675,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener private fun Float.toZoom(@ZoomType inputZoomType: Int): Float { when (inputZoomType) { ZoomApi.TYPE_ZOOM -> return this - ZoomApi.TYPE_REAL_ZOOM -> return this / mBaseZoom + ZoomApi.TYPE_REAL_ZOOM -> return this / mTransformationZoom } throw IllegalArgumentException("Unknown ZoomType $inputZoomType") } @@ -1686,7 +1686,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener @RealZoom private fun Float.toRealZoom(@ZoomType inputZoomType: Int): Float { when (inputZoomType) { - ZoomApi.TYPE_ZOOM -> return this * mBaseZoom + ZoomApi.TYPE_ZOOM -> return this * mTransformationZoom ZoomApi.TYPE_REAL_ZOOM -> return this } throw IllegalArgumentException("Unknown ZoomType $inputZoomType") From 7df592becf3c637d62dc7b7595c2db7f9df41ebb Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sat, 12 Jan 2019 22:08:11 +0100 Subject: [PATCH 09/11] Fix fling with Alignment.NONE --- .../java/com/otaliastudios/zoom/ZoomEngine.kt | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index 1b523cb..a810e08 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -795,7 +795,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener min = correction max = correction } else { - // This is Alignment.NONE. Don't force a value, just stay in the container boundaries. + // This is Alignment.NONE or NO_VALUE. Don't force a value, just stay in the container boundaries. min = 0F max = extraSpace } @@ -1533,32 +1533,38 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener // while max values are related to top-left. private fun computeScrollerValues(horizontal: Boolean, output: ScrollerValues) { @ScaledPan val currentPan = (if (horizontal) scaledPanX else scaledPanY).toInt() - val viewDim = (if (horizontal) mContainerWidth else mContainerHeight).toInt() + val containerDim = (if (horizontal) mContainerWidth else mContainerHeight).toInt() @ScaledPan val contentDim = (if (horizontal) mContentScaledWidth else mContentScaledHeight).toInt() val fix = checkPanBounds(horizontal, false).toInt() - if (viewDim >= contentDim) { - // Content is smaller, we are showing some boundary. - // We can't move in any direction (but we can overScroll). - output.minValue = currentPan + fix - output.startValue = currentPan - output.maxValue = currentPan + fix - } else { - // Content is bigger, we can move. - // in this case minPan + viewDim = contentDim - output.minValue = -(contentDim - viewDim) - output.startValue = currentPan + val alignment = if (horizontal) Alignment.getHorizontal(mAlignment) else Alignment.getVertical(mAlignment) + if (contentDim > containerDim) { + // Content is bigger. We can move between 0 and extraSpace, but since our pans + // are negative, we must invert the sign. + val extraSpace = contentDim - containerDim + output.minValue = -extraSpace output.maxValue = 0 + } else if (alignment == Alignment.NO_VALUE + || alignment == Alignment.NONE_VERTICAL + || alignment == Alignment.NONE_HORIZONTAL) { + // Content is free to be moved, although smaller than the container. We can move + // between 0 and extraSpace (and when content is smaller, pan is positive). + val extraSpace = containerDim - contentDim + output.minValue = 0 + output.maxValue = extraSpace + } else { + // Content can't move in this dimensions. Go back to the correct value. + val finalValue = currentPan + fix + output.minValue = finalValue + output.maxValue = finalValue } + output.startValue = currentPan output.isInOverScroll = fix != 0 } private class ScrollerValues { - @ScaledPan - internal var minValue: Int = 0 - @ScaledPan - internal var startValue: Int = 0 - @ScaledPan - internal var maxValue: Int = 0 + @ScaledPan internal var minValue: Int = 0 + @ScaledPan internal var startValue: Int = 0 + @ScaledPan internal var maxValue: Int = 0 internal var isInOverScroll: Boolean = false } From 08cda6464344a8980c8a2cd465023dfa51a53897 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Sun, 13 Jan 2019 14:48:44 +0100 Subject: [PATCH 10/11] Address review, move gravity logic to Alignment --- README.md | 8 +- .../java/com/otaliastudios/zoom/Alignment.kt | 78 +++++++++++++++++++ .../java/com/otaliastudios/zoom/ZoomApi.kt | 8 +- .../java/com/otaliastudios/zoom/ZoomEngine.kt | 53 +++++-------- 4 files changed, 104 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index a67fb2a..5f0eab0 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ which can be controlled through `setTransformation(int, int)` or `app:transforma |`none`|No transformation is applied.| After transformation is applied, the transformation gravity will reposition the content with -the specified value. Supported values are most of the Android `Gravity` flags, plus `TRANSFORMATION_GRAVITY_AUTO`. +the specified value. Supported values are most of the `android.view.Gravity` flags like `Gravity.TOP`, plus `TRANSFORMATION_GRAVITY_AUTO`. |Transformation Gravity|Description| |----------------------|-----------| @@ -181,8 +181,8 @@ the specified value. Supported values are most of the Android `Gravity` flags, p **Note: after transformation and gravity are applied, the engine will apply - as always - all the active constraints, including minZoom, maxZoom, alignment. This means that the final position might be slightly (or completely) different.** -For example, when `maxZoom == 1`, is forced to not be any larger than the container. This means that -a `centerCrop` transformation will not work (will act just like a `centerInside`). +For example, when `maxZoom == 1`, the content is forced to not be any larger than the container. This means that +a `centerCrop` transformation will not have the desired effect: it will act just like a `centerInside`. #### Alignment @@ -199,7 +199,7 @@ alignment (e.g. left) would mean making part of the content unreachable (e.g. th |`center_horizontal`, `center_vertical`|Force the content to be centered inside the container on that axis.| |`none_horizontal`, `none_vertical`|No alignment set: content is free to be moved on that axis.| -You can use the or operation to mix the vertical and horizontal flags. +You can use the `or` operation to mix the vertical and horizontal flags: ```kotlin engine.setAlignment(Alignment.TOP or Alignment.LEFT) diff --git a/library/src/main/java/com/otaliastudios/zoom/Alignment.kt b/library/src/main/java/com/otaliastudios/zoom/Alignment.kt index 669e184..520b594 100644 --- a/library/src/main/java/com/otaliastudios/zoom/Alignment.kt +++ b/library/src/main/java/com/otaliastudios/zoom/Alignment.kt @@ -1,5 +1,11 @@ package com.otaliastudios.zoom +import android.annotation.SuppressLint +import android.view.Gravity + +/** + * Holds constants for [ZoomApi.setAlignment]. + */ object Alignment { // Will use one hexadecimal value for each axis, so 16 possible values. @@ -9,15 +15,47 @@ object Alignment { internal const val NO_VALUE = 0x0 // Vertical + + /** + * Aligns top side of the content to the top side of the container. + */ const val TOP = 0x01 // 0000 0001 + + /** + * Aligns the bottom side of the content to the bottom side of the container. + */ const val BOTTOM = 0x02 // 0000 0010 + + /** + * Centers the content vertically inside the container. + */ const val CENTER_VERTICAL = 0x03 // 0000 0011 + + /** + * No forced alignment on the vertical axis. + */ const val NONE_VERTICAL = 0x04 // 0000 0100 // Horizontal + + /** + * Aligns left side of the content to the left side of the container. + */ const val LEFT = 0x10 // 0001 0000 + + /** + * Aligns right side of the content to the right side of the container. + */ const val RIGHT = 0x20 // 0010 0000 + + /** + * Centers the content horizontally inside the container. + */ const val CENTER_HORIZONTAL = 0x30 // 0011 0000 + + /** + * No forced alignment on the horizontal axis. + */ const val NONE_HORIZONTAL = 0x40 // 0100 0000 // TODO support START and END @@ -47,4 +85,44 @@ object Alignment { internal fun getVertical(alignment: Int): Int { return alignment and MASK.inv() } + + /** + * Returns whether this alignment is of 'none' type. + * In case [alignment] includes both axes, both are required to be 'none' or [NO_VALUE]. + */ + internal fun isNone(alignment: Int): Boolean { + return alignment == Alignment.NONE + || alignment == Alignment.NO_VALUE + || alignment == Alignment.NONE_HORIZONTAL + || alignment == Alignment.NONE_VERTICAL + } + + /** + * Transforms this alignment to a horizontal gravity value. + */ + @SuppressLint("RtlHardcoded") + internal fun toHorizontalGravity(alignment: Int, valueIfNone: Int): Int { + val horizontalAlignment = getHorizontal(alignment) + return when (horizontalAlignment) { + Alignment.LEFT -> Gravity.LEFT + Alignment.RIGHT -> Gravity.RIGHT + Alignment.CENTER_HORIZONTAL -> Gravity.CENTER_HORIZONTAL + Alignment.NONE_HORIZONTAL -> valueIfNone + else -> valueIfNone + } + } + + /** + * Transforms this alignment to a vertical gravity value. + */ + internal fun toVerticalGravity(alignment: Int, valueIfNone: Int): Int { + val verticalAlignment = getHorizontal(alignment) + return when (verticalAlignment) { + Alignment.TOP -> Gravity.TOP + Alignment.BOTTOM -> Gravity.BOTTOM + Alignment.CENTER_VERTICAL -> Gravity.CENTER_VERTICAL + Alignment.NONE_VERTICAL -> valueIfNone + else -> valueIfNone + } + } } \ No newline at end of file diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt index 781a3a7..39c011e 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt @@ -224,10 +224,10 @@ interface ZoomApi { /** * Sets the content alignment. Can be any of the constants defined in [com.otaliastudios.zoom.Alignment]. * The content will be aligned and forced to the specified side of the container. - * Defaults to [com.otaliastudios.zoom.Alignment.CENTER]. + * Defaults to [ALIGNMENT_DEFAULT]. * - * Of course, this is disabled when the content is larger than the container, - * because a forced alignment would mean making part of the content unreachable. + * Keep in mind that this is disabled when the content is larger than the container, + * because a forced alignment in this case would result in part of the content being unreachable. * * @param alignment the new alignment */ @@ -380,7 +380,7 @@ interface ZoomApi { /** * Constant for [ZoomApi.setTransformation] gravity. - * This currently means that the gravity will be inferred from the alignment or + * This means that the gravity will be inferred from the alignment or * fallback to a reasonable default. */ const val TRANSFORMATION_GRAVITY_AUTO = 0 diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index a810e08..9c65015 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -431,14 +431,14 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener /** * Sets the content alignment. Can be any of the constants defined in [Alignment]. * The content will be aligned and forced to the specified side of the container. - * Defaults to [Alignment.CENTER]. + * Defaults to [ZoomApi.ALIGNMENT_DEFAULT]. * - * Of course, this is disabled when the content is larger than the container, - * because a forced alignment would mean making part of the content unreachable. + * Keep in mind that this is disabled when the content is larger than the container, + * because a forced alignment in this case would result in part of the content being unreachable. * * @param alignment the new alignment */ - override fun setAlignment(alignment: Int) { + override fun setAlignment(@ZoomApi.Alignment alignment: Int) { mAlignment = alignment } @@ -643,24 +643,13 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener */ @SuppressLint("RtlHardcoded") private fun computeTransformationGravity(input: Int): Int { - return if (input != ZoomApi.TRANSFORMATION_GRAVITY_AUTO) { - input - } else { - val horizontalAlignment = Alignment.getHorizontal(mAlignment) - val verticalAlignment = Alignment.getVertical(mAlignment) - val horizontal = when (horizontalAlignment) { - Alignment.LEFT -> Gravity.LEFT - Alignment.RIGHT -> Gravity.RIGHT - Alignment.CENTER_HORIZONTAL, Alignment.NONE_HORIZONTAL -> Gravity.CENTER_HORIZONTAL - else -> Gravity.CENTER_HORIZONTAL - } - val vertical = when (verticalAlignment) { - Alignment.TOP -> Gravity.TOP - Alignment.BOTTOM -> Gravity.BOTTOM - Alignment.CENTER_VERTICAL, Alignment.NONE_VERTICAL -> Gravity.CENTER_VERTICAL - else -> Gravity.CENTER_VERTICAL + return when (input) { + ZoomApi.TRANSFORMATION_GRAVITY_AUTO -> { + val horizontal = Alignment.toHorizontalGravity(mAlignment, Gravity.CENTER_HORIZONTAL) + val vertical = Alignment.toVerticalGravity(mAlignment, Gravity.CENTER_VERTICAL) + return horizontal or vertical } - return horizontal or vertical + else -> input } } @@ -774,7 +763,11 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener @ScaledPan val contentSize = if (horizontal) mContentScaledWidth else mContentScaledHeight val overScrollable = if (horizontal) mOverScrollHorizontal else mOverScrollVertical @ScaledPan val overScroll = (if (overScrollable && allowOverScroll) maxOverScroll else 0).toFloat() - val alignment = if (horizontal) Alignment.getHorizontal(mAlignment) else Alignment.getVertical(mAlignment) + val alignmentGravity = if (horizontal) { + Alignment.toHorizontalGravity(mAlignment, Gravity.NO_GRAVITY) + } else { + Alignment.toVerticalGravity(mAlignment, Gravity.NO_GRAVITY) + } var min: Float var max: Float @@ -782,16 +775,8 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener // If content is smaller than container, act according to the alignment. // Expect the output to be >= 0, we will show part of the container background. val extraSpace = containerSize - contentSize // > 0 - val correction = when (alignment) { - Alignment.TOP -> applyGravity(Gravity.TOP, extraSpace, horizontal) - Alignment.BOTTOM -> applyGravity(Gravity.BOTTOM, extraSpace, horizontal) - Alignment.LEFT -> applyGravity(Gravity.LEFT, extraSpace, horizontal) - Alignment.RIGHT -> applyGravity(Gravity.RIGHT, extraSpace, horizontal) - Alignment.CENTER_VERTICAL -> applyGravity(Gravity.CENTER_VERTICAL, extraSpace, horizontal) - Alignment.CENTER_HORIZONTAL -> applyGravity(Gravity.CENTER_HORIZONTAL, extraSpace, horizontal) - else -> null - } - if (correction != null) { + if (alignmentGravity != Gravity.NO_GRAVITY) { + val correction = applyGravity(alignmentGravity, extraSpace, horizontal) min = correction max = correction } else { @@ -1543,9 +1528,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener val extraSpace = contentDim - containerDim output.minValue = -extraSpace output.maxValue = 0 - } else if (alignment == Alignment.NO_VALUE - || alignment == Alignment.NONE_VERTICAL - || alignment == Alignment.NONE_HORIZONTAL) { + } else if (Alignment.isNone(alignment)) { // Content is free to be moved, although smaller than the container. We can move // between 0 and extraSpace (and when content is smaller, pan is positive). val extraSpace = containerDim - contentDim From ffc6e1d53953e4dea51252465f3cc45af0924f33 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Wed, 23 Jan 2019 16:40:32 +0100 Subject: [PATCH 11/11] Improve #79 --- .../java/com/otaliastudios/zoom/ZoomEngine.kt | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt index 99e61e8..f682be2 100644 --- a/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt +++ b/library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt @@ -1043,10 +1043,9 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener } override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { - if (!mFlingEnabled) { - // fling is disabled, so we just ignore the event - return false - } + // If disabled, don't start the gesture. + if (!mFlingEnabled) return false + if (!mHorizontalPanEnabled && !mVerticalPanEnabled) return false val vX = (if (mHorizontalPanEnabled) velocityX else 0F).toInt() val vY = (if (mVerticalPanEnabled) velocityY else 0F).toInt() @@ -1064,15 +1063,11 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener */ override fun onScroll(e1: MotionEvent, e2: MotionEvent, @AbsolutePan distanceX: Float, @AbsolutePan distanceY: Float): Boolean { + if (!mHorizontalPanEnabled && !mVerticalPanEnabled) return false - if (!mHorizontalPanEnabled && !mVerticalPanEnabled) { - return false - } - - var delta = AbsolutePoint(distanceX, distanceY) if (setState(SCROLLING)) { // Change sign, since we work with opposite values. - delta = -delta + val delta = AbsolutePoint(-distanceX, -distanceY) // See if we are overscrolling. val panFix = mCurrentPanCorrection @@ -1376,7 +1371,9 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener } val newZoom = it.getAnimatedValue("zoom") as Float val currentPan = it.getAnimatedValue("pan") as AbsolutePoint - applyZoomAndAbsolutePan(newZoom, currentPan.x, currentPan.y, allowOverScroll, allowOverPinch, zoomTargetX, zoomTargetY) + applyZoomAndAbsolutePan(newZoom, currentPan.x, currentPan.y, + allowOverScroll, allowOverPinch, + zoomTargetX, zoomTargetY) } animator.start() } @@ -1431,14 +1428,12 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener private fun applyZoom(@Zoom zoom: Float, allowOverPinch: Boolean, allowOverScroll: Boolean = false, - zoomTargetX: Float? = null, - zoomTargetY: Float? = null, + zoomTargetX: Float = mContainerWidth / 2f, + zoomTargetY: Float = mContainerHeight / 2f, notifyListeners: Boolean = true) { val newZoom = checkZoomBounds(zoom, allowOverPinch) val scaleFactor = newZoom / this.zoom - - mMatrix.postScale(scaleFactor, scaleFactor, - zoomTargetX ?: mContainerWidth / 2f, zoomTargetY ?: mContainerHeight / 2f) + mMatrix.postScale(scaleFactor, scaleFactor, zoomTargetX, zoomTargetY) mMatrix.mapRect(mContentScaledRect, mContentRect) this.zoom = newZoom ensurePanBounds(allowOverScroll) @@ -1483,10 +1478,8 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener // mMatrix.postScale(scaleFactor, scaleFactor, getScaledPanX(), getScaledPanY()); // It keeps the pivot point at the scaled values 0, 0 (see applyPinch). // I think we should keep the current top, left.. Let's try: - val pivotX = zoomTargetX ?: 0F val pivotY = zoomTargetY ?: 0F - mMatrix.postScale(scaleFactor, scaleFactor, pivotX, pivotY) mMatrix.mapRect(mContentScaledRect, mContentRect) this.zoom = newZoom