Skip to content

Commit

Permalink
Base transformations & bump version (natario1#28)
Browse files Browse the repository at this point in the history
* Add transformation and transformation gravity for base transformations.

* Improve docs

* Bump version

* Revert 50 rows
  • Loading branch information
natario1 committed May 13, 2018
1 parent d00f2a2 commit caadad5
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 66 deletions.
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Flexible utilities to control and animate zoom and translation of Views and much
programmatically or through touch events.

```groovy
compile 'com.otaliastudios:zoomlayout:1.1.1'
compile 'com.otaliastudios:zoomlayout:1.2.0'
```

<p>
Expand Down Expand Up @@ -134,19 +134,43 @@ There is no strict limit over what you can do with a `Matrix`,

### Zoom

The engine currently applies, by default, a "center inside" policy when it is initialized.
This means that the content (whatever it is) is scaled down (or up) to fit the parent view bounds,
without cropping.
#### Transformations

This base zoom makes the difference between **zoom** and **realZoom**. Table should be descriptive enough:
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`.
It is applied only once, and defines the starting viewport over our content.

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

If, after applying the transformation (and any minZoom / maxZoom constraint), the content is partially
cropped along some dimension, the engine will also apply a translation according to the given transformation gravity.

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

#### Zoom Types

The base transformation makes the difference between **zoom** and **realZoom**. Since we have silently applied
a base zoom to the content, we must introduce two separate types:

|Zoom type|Value|Description|
|---------|-----|-----------|
|Zoom|`ZoomEngine.TYPE_ZOOM`|The scale value after the initial, center-inside base zoom was applied. `zoom == 1` means that the content fits the screen perfectly.|
|Real zoom|`ZoomEngine.TYPE_REAL_ZOOM`|The actual scale value, including the initial base zoom. `realZoom == 1` means that the 1 inch of the content fits 1 inch of the screen.|
|Zoom|`TYPE_ZOOM`|The scale value after the initial transformation. `zoom == 1` means that the content was untouched after the transformation.|
|Real zoom|`TYPE_REAL_ZOOM`|The actual scale value, including the initial transformation. `realZoom == 1` means that the 1 inch of the content fits 1 inch of the screen.|

To make things clearer, when transformation is `none`, the zoom and the real zoom will be identical.
The distinction is very useful when it comes to imposing min and max constraints to our zoom value.

#### APIs

Some of the zoom APIs will let you pass an integer (either `TYPE_ZOOM` or `TYPE_REAL_ZOOM`)
to define the zoom you are referencing to. Depending on the context, imposing restrictions on one type
to define the zoom type you are referencing to. Depending on the context, imposing restrictions on one type
will make more sense than the other - e. g., in a PDF viewer, you might want to cap real zoom at `1`.

|API|Description|Default value|
Expand Down
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ apply plugin: 'com.jfrog.bintray'
// Required by bintray
// archivesBaseName is required if artifactId is different from gradle module name
// or you can add baseName to each archive task (sources, javadoc, aar)
version = '1.1.1'
version = '1.2.0'
group = 'com.otaliastudios'
archivesBaseName = 'zoomlayout'

Expand Down
40 changes: 37 additions & 3 deletions library/src/main/java/com/otaliastudios/zoom/ZoomApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public interface ZoomApi {
* @see #getZoom()
* @see #getRealZoom()
*/
public static final int TYPE_ZOOM = 0;
int TYPE_ZOOM = 0;

/**
* Flag for zoom constraints and settings.
Expand All @@ -53,11 +53,35 @@ public interface ZoomApi {
* @see #getZoom()
* @see #getRealZoom()
*/
public static final int TYPE_REAL_ZOOM = 1;
int TYPE_REAL_ZOOM = 1;

@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_ZOOM, TYPE_REAL_ZOOM})
public @interface ZoomType {}
@interface ZoomType {}

/**
* Constant for {@link #setTransformation(int, int)}.
* The content will be zoomed so that it fits completely inside the container.
*/
int TRANSFORMATION_CENTER_INSIDE = 0;

/**
* Constant for {@link #setTransformation(int, int)}.
* The content will be zoomed so that its smaller side fits exactly inside the container.
* The larger side will be partially cropped.
*/
int TRANSFORMATION_CENTER_CROP = 1;

/**
* Constant for {@link #setTransformation(int, int)}.
* No transformation will be applied, which means that both {@link #getZoom()} and
* {@link #getRealZoom()} will return the same value.
*/
int TRANSFORMATION_NONE = 2;

@Retention(RetentionPolicy.SOURCE)
@IntDef({TRANSFORMATION_CENTER_INSIDE, TRANSFORMATION_CENTER_CROP, TRANSFORMATION_NONE})
@interface Transformation {}

/**
* Controls whether the content should be over-scrollable horizontally.
Expand Down Expand Up @@ -86,6 +110,16 @@ public interface ZoomApi {
*/
void setOverPinchable(boolean overPinchable);

/**
* Sets the base transformation to be applied to the content.
* Defaults to {@link #TRANSFORMATION_CENTER_INSIDE} with {@link android.view.Gravity#CENTER},
* which means that the content will be zoomed so that it fits completely inside the container.
*
* @param transformation the transformation type
* @param gravity the transformation gravity. Might be ignored for some transformations
*/
void setTransformation(@Transformation int transformation, int gravity);

/**
* A low level API that can animate both zoom and pan at the same time.
* Zoom might not be the actual matrix scale, see {@link #getZoom()} and {@link #getRealZoom()}.
Expand Down
91 changes: 75 additions & 16 deletions library/src/main/java/com/otaliastudios/zoom/ZoomEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.os.Build;
import android.support.annotation.IntDef;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
Expand All @@ -29,8 +30,11 @@
* - Notify the helper of the content size, using {@link #setContentSize(RectF)}
* - Pass touch events to {@link #onInterceptTouchEvent(MotionEvent)} and {@link #onTouchEvent(MotionEvent)}
*
* This class will try to keep the content centered. It also starts with a "center inside" policy
* that will apply a base zoom to the content, so that it fits inside the view container.
* This class will apply a base transformation to the content, see {@link #setTransformation(int, int)},
* so that it is laid out initially as we wish.
*
* When the scaling makes the content smaller than our viewport, the engine will always try
* to keep the content centered.
*/
public final class ZoomEngine implements ViewTreeObserver.OnGlobalLayoutListener, ZoomApi {

Expand Down Expand Up @@ -90,6 +94,8 @@ interface Listener {
private int mMaxZoomMode = TYPE_ZOOM;
@Zoom private float mZoom = 1f; // Not necessarily equal to the matrix scale.
private float mBaseZoom; // mZoom * mBaseZoom matches the matrix scale.
private int mTransformation = TRANSFORMATION_CENTER_INSIDE;
private int mTransformationGravity = Gravity.CENTER;
private boolean mOverScrollHorizontal = true;
private boolean mOverScrollVertical = true;
private boolean mOverPinchable = true;
Expand Down Expand Up @@ -231,6 +237,20 @@ private float getCurrentOverPinch() {

//region Initialize

/**
* Sets the base transformation to be applied to the content.
* Defaults to {@link #TRANSFORMATION_CENTER_INSIDE} with {@link Gravity#CENTER},
* which means that the content will be zoomed so that it fits completely inside the container.
*
* @param transformation the transformation type
* @param gravity the transformation gravity. Might be ignored for some transformations
*/
@Override
public void setTransformation(int transformation, int gravity) {
mTransformation = transformation;
mTransformationGravity = gravity;
}

@Override
public void onGlobalLayout() {
int width = mView.getWidth();
Expand Down Expand Up @@ -296,7 +316,6 @@ private void init(float viewWidth, float viewHeight, RectF rect) {

} else {
// First time. Apply base zoom, dispatch first event and return.
// Auto scale to center-inside.
mBaseZoom = computeBaseZoom();
mMatrix.setScale(mBaseZoom, mBaseZoom);
mMatrix.mapRect(mContentRect, mContentBaseRect);
Expand All @@ -305,10 +324,13 @@ private void init(float viewWidth, float viewHeight, RectF rect) {

@Zoom float newZoom = ensureScaleBounds(mZoom, false);
LOG.i("init:", "fromScratch:", "scaleBounds:", "we need a zoom correction of", (newZoom - mZoom));
if (newZoom != mZoom) {
// Zoom only would zoom in the center of the content. Keep it left.
applyZoomAndAbsolutePan(newZoom, 0, 0, false);
}
if (newZoom != mZoom) applyZoom(newZoom, false);

// pan based on transformation gravity.
@ScaledPan float[] newPan = computeBasePan();
@ScaledPan float deltaX = newPan[0] - getScaledPanX();
@ScaledPan float deltaY = newPan[1] - getScaledPanY();
if (deltaX != 0 || deltaY != 0) applyScaledPan(deltaX, deltaY, false);

ensureCurrentTranslationBounds(false);
dispatchOnMatrix();
Expand All @@ -333,10 +355,47 @@ public void clear() {
}

private float computeBaseZoom() {
float scaleX = mViewWidth / mContentRect.width();
float scaleY = mViewHeight / mContentRect.height();
LOG.v("computeBaseZoom", "scaleX:", scaleX, "scaleY:", scaleY);
return Math.min(scaleX, scaleY);
switch (mTransformation) {
case TRANSFORMATION_CENTER_INSIDE: {
float scaleX = mViewWidth / mContentRect.width();
float scaleY = mViewHeight / mContentRect.height();
LOG.v("computeBaseZoom", "centerInside", "scaleX:", scaleX, "scaleY:", scaleY);
return Math.min(scaleX, scaleY);
}
case TRANSFORMATION_CENTER_CROP: {
float scaleX = mViewWidth / mContentRect.width();
float scaleY = mViewHeight / mContentRect.height();
LOG.v("computeBaseZoom", "centerCrop", "scaleX:", scaleX, "scaleY:", scaleY);
return Math.max(scaleX, scaleY);
}
case TRANSFORMATION_NONE:
default:
return 1f;
}
}

@ScaledPan
private float[] computeBasePan() {
float[] result = new float[]{0f, 0f};
float extraWidth = mContentRect.width() - mViewWidth;
float extraHeight = mContentRect.height() - mViewHeight;
if (extraWidth > 0) {
// Honour the horizontal gravity indication.
switch (mTransformationGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT: result[0] = 0; break;
case Gravity.CENTER_HORIZONTAL: result[0] = -0.5F * extraWidth; break;
case Gravity.RIGHT: result[0] = -extraWidth; break;
}
}
if (extraHeight > 0) {
// Honour the vertical gravity indication.
switch (mTransformationGravity & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP: result[1] = 0; break;
case Gravity.CENTER_VERTICAL: result[1] = -0.5F * extraHeight; break;
case Gravity.BOTTOM: result[1] = -extraHeight; break;
}
}
return result;
}

//endregion
Expand Down Expand Up @@ -772,7 +831,7 @@ public void setMinZoom(float minZoom, @ZoomType int type) {
* {@link #zoomTo(float, boolean)} or {@link #zoomBy(float, boolean)}.
*
* This can be different than the actual scale you get in the matrix, because at startup
* we apply a base zoom to respect the "center inside" policy.
* we apply a base transformation, see {@link #setTransformation(int, int)}.
* All zoom calls, including min zoom and max zoom, refer to this axis, where zoom is set to 1
* right after the initial transformation.
*
Expand All @@ -786,10 +845,10 @@ public float getZoom() {
}

/**
* Gets the current zoom value, including the base zoom that was eventually applied when
* initializing to respect the "center inside" policy. This will match the scaleX - scaleY
* values you get into the {@link Matrix}, and is the actual scale value of the content
* from its original size.
* Gets the current zoom value, including the base zoom that was eventually applied during
* the starting transformation, see {@link #setTransformation(int, int)}.
* This value will match the scaleX - scaleY values you get into the {@link Matrix},
* and is the actual scale value of the content from its original size.
*
* @return the real zoom
*/
Expand Down
45 changes: 26 additions & 19 deletions library/src/main/java/com/otaliastudios/zoom/ZoomImageView.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.ImageView;

Expand Down Expand Up @@ -40,31 +41,24 @@ public ZoomImageView(@NonNull Context context, @Nullable AttributeSet attrs, @At
super(context, attrs, defStyleAttr);

TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ZoomEngine, defStyleAttr, 0);
// Support deprecated overScrollable
boolean overScrollHorizontal, overScrollVertical;
if (a.hasValue(R.styleable.ZoomEngine_overScrollable)) {
overScrollHorizontal = a.getBoolean(R.styleable.ZoomEngine_overScrollable, true);
overScrollVertical = a.getBoolean(R.styleable.ZoomEngine_overScrollable, true);
} else {
overScrollHorizontal = a.getBoolean(R.styleable.ZoomEngine_overScrollHorizontal, true);
overScrollVertical = a.getBoolean(R.styleable.ZoomEngine_overScrollVertical, true);
}
boolean overScrollHorizontal = a.getBoolean(R.styleable.ZoomEngine_overScrollHorizontal, true);
boolean overScrollVertical = a.getBoolean(R.styleable.ZoomEngine_overScrollVertical, true);
boolean overPinchable = a.getBoolean(R.styleable.ZoomEngine_overPinchable, true);
float minZoom = a.getFloat(R.styleable.ZoomEngine_minZoom, -1);
float maxZoom = a.getFloat(R.styleable.ZoomEngine_maxZoom, -1);
@ZoomEngine.ZoomType int minZoomMode = a.getInteger(
R.styleable.ZoomEngine_minZoomType, ZoomEngine.TYPE_ZOOM);
@ZoomEngine.ZoomType int maxZoomMode = a.getInteger(
R.styleable.ZoomEngine_maxZoomType, ZoomEngine.TYPE_ZOOM);

@ZoomType int minZoomMode = a.getInteger(R.styleable.ZoomEngine_minZoomType, TYPE_ZOOM);
@ZoomType int maxZoomMode = a.getInteger(R.styleable.ZoomEngine_maxZoomType, TYPE_ZOOM);
int transformation = a.getInteger(R.styleable.ZoomEngine_transformation, TRANSFORMATION_CENTER_INSIDE);
int transformationGravity = a.getInt(R.styleable.ZoomEngine_transformationGravity, Gravity.CENTER);
a.recycle();

mEngine = new ZoomEngine(context, this, this);
mEngine.setOverScrollHorizontal(overScrollHorizontal);
mEngine.setOverScrollVertical(overScrollVertical);
mEngine.setOverPinchable(overPinchable);
if (minZoom > -1) mEngine.setMinZoom(minZoom, minZoomMode);
if (maxZoom > -1) mEngine.setMaxZoom(maxZoom, maxZoomMode);
setTransformation(transformation, transformationGravity);
setOverScrollHorizontal(overScrollHorizontal);
setOverScrollVertical(overScrollVertical);
setOverPinchable(overPinchable);
if (minZoom > -1) setMinZoom(minZoom, minZoomMode);
if (maxZoom > -1) setMaxZoom(maxZoom, maxZoomMode);

setImageMatrix(mMatrix);
setScaleType(ScaleType.MATRIX);
Expand Down Expand Up @@ -155,6 +149,19 @@ public void setOverPinchable(boolean overPinchable) {
getEngine().setOverPinchable(overPinchable);
}

/**
* Sets the base transformation to be applied to the content.
* Defaults to {@link #TRANSFORMATION_CENTER_INSIDE} with {@link Gravity#CENTER},
* which means that the content will be zoomed so that it fits completely inside the container.
*
* @param transformation the transformation type
* @param gravity the transformation gravity. Might be ignored for some transformations
*/
@Override
public void setTransformation(int transformation, int gravity) {
getEngine().setTransformation(transformation, gravity);
}

/**
* A low level API that can animate both zoom and pan at the same time.
* Zoom might not be the actual matrix scale, see {@link #getZoom()} and {@link #getRealZoom()}.
Expand Down
Loading

0 comments on commit caadad5

Please sign in to comment.