Bug 1165127 - Animations for displaying/closing the zoomed view. r=mcomella
authordominique vincent <domivinc@toitl.com>
Sat, 13 Jun 2015 22:42:20 +0200
changeset 249072 05e42bcd0d7740d6d1f05e1b1470cacf42097967
parent 249071 9dc97d73b64fa6dbe7bde696965f5e07266f6298
child 249073 a6cf5aa6f5f1da99bf0f3f73551260f413782cbd
push id61139
push usercbook@mozilla.com
push dateTue, 16 Jun 2015 14:53:44 +0000
treeherdermozilla-inbound@7299b8b5a8a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs1165127
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1165127 - Animations for displaying/closing the zoomed view. r=mcomella
mobile/android/base/ZoomedView.java
--- a/mobile/android/base/ZoomedView.java
+++ b/mobile/android/base/ZoomedView.java
@@ -30,16 +30,20 @@ import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.ScaleAnimation;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import java.nio.ByteBuffer;
 import java.text.DecimalFormat;
 
@@ -47,45 +51,55 @@ public class ZoomedView extends FrameLay
         LayerView.ZoomedViewListener, GeckoEventListener {
     private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
 
     private static final float[] ZOOM_FACTORS_LIST = {2.0f, 3.0f, 1.5f};
     private static final int W_CAPTURED_VIEW_IN_PERCENT = 50;
     private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
     private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
     private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
+    private static final int OPENING_ANIMATION_DURATION_MS = 250;
+    private static final int CLOSING_ANIMATION_DURATION_MS = 150;
+    private static final float OVERSHOOT_INTERPOLATOR_TENSION = 1.5f;
 
     private float zoomFactor;
     private int currentZoomFactorIndex;
     private ImageView zoomedImageView;
     private LayerView layerView;
     private int viewWidth;
     private int viewHeight; // Only the zoomed view height, no toolbar, no shadow ...
     private int viewContainerWidth;
     private int viewContainerHeight; // Zoomed view height with toolbar and other elements like shadow, ...
     private int containterSize; // shadow, margin, ...
     private Point lastPosition;
     private boolean shouldSetVisibleOnUpdate;
     private PointF returnValue;
+    private final PointF animationStart;
     private ImageView closeButton;
     private TextView changeZoomFactorButton;
     private boolean toolbarOnTop;
     private float offsetDueToToolBarPosition;
     private int toolbarHeight;
     private int cornerRadius;
 
     private boolean stopUpdateView;
 
     private int lastOrientation;
 
     private ByteBuffer buffer;
     private Runnable requestRenderRunnable;
     private long startTimeReRender;
     private long lastStartTimeReRender;
 
+    private ZoomedViewTouchListener touchListener;
+
+    private enum StartPointUpdate {
+        GECKO_POSITION, CENTER, NO_CHANGE
+    }
+
     private class RoundedBitmapDrawable extends BitmapDrawable {
         private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
         final float cornerRadius;
         final boolean squareOnTopOfDrawable;
 
         RoundedBitmapDrawable(Resources res, Bitmap bitmap, boolean squareOnTop, int radius) {
             super(res, bitmap);
             squareOnTopOfDrawable = squareOnTop;
@@ -177,17 +191,17 @@ public class ZoomedView extends FrameLay
                 // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position.
                 // In this case, the move is ignored if the delta is lower than 1 unit.
                 return false;
             }
 
             float newLeftMargin = params.leftMargin + event.getRawX() - originRawX;
             float newTopMargin = params.topMargin + event.getRawY() - originRawY;
             ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
-            ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin);
+            ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin, StartPointUpdate.CENTER);
             originRawX = event.getRawX();
             originRawY = event.getRawY();
             return true;
         }
     }
 
     public ZoomedView(Context context) {
         this(context, null, 0);
@@ -195,24 +209,26 @@ public class ZoomedView extends FrameLay
 
     public ZoomedView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
     public ZoomedView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         returnValue = new PointF();
+        animationStart = new PointF();
         currentZoomFactorIndex = 0;
         zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
         requestRenderRunnable = new Runnable() {
             @Override
             public void run() {
                 requestZoomedViewRender();
             }
         };
+        touchListener = new ZoomedViewTouchListener();
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
                 "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
     }
 
     void destroy() {
         ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
                 "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
@@ -220,40 +236,51 @@ public class ZoomedView extends FrameLay
 
     // This method (onFinishInflate) is called only when the zoomed view class is used inside
     // an xml structure <org.mozilla.gecko.ZoomedView ...
     // It won't be called if the class is used from java code like "new  ZoomedView(context);"
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         closeButton = (ImageView) findViewById(R.id.dialog_close);
-        closeButton.setOnClickListener(new View.OnClickListener() {
-            public void onClick(View view) {
-                stopZoomDisplay();
-            }
-        });
+        changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
+        zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
 
-        changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
-        changeZoomFactorButton.setOnClickListener(new View.OnClickListener() {
-            public void onClick(View view) {
-                changeZoomFactor();
-            }
-        });
         setTextInZoomFactorButton(ZOOM_FACTORS_LIST[0]);
 
-        zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
-        this.setOnTouchListener(new ZoomedViewTouchListener());
-
         toolbarHeight = getResources().getDimensionPixelSize(R.dimen.zoomed_view_toolbar_height);
         containterSize = getResources().getDimensionPixelSize(R.dimen.drawable_dropshadow_size);
         cornerRadius = getResources().getDimensionPixelSize(R.dimen.button_corner_radius);
 
         moveToolbar(true);
     }
 
+    private void setListeners() {
+        closeButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                stopZoomDisplay(true);
+            }
+        });
+
+        changeZoomFactorButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                changeZoomFactor();
+            }
+        });
+
+        setOnTouchListener(touchListener);
+    }
+
+    private void removeListeners() {
+        closeButton.setOnClickListener(null);
+
+        changeZoomFactorButton.setOnClickListener(null);
+
+        setOnTouchListener(null);
+    }
     /*
      * Convert a click from ZoomedView. Return the position of the click in the
      * LayerView
      */
     private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
         if (toolbarOnTop && y > toolbarHeight) {
            y = y - toolbarHeight;
         }
@@ -318,17 +345,18 @@ public class ZoomedView extends FrameLay
         returnValue.y = (int) ((((y + offsetDueToToolBarPosition - (viewHeight / (2 * zoomFactor)))) /
                         ((parentHeight - offset.y + offsetDueToToolBarPosition - (viewHeight / zoomFactor)) /
                         (parentHeight - offset.y - viewContainerHeight)))
                 + offset.y);
 
         return returnValue;
     }
 
-    private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin) {
+    private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin,
+            StartPointUpdate animateStartPoint) {
         final float parentWidth = metrics.getWidth();
         final float parentHeight = metrics.getHeight();
         RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
         newLayoutParams.leftMargin = (int) newLeftMargin;
         newLayoutParams.topMargin = (int) newTopMargin;
         int topMarginMin;
         int leftMarginMin;
         PointF offset = metrics.getMarginOffset();
@@ -348,16 +376,32 @@ public class ZoomedView extends FrameLay
         }
 
         if (newLayoutParams.topMargin < topMarginMin + 1) {
             moveToolbar(false);
         } else if (newLayoutParams.topMargin + viewContainerHeight > parentHeight - 1) {
             moveToolbar(true);
         }
 
+        if (animateStartPoint == StartPointUpdate.GECKO_POSITION) {
+            // Before this point, the animationStart point is relative to the layerView.
+            // The value is initialized in startZoomDisplay using the click point position coming from Gecko.
+            // The position of the zoomed view is now calculated, so the position of the animation
+            // can now be correctly set relative to the zoomed view
+            animationStart.x = animationStart.x - newLayoutParams.leftMargin;
+            animationStart.y = animationStart.y - newLayoutParams.topMargin;
+        } else if (animateStartPoint == StartPointUpdate.CENTER) {
+            // At this point, the animationStart point is no more valid probably because
+            // the zoomed view has been moved by the user.
+            // In this case, the animationStart point is set to the center point of the zoomed view.
+            PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(viewContainerWidth / 2, viewContainerHeight / 2);
+            animationStart.x = convertedPosition.x - newLayoutParams.leftMargin;
+            animationStart.y = convertedPosition.y - newLayoutParams.topMargin;
+        }
+
         setLayoutParams(newLayoutParams);
         PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, 0);
         lastPosition = PointUtils.round(convertedPosition);
         requestZoomedViewRender();
     }
 
     private void moveToolbar(boolean moveTop) {
         if (toolbarOnTop == moveTop) {
@@ -404,17 +448,17 @@ public class ZoomedView extends FrameLay
 
     private void refreshZoomedViewSize(ImmutableViewportMetrics viewport) {
         if (layerView == null) {
             return;
         }
 
         RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
         setCapturedSize(viewport);
-        moveZoomedView(viewport, params.leftMargin, params.topMargin);
+        moveZoomedView(viewport, params.leftMargin, params.topMargin, StartPointUpdate.NO_CHANGE);
     }
 
     private void setCapturedSize(ImmutableViewportMetrics metrics) {
         float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
         viewWidth = (int) ((parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
         viewHeight = (int) ((parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
         viewContainerHeight = viewHeight + toolbarHeight +
                 2 * containterSize; // Top and bottom shadows
@@ -438,22 +482,31 @@ public class ZoomedView extends FrameLay
             layerView = aLayerView;
             layerView.addZoomedViewListener(this);
             layerView.setOnMetricsChangedZoomedViewportListener(this);
             ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
             setCapturedSize(metrics);
         }
         startTimeReRender = 0;
         shouldSetVisibleOnUpdate = true;
+
+        ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
+        PointF offset = metrics.getMarginOffset();
+        // At this point, the start point is relative to the layerView.
+        // Later, it will be converted relative to the zoomed view as soon as
+        // the position of the zoomed view will be calculated.
+        animationStart.x = (float) leftFromGecko * metrics.zoomFactor + offset.x;
+        animationStart.y = (float) topFromGecko * metrics.zoomFactor + offset.y;
+
         moveUsingGeckoPosition(leftFromGecko, topFromGecko);
     }
 
-    private void stopZoomDisplay() {
+    private void stopZoomDisplay(boolean withAnimation) {
         shouldSetVisibleOnUpdate = false;
-        this.setVisibility(View.GONE);
+        hideZoomedView(withAnimation);
         ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
         if (layerView != null) {
             layerView.setOnMetricsChangedZoomedViewportListener(null);
             layerView.removeZoomedViewListener(this);
             layerView = null;
         }
     }
 
@@ -489,34 +542,34 @@ public class ZoomedView extends FrameLay
                         LayerView geckoAppLayerView = GeckoAppShell.getLayerView();
                         if (geckoAppLayerView != null) {
                             startZoomDisplay(geckoAppLayerView, left, top);
                         }
                     } else if (event.equals("Window:Resize")) {
                         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
                         refreshZoomedViewSize(metrics);
                     } else if (event.equals("Content:LocationChange")) {
-                        stopZoomDisplay();
+                        stopZoomDisplay(false);
                     }
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "JSON exception", e);
                 }
             }
         });
     }
 
     private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) {
         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
         final float parentHeight = metrics.getHeight();
         // moveToolbar is called before getZoomedViewTopLeftPositionFromTouchPosition in order to
         // correctly center vertically the zoomed area
         moveToolbar((topFromGecko * metrics.zoomFactor > parentHeight / 2));
         PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor),
                 (topFromGecko * metrics.zoomFactor));
-        moveZoomedView(metrics, convertedPosition.x, convertedPosition.y);
+        moveZoomedView(metrics, convertedPosition.x, convertedPosition.y, StartPointUpdate.GECKO_POSITION);
     }
 
     @Override
     public void onMetricsChanged(final ImmutableViewportMetrics viewport) {
         // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient).
         // Post to UI Thread to avoid Exception:
         //    "Only the original thread that created a view hierarchy can touch its views."
         ThreadUtils.postToUiThread(new Runnable() {
@@ -543,23 +596,76 @@ public class ZoomedView extends FrameLay
                 Log.w(LOGTAG, iae.toString());
             }
             if (zoomedImageView != null) {
                 RoundedBitmapDrawable ob3 = new RoundedBitmapDrawable(getResources(), sb3, toolbarOnTop, cornerRadius);
                 zoomedImageView.setImageDrawable(ob3);
             }
         }
         if (shouldSetVisibleOnUpdate) {
-            this.setVisibility(View.VISIBLE);
-            shouldSetVisibleOnUpdate = false;
+            this.showZoomedView();
         }
         lastStartTimeReRender = startTimeReRender;
         startTimeReRender = 0;
     }
 
+    private void showZoomedView() {
+        // no animation if the zoomed view is already visible
+        if (getVisibility() != View.VISIBLE) {
+            final Animation anim = new ScaleAnimation(
+                    0f, 1f, // Start and end values for the X axis scaling
+                    0f, 1f, // Start and end values for the Y axis scaling
+                    Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
+                    Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
+            anim.setFillAfter(true); // Needed to keep the result of the animation
+            anim.setDuration(OPENING_ANIMATION_DURATION_MS);
+            anim.setInterpolator(new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION));
+            anim.setAnimationListener(new AnimationListener() {
+                public void onAnimationEnd(Animation animation) {
+                    setListeners();
+                }
+                public void onAnimationRepeat(Animation animation) {
+                }
+                public void onAnimationStart(Animation animation) {
+                    removeListeners();
+                }
+            });
+            setAnimation(anim);
+        }
+        setVisibility(View.VISIBLE);
+        shouldSetVisibleOnUpdate = false;
+    }
+
+    private void hideZoomedView(boolean withAnimation) {
+        if (withAnimation) {
+            final Animation anim = new ScaleAnimation(
+                1f, 0f, // Start and end values for the X axis scaling
+                1f, 0f, // Start and end values for the Y axis scaling
+                Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
+                Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
+            anim.setFillAfter(true); // Needed to keep the result of the animation
+            anim.setDuration(CLOSING_ANIMATION_DURATION_MS);
+            anim.setAnimationListener(new AnimationListener() {
+                public void onAnimationEnd(Animation animation) {
+                }
+                public void onAnimationRepeat(Animation animation) {
+                }
+                public void onAnimationStart(Animation animation) {
+                    removeListeners();
+                }
+            });
+            setAnimation(anim);
+        } else {
+            removeListeners();
+            setAnimation(null);
+        }
+        setVisibility(View.GONE);
+        shouldSetVisibleOnUpdate = false;
+    }
+
     private void updateBufferSize() {
         int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
         int capacity = viewWidth * viewHeight * pixelSize;
         if (buffer == null || buffer.capacity() != capacity) {
             buffer = DirectBufferAllocator.free(buffer);
             buffer = DirectBufferAllocator.allocate(capacity);
         }
     }