jenkins-bot has submitted this change and it was merged.

Change subject: Bring back pinch-to-zoom in gallery.
......................................................................


Bring back pinch-to-zoom in gallery.

So, it looks like the Fresco repo contains a "samples" directory that
contains a "ZoomableDraweeView" that precisely suits our needs.
https://github.com/facebook/fresco/tree/master/samples/zoomable

Unfortunately, this is not yet part of the packaged distribution of
Fresco, so we need to package it ourselves for now, and keep an eye out
for updates.

Bug: T126164
Change-Id: I764a81e904f889318744a985fa197d4c1373c971
---
A 
app/src/main/java/com/facebook/samples/gestures/MultiPointerGestureDetector.java
A app/src/main/java/com/facebook/samples/gestures/TransformGestureDetector.java
A app/src/main/java/com/facebook/samples/zoomable/DefaultZoomableController.java
A app/src/main/java/com/facebook/samples/zoomable/ZoomableController.java
A app/src/main/java/com/facebook/samples/zoomable/ZoomableDraweeView.java
M app/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
M app/src/main/res/layout/fragment_gallery_item.xml
7 files changed, 1,250 insertions(+), 10 deletions(-)

Approvals:
  Sniedzielski: Looks good to me, approved
  jenkins-bot: Verified

Objections:
  Niedzielski: There's a problem with this change, please improve



diff --git 
a/app/src/main/java/com/facebook/samples/gestures/MultiPointerGestureDetector.java
 
b/app/src/main/java/com/facebook/samples/gestures/MultiPointerGestureDetector.java
new file mode 100644
index 0000000..5ed4874
--- /dev/null
+++ 
b/app/src/main/java/com/facebook/samples/gestures/MultiPointerGestureDetector.java
@@ -0,0 +1,240 @@
+/*
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only.  Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.facebook.samples.gestures;
+
+import android.view.MotionEvent;
+
+/**
+ * Component that detects and tracks multiple pointers based on touch events.
+ * <p>
+ * Each time a pointer gets pressed or released, the current gesture (if any) 
will end, and a new
+ * one will be started (if there are still pressed pointers left). It is 
guaranteed that the number
+ * of pointers within the single gesture will remain the same during the whole 
gesture.
+ */
+public class MultiPointerGestureDetector {
+
+  /** The listener for receiving notifications when gestures occur. */
+  public interface Listener {
+    /** Responds to the beginning of a gesture. */
+    void onGestureBegin(MultiPointerGestureDetector detector);
+
+    /** Responds to the update of a gesture in progress. */
+    void onGestureUpdate(MultiPointerGestureDetector detector);
+
+    /** Responds to the end of a gesture. */
+    void onGestureEnd(MultiPointerGestureDetector detector);
+  }
+
+  private static final int MAX_POINTERS = 2;
+
+  private boolean mGestureInProgress;
+  private int mCount;
+  private final int[] mId = new int[MAX_POINTERS];
+  private final float[] mStartX = new float[MAX_POINTERS];
+  private final float[] mStartY = new float[MAX_POINTERS];
+  private final float[] mCurrentX = new float[MAX_POINTERS];
+  private final float[] mCurrentY = new float[MAX_POINTERS];
+
+  private Listener mListener = null;
+
+  public MultiPointerGestureDetector() {
+    reset();
+  }
+
+  /** Factory method that creates a new instance of 
MultiPointerGestureDetector */
+  public static MultiPointerGestureDetector newInstance() {
+    return new MultiPointerGestureDetector();
+  }
+
+  /**
+   * Sets the listener.
+   * @param listener listener to set
+   */
+  public void setListener(Listener listener) {
+    mListener = listener;
+  }
+
+  /**
+   * Resets the component to the initial state.
+   */
+  public void reset() {
+    mGestureInProgress = false;
+    mCount = 0;
+    for (int i = 0; i < MAX_POINTERS; i++) {
+      mId[i] = MotionEvent.INVALID_POINTER_ID;
+    }
+  }
+
+  /**
+   * This method can be overridden in order to perform threshold check or 
something similar.
+   * @return whether or not to start a new gesture
+   */
+  protected boolean shouldStartGesture() {
+    return true;
+  }
+
+  private void startGesture() {
+    if (!mGestureInProgress) {
+      mGestureInProgress = true;
+      if (mListener != null) {
+        mListener.onGestureBegin(this);
+      }
+    }
+  }
+
+  private void stopGesture() {
+    if (mGestureInProgress) {
+      mGestureInProgress = false;
+      if (mListener != null) {
+        mListener.onGestureEnd(this);
+      }
+    }
+  }
+
+  /**
+   * Gets the index of the i-th pressed pointer.
+   * Normally, the index will be equal to i, except in the case when the 
pointer is released.
+   * @return index of the specified pointer or -1 if not found (i.e. not 
enough pointers are down)
+   */
+  private int getPressedPointerIndex(MotionEvent event, int i) {
+    final int count = event.getPointerCount();
+    final int action = event.getActionMasked();
+    final int index = event.getActionIndex();
+    if (action == MotionEvent.ACTION_UP || action == 
MotionEvent.ACTION_POINTER_UP) {
+      if (i >= index) {
+        i++;
+      }
+    }
+    return (i < count) ? i : -1;
+  }
+
+  /**
+   * Handles the given motion event.
+   * @param event event to handle
+   * @return whether or not the event was handled
+   */
+  public boolean onTouchEvent(final MotionEvent event) {
+    switch (event.getActionMasked()) {
+      case MotionEvent.ACTION_MOVE:
+        // update pointers
+        for (int i = 0; i < MAX_POINTERS; i++) {
+          int index = event.findPointerIndex(mId[i]);
+          if (index != -1) {
+            mCurrentX[i] = event.getX(index);
+            mCurrentY[i] = event.getY(index);
+          }
+        }
+        // start a new gesture if not already started
+        if (!mGestureInProgress && shouldStartGesture()) {
+          startGesture();
+        }
+        // notify listener
+        if (mGestureInProgress && mListener != null) {
+          mListener.onGestureUpdate(this);
+        }
+        break;
+
+      case MotionEvent.ACTION_DOWN:
+      case MotionEvent.ACTION_POINTER_DOWN:
+      case MotionEvent.ACTION_POINTER_UP:
+      case MotionEvent.ACTION_UP:
+        // we'll restart the current gesture (if any) whenever the number of 
pointers changes
+        // NOTE: we only restart existing gestures here, new gestures are 
started in ACTION_MOVE
+        boolean wasGestureInProgress = mGestureInProgress;
+        stopGesture();
+        reset();
+        // update pointers
+        for (int i = 0; i < MAX_POINTERS; i++) {
+          int index = getPressedPointerIndex(event, i);
+          if (index == -1) {
+            break;
+          }
+          mId[i] = event.getPointerId(index);
+          mStartX[i] = event.getX(index);
+          mCurrentX[i] = mStartX[i];
+          mStartY[i] = event.getY(index);
+          mCurrentY[i] = mStartY[i];
+          mCount++;
+        }
+        // restart the gesture (if any) if there are still pointers left
+        if (wasGestureInProgress && mCount > 0) {
+          startGesture();
+        }
+        break;
+
+      case MotionEvent.ACTION_CANCEL:
+        stopGesture();
+        reset();
+        break;
+
+      default:
+        break;
+    }
+    return true;
+  }
+
+  /** Restarts the current gesture */
+  public void restartGesture() {
+    if (!mGestureInProgress) {
+      return;
+    }
+    stopGesture();
+    for (int i = 0; i < MAX_POINTERS; i++) {
+      mStartX[i] = mCurrentX[i];
+      mStartY[i] = mCurrentY[i];
+    }
+    startGesture();
+  }
+
+  /** Gets whether gesture is in progress or not */
+  public boolean isGestureInProgress() {
+    return mGestureInProgress;
+  }
+
+  /** Gets the number of pointers in the current gesture */
+  public int getCount() {
+    return mCount;
+  }
+
+  /**
+   * Gets the start X coordinates for the all pointers
+   * Mutable array is exposed for performance reasons and is not to be 
modified by the callers.
+   */
+  public float[] getStartX() {
+    return mStartX;
+  }
+
+  /**
+   * Gets the start Y coordinates for the all pointers
+   * Mutable array is exposed for performance reasons and is not to be 
modified by the callers.
+   */
+  public float[] getStartY() {
+    return mStartY;
+  }
+
+  /**
+   * Gets the current X coordinates for the all pointers
+   * Mutable array is exposed for performance reasons and is not to be 
modified by the callers.
+   */
+  public float[] getCurrentX() {
+    return mCurrentX;
+  }
+
+  /**
+   * Gets the current Y coordinates for the all pointers
+   * Mutable array is exposed for performance reasons and is not to be 
modified by the callers.
+   */
+  public float[] getCurrentY() {
+    return mCurrentY;
+  }
+}
diff --git 
a/app/src/main/java/com/facebook/samples/gestures/TransformGestureDetector.java 
b/app/src/main/java/com/facebook/samples/gestures/TransformGestureDetector.java
new file mode 100644
index 0000000..d5baf79
--- /dev/null
+++ 
b/app/src/main/java/com/facebook/samples/gestures/TransformGestureDetector.java
@@ -0,0 +1,166 @@
+/*
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only.  Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.facebook.samples.gestures;
+
+import android.view.MotionEvent;
+
+/**
+ * Component that detects translation, scale and rotation based on touch 
events.
+ * <p>
+ * This class notifies its listeners whenever a gesture begins, updates or 
ends.
+ * The instance of this detector is passed to the listeners, so it can be 
queried
+ * for pivot, translation, scale or rotation.
+ */
+public class TransformGestureDetector implements 
MultiPointerGestureDetector.Listener {
+
+  /** The listener for receiving notifications when gestures occur. */
+  public interface Listener {
+    /** Responds to the beginning of a gesture. */
+    void onGestureBegin(TransformGestureDetector detector);
+
+    /** Responds to the update of a gesture in progress. */
+    void onGestureUpdate(TransformGestureDetector detector);
+
+    /** Responds to the end of a gesture. */
+    void onGestureEnd(TransformGestureDetector detector);
+  }
+
+  private final MultiPointerGestureDetector mDetector;
+
+  private Listener mListener = null;
+
+  public TransformGestureDetector(MultiPointerGestureDetector 
multiPointerGestureDetector) {
+    mDetector = multiPointerGestureDetector;
+    mDetector.setListener(this);
+  }
+
+  /** Factory method that creates a new instance of TransformGestureDetector */
+  public static TransformGestureDetector newInstance() {
+    return new 
TransformGestureDetector(MultiPointerGestureDetector.newInstance());
+  }
+
+  /**
+   * Sets the listener.
+   * @param listener listener to set
+   */
+  public void setListener(Listener listener) {
+    mListener = listener;
+  }
+
+  /**
+   * Resets the component to the initial state.
+   */
+  public void reset() {
+    mDetector.reset();
+  }
+
+  /**
+   * Handles the given motion event.
+   * @param event event to handle
+   * @return whether or not the event was handled
+   */
+  public boolean onTouchEvent(final MotionEvent event) {
+    return mDetector.onTouchEvent(event);
+  }
+
+  @Override
+  public void onGestureBegin(MultiPointerGestureDetector detector) {
+    if (mListener != null) {
+      mListener.onGestureBegin(this);
+    }
+  }
+
+  @Override
+  public void onGestureUpdate(MultiPointerGestureDetector detector) {
+    if (mListener != null) {
+      mListener.onGestureUpdate(this);
+    }
+  }
+
+  @Override
+  public void onGestureEnd(MultiPointerGestureDetector detector) {
+    if (mListener != null) {
+      mListener.onGestureEnd(this);
+    }
+  }
+
+  private float calcAverage(float[] arr, int len) {
+    float sum = 0;
+    for (int i = 0; i < len; i++) {
+      sum += arr[i];
+    }
+    return (len > 0) ? sum / len : 0;
+  }
+
+  /** Restarts the current gesture */
+  public void restartGesture() {
+    mDetector.restartGesture();
+  }
+
+  /** Gets whether gesture is in progress or not */
+  public boolean isGestureInProgress() {
+    return mDetector.isGestureInProgress();
+  }
+
+  /** Gets the X coordinate of the pivot point */
+  public float getPivotX() {
+    return calcAverage(mDetector.getStartX(), mDetector.getCount());
+  }
+
+  /** Gets the Y coordinate of the pivot point */
+  public float getPivotY() {
+    return calcAverage(mDetector.getStartY(), mDetector.getCount());
+  }
+
+  /** Gets the X component of the translation */
+  public float getTranslationX() {
+    return calcAverage(mDetector.getCurrentX(), mDetector.getCount())
+            - calcAverage(mDetector.getStartX(), mDetector.getCount());
+  }
+
+  /** Gets the Y component of the translation */
+  public float getTranslationY() {
+    return calcAverage(mDetector.getCurrentY(), mDetector.getCount())
+            - calcAverage(mDetector.getStartY(), mDetector.getCount());
+  }
+
+  /** Gets the scale */
+  public float getScale() {
+    if (mDetector.getCount() < 2) {
+      return 1;
+    } else {
+      float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0];
+      float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0];
+      float currentDeltaX = mDetector.getCurrentX()[1] - 
mDetector.getCurrentX()[0];
+      float currentDeltaY = mDetector.getCurrentY()[1] - 
mDetector.getCurrentY()[0];
+      float startDist = (float) Math.hypot(startDeltaX, startDeltaY);
+      float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY);
+      return currentDist / startDist;
+    }
+  }
+
+  /** Gets the rotation in radians */
+  public float getRotation() {
+    if (mDetector.getCount() < 2) {
+      return 0;
+    } else {
+      float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0];
+      float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0];
+      float currentDeltaX = mDetector.getCurrentX()[1] - 
mDetector.getCurrentX()[0];
+      float currentDeltaY = mDetector.getCurrentY()[1] - 
mDetector.getCurrentY()[0];
+      float startAngle = (float) Math.atan2(startDeltaY, startDeltaX);
+      float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX);
+      return currentAngle - startAngle;
+    }
+  }
+}
diff --git 
a/app/src/main/java/com/facebook/samples/zoomable/DefaultZoomableController.java
 
b/app/src/main/java/com/facebook/samples/zoomable/DefaultZoomableController.java
new file mode 100644
index 0000000..877e56a
--- /dev/null
+++ 
b/app/src/main/java/com/facebook/samples/zoomable/DefaultZoomableController.java
@@ -0,0 +1,487 @@
+/*
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only.  Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.facebook.samples.zoomable;
+
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import com.facebook.common.internal.Preconditions;
+import com.facebook.samples.gestures.TransformGestureDetector;
+
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.AnimatorListenerAdapter;
+import com.nineoldandroids.animation.ValueAnimator;
+
+/**
+ * Zoomable controller that calculates transformation based on touch events.
+ */
+public class DefaultZoomableController
+    implements ZoomableController, TransformGestureDetector.Listener {
+
+  private static final int MATRIX_SIZE = 9;
+  private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1);
+
+  private TransformGestureDetector mGestureDetector;
+
+  private Listener mListener = null;
+
+  private boolean mIsEnabled = false;
+  private boolean mIsRotationEnabled = false;
+  private boolean mIsScaleEnabled = true;
+  private boolean mIsTranslationEnabled = true;
+
+  private float mMinScaleFactor = 1.0f;
+  private float mMaxScaleFactor = Float.POSITIVE_INFINITY;
+
+  private final RectF mViewBounds = new RectF();
+  private final RectF mImageBounds = new RectF();
+  private final RectF mTransformedImageBounds = new RectF();
+  private final Matrix mPreviousTransform = new Matrix();
+  private final Matrix mActiveTransform = new Matrix();
+  private final Matrix mActiveTransformInverse = new Matrix();
+  private final float[] mTempValues = new float[MATRIX_SIZE];
+  private final RectF mTempRect = new RectF();
+
+  private final ValueAnimator mValueAnimator;
+  private final float[] mAnimationStartValues = new float[MATRIX_SIZE];
+  private final float[] mAnimationDestValues = new float[MATRIX_SIZE];
+  private final float[] mAnimationCurrValues = new float[MATRIX_SIZE];
+  private final Matrix mNewTransform = new Matrix();
+
+  public DefaultZoomableController(TransformGestureDetector gestureDetector) {
+    mGestureDetector = gestureDetector;
+    mGestureDetector.setListener(this);
+    mValueAnimator = ValueAnimator.ofFloat(0, 1);
+    mValueAnimator.setInterpolator(new DecelerateInterpolator());
+  }
+
+  public static DefaultZoomableController newInstance() {
+    return new 
DefaultZoomableController(TransformGestureDetector.newInstance());
+  }
+
+  @Override
+  public void setListener(Listener listener) {
+    mListener = listener;
+  }
+
+  /** Rests the controller. */
+  public void reset() {
+    mGestureDetector.reset();
+    mPreviousTransform.reset();
+    mActiveTransform.reset();
+    onTransformChanged();
+  }
+
+  /** Sets whether the controller is enabled or not. */
+  @Override
+  public void setEnabled(boolean enabled) {
+    mIsEnabled = enabled;
+    if (!enabled) {
+      reset();
+    }
+  }
+
+  /** Returns whether the controller is enabled or not. */
+  @Override
+  public boolean isEnabled() {
+    return mIsEnabled;
+  }
+
+  /** Sets whether the rotation gesture is enabled or not. */
+  public void setRotationEnabled(boolean enabled) {
+    mIsRotationEnabled = enabled;
+  }
+
+  /** Gets whether the rotation gesture is enabled or not. */
+  public boolean isRotationEnabled() {
+    return  mIsRotationEnabled;
+  }
+
+  /** Sets whether the scale gesture is enabled or not. */
+  public void setScaleEnabled(boolean enabled) {
+    mIsScaleEnabled = enabled;
+  }
+
+  /** Gets whether the scale gesture is enabled or not. */
+  public boolean isScaleEnabled() {
+    return  mIsScaleEnabled;
+  }
+
+  /** Sets whether the translation gesture is enabled or not. */
+  public void setTranslationEnabled(boolean enabled) {
+    mIsTranslationEnabled = enabled;
+  }
+
+  /** Gets whether the translations gesture is enabled or not. */
+  public boolean isTranslationEnabled() {
+    return  mIsTranslationEnabled;
+  }
+
+  /** Gets the image bounds before zoomable transformation is applied. */
+  public RectF getImageBounds() {
+    return mImageBounds;
+  }
+
+  protected RectF getTransformedImageBounds() {
+    return mTransformedImageBounds;
+  }
+
+  /** Sets the image bounds before zoomable transformation is applied. */
+  @Override
+  public void setImageBounds(RectF imageBounds) {
+    if (!imageBounds.equals(mImageBounds)) {
+      mImageBounds.set(imageBounds);
+      onTransformChanged();
+    }
+  }
+
+  /** Gets the view bounds. */
+  public RectF getViewBounds() {
+    return mViewBounds;
+  }
+
+  /** Sets the view bounds. */
+  @Override
+  public void setViewBounds(RectF viewBounds) {
+    mViewBounds.set(viewBounds);
+  }
+
+  /** Gets the minimum scale factor allowed. */
+  public float getMinScaleFactor() {
+    return mMinScaleFactor;
+  }
+
+  /**
+   * Sets the minimum scale factor allowed.
+   * <p>
+   * Note that the hierarchy performs scaling as well, which
+   * is not accounted here, so the actual scale factor may differ.
+   */
+  public void setMinScaleFactor(float minScaleFactor) {
+    mMinScaleFactor = minScaleFactor;
+  }
+
+  /** Gets the maximum scale factor allowed. */
+  public float getMaxScaleFactor() {
+    return mMaxScaleFactor;
+  }
+
+  /**
+   * Sets the maximum scale factor allowed.
+   * <p>
+   * Note that the hierarchy performs scaling as well, which
+   * is not accounted here, so the actual scale factor may differ.
+   */
+  public void setMaxScaleFactor(float maxScaleFactor) {
+    mMaxScaleFactor = maxScaleFactor;
+  }
+
+  /**
+   * Maps point from the view's to the image's relative coordinate system.
+   * This takes into account the zoomable transformation.
+   */
+  public PointF mapViewToImage(PointF viewPoint) {
+    float[] points = mTempValues;
+    points[0] = viewPoint.x;
+    points[1] = viewPoint.y;
+    mActiveTransform.invert(mActiveTransformInverse);
+    mActiveTransformInverse.mapPoints(points, 0, points, 0, 1);
+    mapAbsoluteToRelative(points, points, 1);
+    return new PointF(points[0], points[1]);
+  }
+
+  /**
+   * Maps point from the image's relative to the view's coordinate system.
+   * This takes into account the zoomable transformation.
+   */
+  public PointF mapImageToView(PointF imagePoint) {
+    float[] points = mTempValues;
+    points[0] = imagePoint.x;
+    points[1] = imagePoint.y;
+    mapRelativeToAbsolute(points, points, 1);
+    mActiveTransform.mapPoints(points, 0, points, 0, 1);
+    return new PointF(points[0], points[1]);
+  }
+
+  /**
+   * Maps array of 2D points from absolute to the image's relative coordinate 
system,
+   * and writes the transformed points back into the array.
+   * Points are represented by float array of [x0, y0, x1, y1, ...].
+   *
+   * @param destPoints destination array (may be the same as source array)
+   * @param srcPoints source array
+   * @param numPoints number of points to map
+   */
+  private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, 
int numPoints) {
+    for (int i = 0; i < numPoints; i++) {
+      destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / 
mImageBounds.width();
+      destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top)  / 
mImageBounds.height();
+    }
+  }
+
+  /**
+   * Maps array of 2D points from relative to the image's absolute coordinate 
system,
+   * and writes the transformed points back into the array
+   * Points are represented by float array of [x0, y0, x1, y1, ...].
+   *
+   * @param destPoints destination array (may be the same as source array)
+   * @param srcPoints source array
+   * @param numPoints number of points to map
+   */
+  private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, 
int numPoints) {
+    for (int i = 0; i < numPoints; i++) {
+      destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + 
mImageBounds.left;
+      destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + 
mImageBounds.top;
+    }
+  }
+
+  /**
+   * Gets the zoomable transformation
+   * Internal matrix is exposed for performance reasons and is not to be 
modified by the callers.
+   */
+  @Override
+  public Matrix getTransform() {
+    return mActiveTransform;
+  }
+
+  /**
+   * Returns the matrix that fully transforms the image from image-relative 
coordinates
+   * to scaled view-absolute coordinates.
+   */
+  public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) {
+    mActiveTransform.mapRect(mTempRect, mImageBounds);
+    outMatrix.setRectToRect(IDENTITY_RECT, mTempRect, Matrix.ScaleToFit.FILL);
+  }
+
+  // TODO(balazsbalazs) resolve issues with interrupting an existing 
animation/gesture with
+  // a new animation or transform
+
+  /**
+   * Sets a new zoom transformation.
+   *
+   * <p>If this method is called while an animation or gesture is already in 
progress,
+   * this will currently result in undefined behavior.
+   */
+  public void setTransform(Matrix newTransform) {
+    setTransform(newTransform, 0, null);
+  }
+
+  /**
+   * Sets a new zoomable transformation and animates to it if desired.
+   *
+   * <p>If this method is called while an animation or gesture is already in 
progress,
+   * this will currently result in undefined behavior.
+   *
+   * @param newTransform new transform to make active
+   * @param durationMs duration of the animation, or 0 to not animate
+   * @param onAnimationComplete code to run when the animation completes. 
Ignored if durationMs=0
+   */
+  public void setTransform(
+      Matrix newTransform,
+      long durationMs,
+      @Nullable Runnable onAnimationComplete) {
+    if (mGestureDetector.isGestureInProgress()) {
+      mGestureDetector.reset();
+    }
+    cancelAnimation();
+    if (durationMs <= 0) {
+      mActiveTransform.set(newTransform);
+      onTransformChanged();
+    } else {
+      setTransformAnimated(newTransform, durationMs, onAnimationComplete);
+    }
+  }
+
+  /** Do not call this method directly; call it only from setTransform. */
+  private void setTransformAnimated(
+      final Matrix newTransform,
+      long durationMs,
+      @Nullable final Runnable onAnimationComplete) {
+    Preconditions.checkArgument(durationMs > 0);
+    Preconditions.checkState(!mValueAnimator.isRunning());
+    mValueAnimator.setDuration(durationMs);
+    mActiveTransform.getValues(mAnimationStartValues);
+    newTransform.getValues(mAnimationDestValues);
+    mValueAnimator.addUpdateListener(new 
ValueAnimator.AnimatorUpdateListener() {
+      @Override
+      public void onAnimationUpdate(ValueAnimator valueAnimator) {
+        float fraction = (float) valueAnimator.getAnimatedValue();
+        for (int i = 0; i < mAnimationCurrValues.length; i++) {
+          mAnimationCurrValues[i] = (1 - fraction) * mAnimationStartValues[i]
+                  + fraction * mAnimationDestValues[i];
+        }
+        mActiveTransform.setValues(mAnimationCurrValues);
+        onTransformChanged();
+      }
+    });
+    if (onAnimationComplete != null) {
+      mValueAnimator.addListener(new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+          onAnimationComplete.run();
+        }
+      });
+    }
+    mValueAnimator.start();
+  }
+
+  private void cancelAnimation() {
+    mValueAnimator.removeAllUpdateListeners();
+    mValueAnimator.removeAllListeners();
+    if (mValueAnimator.isRunning()) {
+      mValueAnimator.cancel();
+    }
+  }
+
+  /** Notifies controller of the received touch event.  */
+  @Override
+  public boolean onTouchEvent(MotionEvent event) {
+    if (mIsEnabled) {
+      return mGestureDetector.onTouchEvent(event);
+    }
+    return false;
+  }
+
+  protected void onTransformChanged() {
+    mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds);
+    if (mListener != null && isEnabled()) {
+      mListener.onTransformChanged(mActiveTransform);
+    }
+  }
+
+  /**
+   * Zooms to the desired scale and positions the view so that imagePoint is 
in the center.
+   *
+   * <p>If this method is called while an animation or gesture is already in 
progress,
+   * this will currently result in undefined behavior.
+   *
+   * @param scale desired scale, will be limited to {min, max} scale factor
+   * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 
<= x, y <= 1)
+   * @param viewPoint 2D point in view's absolute coordinate system
+   * @param limitTransX  Whether to adjust the transform to prevent black bars 
from appearing on
+   *                     the left or right.
+   * @param limitTransY Whether to adjust the transform to prevent black bars 
from appearing on
+   *                    the top or bottom.
+   * @param durationMs length of animation of the zoom, or 0 if no animation 
desired
+   * @param onAnimationComplete code to execute when the animation is complete.
+   *                            Ignored if durationMs=0
+   */
+  public void zoomToImagePoint(
+      float scale,
+      PointF imagePoint,
+      PointF viewPoint,
+      boolean limitTransX,
+      boolean limitTransY,
+      long durationMs,
+      @Nullable Runnable onAnimationComplete) {
+    scale = limit(scale, mMinScaleFactor, mMaxScaleFactor);
+    float[] viewAbsolute = mTempValues;
+    viewAbsolute[0] = imagePoint.x;
+    viewAbsolute[1] = imagePoint.y;
+    mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1);
+    float distanceX = viewPoint.x - viewAbsolute[0];
+    float distanceY = viewPoint.y - viewAbsolute[1];
+    mNewTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]);
+    mNewTransform.postTranslate(distanceX, distanceY);
+    limitTranslation(mNewTransform, limitTransX, limitTransY);
+
+    setTransform(mNewTransform, durationMs, onAnimationComplete);
+  }
+
+  /* TransformGestureDetector.Listener methods  */
+
+  @Override
+  public void onGestureBegin(TransformGestureDetector detector) {
+    mPreviousTransform.set(mActiveTransform);
+    // TODO(balazsbalazs): animation should be cancelled here
+  }
+
+  @Override
+  public void onGestureUpdate(TransformGestureDetector detector) {
+    mActiveTransform.set(mPreviousTransform);
+    if (mIsRotationEnabled) {
+      final int oneEighty = 180;
+      float angle = detector.getRotation() * (float) (oneEighty / Math.PI);
+      mActiveTransform.postRotate(angle, detector.getPivotX(), 
detector.getPivotY());
+    }
+    if (mIsScaleEnabled) {
+      float scale = detector.getScale();
+      mActiveTransform.postScale(scale, scale, detector.getPivotX(), 
detector.getPivotY());
+    }
+    limitScale(detector.getPivotX(), detector.getPivotY());
+    if (mIsTranslationEnabled) {
+      mActiveTransform.postTranslate(detector.getTranslationX(), 
detector.getTranslationY());
+    }
+    if (limitTranslation(mActiveTransform, true, true)) {
+      mGestureDetector.restartGesture();
+    }
+    onTransformChanged();
+  }
+
+  @Override
+  public void onGestureEnd(TransformGestureDetector detector) {
+    mPreviousTransform.set(mActiveTransform);
+  }
+
+  /** Gets the current scale factor. */
+  @Override
+  public float getScaleFactor() {
+    mActiveTransform.getValues(mTempValues);
+    return mTempValues[Matrix.MSCALE_X];
+  }
+
+  private void limitScale(float pivotX, float pivotY) {
+    float currentScale = getScaleFactor();
+    float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor);
+    if (targetScale != currentScale) {
+      float scale = targetScale / currentScale;
+      mActiveTransform.postScale(scale, scale, pivotX, pivotY);
+    }
+  }
+
+  /**
+   * Keeps the view inside the image if possible, if not (i.e. image is 
smaller than view)
+   * centers the image.
+   * @param limitX whether to apply the limit on the x-axis
+   * @param limitY whether to apply the limit on the y-axis
+   * @return whether adjustments were needed or not
+   */
+  private boolean limitTranslation(Matrix newTransform, boolean limitX, 
boolean limitY) {
+    RectF bounds = mTransformedImageBounds;
+    bounds.set(mImageBounds);
+    newTransform.mapRect(bounds);
+    float offsetLeft = limitX
+            ? getOffset(bounds.left, bounds.width(), mViewBounds.width()) : 
bounds.left;
+    float offsetTop = limitY
+            ? getOffset(bounds.top, bounds.height(), mViewBounds.height()) : 
bounds.top;
+    if (offsetLeft != bounds.left || offsetTop != bounds.top) {
+      newTransform.postTranslate(offsetLeft - bounds.left, offsetTop - 
bounds.top);
+      return true;
+    }
+    return false;
+  }
+
+  private float getOffset(float offset, float imageDimension, float 
viewDimension) {
+    float diff = viewDimension - imageDimension;
+    return (diff > 0) ? diff / 2 : limit(offset, diff, 0);
+  }
+
+  private float limit(float value, float min, float max) {
+    return Math.min(Math.max(min, value), max);
+  }
+
+}
diff --git 
a/app/src/main/java/com/facebook/samples/zoomable/ZoomableController.java 
b/app/src/main/java/com/facebook/samples/zoomable/ZoomableController.java
new file mode 100644
index 0000000..23d6cd5
--- /dev/null
+++ b/app/src/main/java/com/facebook/samples/zoomable/ZoomableController.java
@@ -0,0 +1,97 @@
+/*
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only.  Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.facebook.samples.zoomable;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+
+/**
+ * Interface for implementing a controller that works with {@link 
ZoomableDraweeView}
+ * to control the zoom.
+ */
+public interface ZoomableController {
+
+  /**
+   * Listener interface.
+   */
+  public interface Listener {
+
+    /**
+     * Notifies the view that the transform changed.
+     *
+     * @param transform the new matrix
+     */
+    void onTransformChanged(Matrix transform);
+  }
+
+  /**
+   * Enables the controller. The controller is enabled when the image has been 
loaded.
+   *
+   * @param enabled whether to enable the controller
+   */
+  void setEnabled(boolean enabled);
+
+  /**
+   * Gets whether the controller is enabled. This should return the last value 
passed to
+   * {@link #setEnabled}.
+   *
+   * @return whether the controller is enabled.
+   */
+  boolean isEnabled();
+
+  /**
+   * Sets the listener for the controller to call back when the matrix changes.
+   *
+   * @param listener the listener
+   */
+  void setListener(Listener listener);
+
+  /**
+   * Gets the current scale factor. A convenience method for calculating the 
scale from the
+   * transform.
+   *
+   * @return the current scale factor
+   */
+  float getScaleFactor();
+
+  /**
+   * Gets the current transform.
+   *
+   * @return the transform
+   */
+  Matrix getTransform();
+
+  /**
+   * Sets the bounds of the image post transform prior to application of the 
zoomable
+   * transformation.
+   *
+   * @param imageBounds the bounds of the image
+   */
+  void setImageBounds(RectF imageBounds);
+
+  /**
+   * Sets the bounds of the view.
+   *
+   * @param viewBounds the bounds of the view
+   */
+  void setViewBounds(RectF viewBounds);
+
+  /**
+   * Allows the controller to handle a touch event.
+   *
+   * @param event the touch event
+   * @return whether the controller handled the event
+   */
+  boolean onTouchEvent(MotionEvent event);
+}
diff --git 
a/app/src/main/java/com/facebook/samples/zoomable/ZoomableDraweeView.java 
b/app/src/main/java/com/facebook/samples/zoomable/ZoomableDraweeView.java
new file mode 100644
index 0000000..ef48379
--- /dev/null
+++ b/app/src/main/java/com/facebook/samples/zoomable/ZoomableDraweeView.java
@@ -0,0 +1,243 @@
+/*
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only.  Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.facebook.samples.zoomable;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Animatable;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.facebook.common.internal.Preconditions;
+import com.facebook.common.logging.FLog;
+import com.facebook.drawee.controller.AbstractDraweeController;
+import com.facebook.drawee.controller.BaseControllerListener;
+import com.facebook.drawee.controller.ControllerListener;
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
+import com.facebook.drawee.interfaces.DraweeController;
+import com.facebook.drawee.view.DraweeView;
+
+/**
+ * DraweeView that has zoomable capabilities.
+ * <p>
+ * Once the image loads, pinch-to-zoom and translation gestures are enabled.
+ */
+public class ZoomableDraweeView extends DraweeView<GenericDraweeHierarchy>
+    implements ZoomableController.Listener {
+
+  private static final Class<?> TAG = ZoomableDraweeView.class;
+
+  private static final float HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f;
+
+  private final RectF mImageBounds = new RectF();
+  private final RectF mViewBounds = new RectF();
+  private int touchSlop = 
ViewConfiguration.get(getContext()).getScaledTouchSlop();
+  private int startX;
+  private int startY;
+
+  private final ControllerListener mControllerListener = new 
BaseControllerListener<Object>() {
+    @Override
+    public void onFinalImageSet(
+        String id,
+        @Nullable Object imageInfo,
+        @Nullable Animatable animatable) {
+      ZoomableDraweeView.this.onFinalImageSet();
+    }
+
+    @Override
+    public void onRelease(String id) {
+      ZoomableDraweeView.this.onRelease();
+    }
+  };
+
+  private DraweeController mHugeImageController;
+  private ZoomableController mZoomableController = 
DefaultZoomableController.newInstance();
+
+  public ZoomableDraweeView(Context context) {
+    super(context);
+    init();
+  }
+
+  public ZoomableDraweeView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    init();
+  }
+
+  public ZoomableDraweeView(Context context, AttributeSet attrs, int defStyle) 
{
+    super(context, attrs, defStyle);
+    init();
+  }
+
+  private void init() {
+    mZoomableController.setListener(this);
+  }
+
+  /**
+   * Returns the matrix that matches the zoom selected by user gestures,
+   * but does not include the base scaling of the image itself. Transforms
+   * from view-absolute to view-absolute coordinates.
+   */
+  public void getTransformMatrix(Matrix outMatrix) {
+    outMatrix.set(mZoomableController.getTransform());
+  }
+
+  /**
+   * Gets the bounds of the image, in view-absolute coordinates,
+   * including the effects of user gestures.
+   */
+  public void getTransformedBounds(RectF outBounds) {
+    getPlainBounds(outBounds);
+    Matrix matrix = mZoomableController.getTransform();
+    matrix.mapRect(outBounds);
+  }
+
+  /**
+   * Gets the bounds of the image, in view-absolute coordinates,
+   * but not including the effets of user gestures.
+   */
+  public void getPlainBounds(RectF outBounds) {
+    getHierarchy().getActualImageBounds(outBounds);
+  }
+
+  public void setZoomableController(ZoomableController zoomableController) {
+    Preconditions.checkNotNull(zoomableController);
+    mZoomableController.setListener(null);
+    mZoomableController = zoomableController;
+    mZoomableController.setListener(this);
+  }
+
+  @Override
+  public void setController(@Nullable DraweeController controller) {
+    setControllers(controller, null);
+  }
+
+  private void setControllersInternal(
+      @Nullable DraweeController controller,
+      @Nullable DraweeController hugeImageController) {
+    removeControllerListener(getController());
+    addControllerListener(controller);
+    mHugeImageController = hugeImageController;
+    super.setController(controller);
+  }
+
+    /**
+     * Sets the controllers for the normal and huge image.
+     *
+     * <p> IMPORTANT: in order to avoid a flicker when switching to the huge 
image, the huge image
+     * controller should have the normal-image-uri set as its low-res-uri.
+     *
+     * @param controller controller to be initially used
+     * @param hugeImageController controller to be used after the client 
starts zooming-in
+     */
+  public void setControllers(
+      @Nullable DraweeController controller,
+      @Nullable DraweeController hugeImageController) {
+    setControllersInternal(null, null);
+    mZoomableController.setEnabled(false);
+    setControllersInternal(controller, hugeImageController);
+  }
+
+  private void maybeSetHugeImageController() {
+    if (mHugeImageController != null
+            && mZoomableController.getScaleFactor() > 
HUGE_IMAGE_SCALE_FACTOR_THRESHOLD) {
+      setControllersInternal(mHugeImageController, null);
+    }
+  }
+
+  private void removeControllerListener(DraweeController controller) {
+    if (controller instanceof AbstractDraweeController) {
+      ((AbstractDraweeController) controller)
+          .removeControllerListener(mControllerListener);
+    }
+  }
+
+  private void addControllerListener(DraweeController controller) {
+    if (controller instanceof AbstractDraweeController) {
+      ((AbstractDraweeController) controller)
+          .addControllerListener(mControllerListener);
+    }
+  }
+
+  @Override
+  protected void onDraw(Canvas canvas) {
+    int saveCount = canvas.save();
+    canvas.concat(mZoomableController.getTransform());
+    super.onDraw(canvas);
+    canvas.restoreToCount(saveCount);
+  }
+
+  @Override
+  public boolean onTouchEvent(MotionEvent event) {
+    if (mZoomableController.onTouchEvent(event)) {
+      if (mZoomableController.getScaleFactor() > 1.0f) {
+        getParent().requestDisallowInterceptTouchEvent(true);
+      }
+      FLog.v(TAG, "onTouchEvent: view %x, handled by zoomable controller", 
this.hashCode());
+      if (event.getAction() == MotionEvent.ACTION_DOWN) {
+        startX = (int) event.getX();
+        startY = (int) event.getY();
+      } else if (event.getAction() == MotionEvent.ACTION_UP
+              && Math.abs((int) event.getX() - startX) < touchSlop
+              && Math.abs((int) event.getY() - startY) < touchSlop) {
+        this.performClick();
+      }
+      return true;
+    }
+    FLog.v(TAG, "onTouchEvent: view %x, handled by the super", 
this.hashCode());
+    return super.onTouchEvent(event);
+  }
+
+  @Override
+  protected void onLayout(boolean changed, int left, int top, int right, int 
bottom) {
+    FLog.v(TAG, "onLayout: view %x", this.hashCode());
+    super.onLayout(changed, left, top, right, bottom);
+    updateZoomableControllerBounds();
+  }
+
+  private void onFinalImageSet() {
+    FLog.v(TAG, "onFinalImageSet: view %x", this.hashCode());
+    if (!mZoomableController.isEnabled()) {
+      updateZoomableControllerBounds();
+      mZoomableController.setEnabled(true);
+    }
+  }
+
+  private void onRelease() {
+    FLog.v(TAG, "onRelease: view %x", this.hashCode());
+    mZoomableController.setEnabled(false);
+  }
+
+  @Override
+  public void onTransformChanged(Matrix transform) {
+    FLog.v(TAG, "onTransformChanged: view %x", this.hashCode());
+    maybeSetHugeImageController();
+    invalidate();
+  }
+
+  private void updateZoomableControllerBounds() {
+    getPlainBounds(mImageBounds);
+    mViewBounds.set(0, 0, getWidth(), getHeight());
+    mZoomableController.setImageBounds(mImageBounds);
+    mZoomableController.setViewBounds(mViewBounds);
+    FLog.v(
+        TAG,
+        "updateZoomableControllerBounds: view %x, view bounds: %s, image 
bounds: %s",
+        this.hashCode(),
+        mViewBounds,
+        mImageBounds);
+  }
+}
diff --git 
a/app/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java 
b/app/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
index 852eac0..f6700d9 100644
--- a/app/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
+++ b/app/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
@@ -33,8 +33,12 @@
 
 import com.facebook.drawee.backends.pipeline.Fresco;
 import com.facebook.drawee.controller.BaseControllerListener;
+import com.facebook.drawee.drawable.ScalingUtils;
+import com.facebook.drawee.generic.GenericDraweeHierarchy;
+import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
 import com.facebook.drawee.view.SimpleDraweeView;
 import com.facebook.imagepipeline.image.ImageInfo;
+import com.facebook.samples.zoomable.ZoomableDraweeView;
 
 import java.util.Map;
 
@@ -53,7 +57,7 @@
     private String mimeType;
     private ProgressBar progressBar;
 
-    private SimpleDraweeView imageView;
+    private ZoomableDraweeView imageView;
 
     private View videoContainer;
     private VideoView videoView;
@@ -92,19 +96,22 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, 
Bundle savedInstanceState) {
         View rootView = inflater.inflate(R.layout.fragment_gallery_item, 
container, false);
-        View containerView = 
rootView.findViewById(R.id.gallery_item_container);
-        containerView.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                parentActivity.toggleControls();
-            }
-        });
         progressBar = (ProgressBar) 
rootView.findViewById(R.id.gallery_item_progress_bar);
         videoContainer = rootView.findViewById(R.id.gallery_video_container);
         videoView = (VideoView) rootView.findViewById(R.id.gallery_video);
         videoThumbnail = (SimpleDraweeView) 
rootView.findViewById(R.id.gallery_video_thumbnail);
         videoPlayButton = 
rootView.findViewById(R.id.gallery_video_play_button);
-        imageView = (SimpleDraweeView) 
rootView.findViewById(R.id.gallery_image);
+        imageView = (ZoomableDraweeView) 
rootView.findViewById(R.id.gallery_image);
+        imageView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                parentActivity.toggleControls();
+            }
+        });
+        GenericDraweeHierarchy hierarchy = new 
GenericDraweeHierarchyBuilder(getResources())
+                .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
+                .build();
+        imageView.setHierarchy(hierarchy);
         return rootView;
     }
 
diff --git a/app/src/main/res/layout/fragment_gallery_item.xml 
b/app/src/main/res/layout/fragment_gallery_item.xml
index 9969208..023e330 100644
--- a/app/src/main/res/layout/fragment_gallery_item.xml
+++ b/app/src/main/res/layout/fragment_gallery_item.xml
@@ -35,7 +35,7 @@
             android:contentDescription="@null"
             />
     </FrameLayout>
-    <com.facebook.drawee.view.SimpleDraweeView
+    <com.facebook.samples.zoomable.ZoomableDraweeView
         android:id="@+id/gallery_image"
         android:visibility="gone"
         android:layout_width="match_parent"

-- 
To view, visit https://gerrit.wikimedia.org/r/269891
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I764a81e904f889318744a985fa197d4c1373c971
Gerrit-PatchSet: 2
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Dbrant <[email protected]>
Gerrit-Reviewer: BearND <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Dbrant <[email protected]>
Gerrit-Reviewer: Mholloway <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: Sniedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to