Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/alignment #76

Merged
merged 12 commits into from
Jan 23, 2019
84 changes: 54 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,22 @@ A container for view hierarchies that can be panned or zoomed.
<com.otaliastudios.zoom.ZoomLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical|horizontal"
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">

<!-- Content here. -->
Expand Down Expand Up @@ -89,24 +94,29 @@ An `ImageView` implementation to control pan and zoom over its Drawable or Bitma
<com.otaliastudios.zoom.ZoomImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical|horizontal"
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: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)

Expand Down Expand Up @@ -142,46 +152,60 @@ 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|
|--------------|-----------|
|`centerInside`|The content is scaled down or up so that it fits completely inside the view bounds.|
|`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`.
markusressel marked this conversation as resolved.
Show resolved Hide resolved

|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
natario1 marked this conversation as resolved.
Show resolved Hide resolved
a `centerCrop` transformation will not work (will act just like a `centerInside`).
markusressel marked this conversation as resolved.
Show resolved Hide resolved

#### 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).**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] so it is only applied when the content is smaller than the container (e.g. zoom < 1)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address all these in a while. But wanted to say that zoom < 1 does not necessarily mean that content is smaller than container. It does only with CENTER_CROP or CENTER_INSIDE transformations and even then, it depends on the axis.

Just saying because I have seen this check (zoom < 1F) in the pivot function, and I don't know if it's correct or not. I'm tempted to say that it's not, and I also wonder if that should be changed according to the alignment? Not sure.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yes that makes sense. I guess I was lucky in my testing.

Yes I think so too. As i mentioned in the comment there I initially wanted to base it on the transformationGravity but as the alignment will be the new thing after this PR it should be based on it instead.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what that function should return? It's not 100% clear to me, I didn't investigate, but I can try to update

Copy link
Collaborator

@markusressel markusressel Jan 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the function decides what pivot point should be used for the zoom animation based on what corner of the screen is currently overscrolled (or will be, with the target zoom applied). I did this because applying the pan fix and the zoom fix at the same time in an animation when overscrolled while zoomed in a bit (specifically overpinching and overscrolling at the same time in a single pinch gesture) didn't result in the expected target position and/or a funky animation. Implementing this was a bit of trial and error tbh and I think this method is only applicable when the content is zoomed in meaning that at most two sides of the content are overscrolled/have visible gray areas. Because of that I added the zoom < 1 condition which just takes the center of the screen as the pivot point when the view has more than two gray areas around it (which doesn't seem to be the correct way of figuring that out). My current guess would be that this should be based on the alignment instead of just using the center when there are more than two gray areas but this whole method (and it's purpose) may require a complete rethinking.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. Looks like it might take some time, I think I will merge this now and we'll fix later.


|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.|
markusressel marked this conversation as resolved.
Show resolved Hide resolved

You can use the or operation to mix the vertical and horizontal flags.
natario1 marked this conversation as resolved.
Show resolved Hide resolved

```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

Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
50 changes: 50 additions & 0 deletions library/src/main/java/com/otaliastudios/zoom/Alignment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.otaliastudios.zoom

object Alignment {
markusressel marked this conversation as resolved.
Show resolved Hide resolved

// 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 = 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 = 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

/**
* 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()
}
}
77 changes: 57 additions & 20 deletions library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,30 @@ 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)
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,
markusressel marked this conversation as resolved.
Show resolved Hide resolved
flag = true
)
annotation class Alignment

/**
* Controls whether the content should be over-scrollable horizontally.
Expand Down Expand Up @@ -189,6 +201,16 @@ interface ZoomApi {
*/
fun setAllowFlingInOverscroll(allow: Boolean)

/**
* Sets the base transformation to be applied to the content.
* See [setTransformation].
*
* @param transformation the transformation type
*/
fun setTransformation(@Transformation transformation: Int) {
setTransformation(transformation, TRANSFORMATION_GRAVITY_AUTO)
markusressel marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Sets the base transformation to be applied to the content.
* Defaults to [TRANSFORMATION_CENTER_INSIDE] with [android.view.Gravity.CENTER],
Expand All @@ -200,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].
natario1 marked this conversation as resolved.
Show resolved Hide resolved
* 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,
natario1 marked this conversation as resolved.
Show resolved Hide resolved
* 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.
Expand Down Expand Up @@ -354,23 +379,35 @@ interface ZoomApi {
const val TRANSFORMATION_NONE = 2

/**
* Constant for [ZoomApi.setSmallerPolicy].
* When smaller than its container, the content will be centered.
* Constant for [ZoomApi.setTransformation] gravity.
* This currently means that the gravity will be inferred from the alignment or
natario1 marked this conversation as resolved.
Show resolved Hide resolved
* fallback to a reasonable default.
*/
const val TRANSFORMATION_GRAVITY_AUTO = 0

/**
* The default [setMinZoom] applied by the engine if none is specified.
*/
const val MIN_ZOOM_DEFAULT = 0.8F

/**
* The default [setMinZoom] type applied by the engine if none is specified.
*/
const val MIN_ZOOM_DEFAULT_TYPE = TYPE_ZOOM

/**
* The default [setMaxZoom] applied by the engine if none is specified.
*/
const val SMALLER_POLICY_CENTER = 0
const val MAX_ZOOM_DEFAULT = 2.5F

/**
* Constant for [ZoomApi.setSmallerPolicy].
* When smaller than its container, the content will be bound to the container
* according to the transformation gravity.
* The default [setMaxZoom] type applied by the engine if none is specified.
*/
const val SMALLER_POLICY_FROM_TRANSFORMATION = 1
const val MAX_ZOOM_DEFAULT_TYPE = TYPE_ZOOM

/**
* 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.
* The default value for [setAlignment].
*/
const val SMALLER_POLICY_NONE = 2
const val ALIGNMENT_DEFAULT = com.otaliastudios.zoom.Alignment.CENTER
}
}
Loading