Niedzielski has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/253065

Change subject: Hygiene: move lead parallax to Drawable matrix
......................................................................

Hygiene: move lead parallax to Drawable matrix

The parallax effect used by the lead image was accomplished by sizing
its housing ImageView to the full dimensions of the image and then
translating the View itself to achieve a parallax effect. This was nice
because it avoided math and translations are cheap but poor because it
changed layout parameters without concern for adjacent Views. This
unusual behavior made it difficult to visualize the View hierarchy.

This patch moves the parallax effect from raw View resizing and
translations to the ImageView's Drawable matrix. The math is a little
intimidating in some parts, but it's mostly pure (free of implementation
details), and the View is now boring and predictable. This change will
make it easy to add or remove Views without surprising things happening.

Bug: T116122
Change-Id: I17ae6c24f876a37b1c86180daf4794dfaa2696bd
---
M app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
M app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
M app/src/main/res/layout/view_article_header.xml
3 files changed, 92 insertions(+), 53 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/65/253065/1

diff --git 
a/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java 
b/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
index dcfbb5e..dc4c222 100755
--- a/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
+++ b/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
@@ -13,7 +13,6 @@
 import android.graphics.PointF;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import org.json.JSONException;
@@ -337,10 +336,6 @@
         displayHeightDp = (int) (displayHeightPx / displayDensity);
     }
 
-    private void setImageLayoutParams(int width, int height) {
-        image.setLayoutParams(new FrameLayout.LayoutParams(width, height));
-    }
-
     private void loadLeadImage() {
         loadLeadImage(getLeadImageUrl());
     }
@@ -352,7 +347,7 @@
     private void loadLeadImage(@Nullable String url) {
         if (!isMainPage() && !TextUtils.isEmpty(url) && leadImagesEnabled) {
             String fullUrl = WikipediaApp.getInstance().getNetworkProtocol() + 
":" + url;
-            articleHeaderView.setImageYOffset(0);
+            articleHeaderView.setImageYScalar(0);
             articleHeaderView.loadImage(fullUrl);
         }
     }
@@ -444,51 +439,36 @@
         }
 
         private void detectFace(int bmpHeight, float aspect, @Nullable PointF 
faceLocation) {
-            int newWidth = image.getWidth();
-            int newHeight = (int) (newWidth * aspect);
+            faceYOffsetNormalized = faceYScalar(bmpHeight, faceLocation);
 
-            // give our image an offset based on the location of the face,
-            // relative to the image container
-            float scale = (float) newHeight / (float) bmpHeight;
-            int imageBaseYOffset;
-            if (faceLocation != null) {
-                int faceY = (int) (faceLocation.y * scale);
-                // if we have a face, then offset to the face location
-                imageBaseYOffset = -(faceY - (imagePlaceholder.getHeight() / 
2));
-                // Adjust the face position by a slight amount.
-                // The face recognizer gives the location of the *eyes*, 
whereas we actually
-                // want to center on the *nose*...
-                imageBaseYOffset += 
getDimension(R.dimen.face_detection_nose_y_offset);
-                faceYOffsetNormalized = faceLocation.y / bmpHeight;
-            } else {
-                // No face, so we'll just chop the top 25% off rather than 
centering
-                final float oneQuarter = 0.25f;
-                imageBaseYOffset = -(int) ((newHeight - 
imagePlaceholder.getHeight()) * oneQuarter);
-                faceYOffsetNormalized = oneQuarter;
-            }
+            float scalar = constrainScalar(faceYOffsetNormalized);
 
-            // is the offset too far to the top?
-            imageBaseYOffset = Math.min(0, imageBaseYOffset);
-
-            // is the offset too far to the bottom?
-            imageBaseYOffset = Math.max(imageBaseYOffset, 
imagePlaceholder.getHeight() - newHeight);
-
-            // resize our image to have the same proportions as the acquired 
bitmap
-            if (newHeight < imagePlaceholder.getHeight()) {
-                // if the height of the image is less than the container, then 
just
-                // make it the same height as the placeholder.
-                setImageLayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 
imagePlaceholder.getHeight());
-                imageBaseYOffset = 0;
-            } else {
-                setImageLayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 
newHeight);
-            }
-
-            articleHeaderView.setImageYOffset(imageBaseYOffset);
+            articleHeaderView.setImageYScalar(scalar);
 
             // fade in the new image!
             ViewAnimations.crossFade(imagePlaceholder, image);
 
             startKenBurnsAnimation();
         }
+
+        private float constrainScalar(float scalar) {
+            scalar = Math.max(0, scalar);
+            scalar = Math.min(scalar, 1);
+            return scalar;
+        }
+
+        private float faceYScalar(int bmpHeight, PointF faceLocation) {
+            final float defaultOffsetScalar = .25f;
+            float scalar = defaultOffsetScalar;
+            if (faceLocation != null) {
+                scalar = faceLocation.y / bmpHeight;
+                // TODO: if it is desirable to offset to the nose, replace 
this arbitrary hardcoded
+                //       value with a proportion. FaceDetector.eyesDistance() 
presumably provides
+                //       the interpupillary distance in pixels. We can 
multiply this measurement by
+                //       the proportion of the length of the nose to the IPD.
+                scalar += getDimension(R.dimen.face_detection_nose_y_offset) / 
bmpHeight;
+            }
+            return scalar;
+        }
     }
 }
diff --git a/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java 
b/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
index 9dc48eb..beb5f9a 100644
--- a/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
+++ b/app/src/main/java/org/wikipedia/views/ArticleHeaderView.java
@@ -5,6 +5,7 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -58,7 +59,7 @@
     @NonNull private CharSequence subtitle = "";
     @Nullable private String pronunciationUrl;
 
-    private int imageYOffset;
+    private float imageYScalar;
 
     @NonNull private final AvPlayer avPlayer = new DefaultAvPlayer(new 
MediaPlayerImplementation());
 
@@ -148,8 +149,8 @@
         return returnedBitmap;
     }
 
-    public void setImageYOffset(int offset) {
-        imageYOffset = offset;
+    public void setImageYScalar(float offset) {
+        imageYScalar = offset;
         updateParallaxScroll();
     }
 
@@ -217,6 +218,12 @@
     }
 
     @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int 
bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateParallaxScroll();
+    }
+
+    @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         avPlayer.deinit();
@@ -229,7 +236,18 @@
     private void updateParallaxScroll(int scrollY) {
         int offset = Math.min(getHeight(), scrollY);
         setTranslationY(-offset);
-        image.setTranslationY(imageYOffset + offset / 2);
+
+        ImageView view;
+        float scalar;
+        if (image.getDrawable() == null) {
+            view = placeholder;
+            scalar = 0;
+        } else {
+            view = image;
+            scalar = imageYScalar;
+        }
+
+        updateImageViewParallax(view, scalar, offset / 2);
     }
 
     private void updateText() {
@@ -250,6 +268,47 @@
         text.setText(builder);
     }
 
+    private void updateImageViewParallax(ImageView view, float scalar, int 
offset) {
+        Matrix matrix = centerCropWithOffsetScalar(view, view.getDrawable(), 
view.getImageMatrix(), scalar);
+        matrix.postTranslate(0, offset);
+        view.setImageMatrix(matrix);
+    }
+
+    // See ImageView
+    private Matrix centerCropWithOffsetScalar(@NonNull View view,
+                                              @NonNull Drawable drawable,
+                                              @NonNull Matrix initial,
+                                              float offsetScalar) {
+        final float halfScalar = .5f;
+
+        Matrix matrix = new Matrix(initial);
+
+        int drawableWidth = drawable.getIntrinsicWidth();
+        int drawableHeight = drawable.getIntrinsicHeight();
+
+        int viewWidth = view.getWidth() - view.getPaddingLeft() - 
view.getPaddingRight();
+        int viewHeight = view.getHeight() - view.getPaddingTop() - 
view.getPaddingBottom();
+
+        float scale;
+        float dx = 0;
+        float dy = 0;
+        if (drawableWidth * viewHeight > viewWidth * drawableHeight) {
+            scale = (float) viewHeight / (float) drawableHeight;
+            dx = (viewWidth - drawableWidth * scale) * halfScalar;
+        } else {
+            scale = (float) viewWidth / (float) drawableWidth;
+            dy = (viewHeight - drawableHeight * scale) * halfScalar;
+        }
+
+        matrix.setScale(scale, scale);
+        matrix.postTranslate(dx, dy);
+
+        float y = (viewHeight - drawableHeight * scale) * (offsetScalar - 
halfScalar);
+        matrix.postTranslate(0, y);
+
+        return matrix;
+    }
+
     private Spanned pronunciationSpanned() {
         AudioUrlSpan span = new AudioUrlSpan(text, avPlayer, pronunciationUrl,
                 AudioUrlSpan.ALIGN_BASELINE);
diff --git a/app/src/main/res/layout/view_article_header.xml 
b/app/src/main/res/layout/view_article_header.xml
index ea8fe21..b8cb038 100644
--- a/app/src/main/res/layout/view_article_header.xml
+++ b/app/src/main/res/layout/view_article_header.xml
@@ -5,8 +5,8 @@
     <org.wikipedia.page.leadimages.ImageViewWithFace
         android:id="@+id/image"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:scaleType="centerCrop"
+        android:layout_height="match_parent"
+        android:scaleType="matrix"
         android:background="@color/white"
         android:contentDescription="@null" />
 
@@ -14,8 +14,8 @@
     <ImageView
         android:id="@+id/placeholder"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:scaleType="centerCrop"
+        android:layout_height="match_parent"
+        android:scaleType="matrix"
         android:src="?lead_image_drawable"
         android:contentDescription="@null" />
 
@@ -32,4 +32,4 @@
         android:lineSpacingMultiplier="@dimen/lead_title_leading_scalar"
         tools:text="Title\nSubtitle" />
 
-</merge>
\ No newline at end of file
+</merge>

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I17ae6c24f876a37b1c86180daf4794dfaa2696bd
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <sniedziel...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to