Skip to content

Commit

Permalink
Feature/alignment (#76)
Browse files Browse the repository at this point in the history
* Versions

* Add TRANSFORMATION_GRAVITY_AUTO

* Create Alignment class

* Create Alignment attrs and update Views

* Implement Alignment in checkPanBounds

* Update docs

* Remove checks to be consistent with docs

* Rename methods and fields

* Fix fling with Alignment.NONE

* Address review, move gravity logic to Alignment

* Improve #79
  • Loading branch information
natario1 committed Jan 23, 2019
1 parent 5f41755 commit fc08082
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 182 deletions.
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.view.Gravity` flags like `Gravity.TOP`, 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`, 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`.

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

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
128 changes: 128 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,128 @@
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.
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

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

/**
* 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()
}

/**
* 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
}
}
}

0 comments on commit fc08082

Please sign in to comment.