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

Change subject: Reduce Share A Fact memory and fix lead font face
......................................................................


Reduce Share A Fact memory and fix lead font face

* Replace LeadImagesHandler.getBitmapFromView() with
  ImageViewWithFace.getImageBitmap(). This method returns a previously
  set Bitmap or null to reduce the number of images in memory. It is
  used indirectly by Share A Fact.
* Replace lead title and subtitle android:fontFamily attribute with
  Java. This gives more consistent appearance and leading on API 15
  which doesn't support the attribute.
* Move large Runnable.run() implementation to private method,
  LeadImagesHandler.detectFace(). This translation likely requires diff
  stitching. No other changes intended other than those mentioned in the
  bullets.
* Move anonymous inline SaneAsyncTask from
  ImageViewWithFace.onBitmapLoaded() to private nested class,
  FaceDetectionTask.
* Change OnImageLoadListener.onImageLoaded() face detection failure
  value from negative to null.
* Rename anonymous ImageViewWithFace SaneAsyncTask subclass local
  variable from tempbmp to testBmp; extract method for generating test
  bitmap, new565ScaledBitmap(); move face detection eligibility check
  before spawning the task.
* Minor reordering of ImageViewWithFace members.
* Replace nullable OnImageLoadListener with null pattern.
* Replace ImageViewWithFace Log.* calls with L.*.
* Add helper method for setting lead image layout parameters, 
setImageLayoutParams().
* Add IDE hint annotations.
* Move "faceBoost", the y-axis offset to the nose, to 
face_detection_nose_y_offset.

Change-Id: Ibc793ab778691b9a2d182527e14a6b9fb2703ef3
---
M app/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
M app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
M app/src/main/res/layout/fragment_page.xml
M app/src/main/res/values/dimens.xml
4 files changed, 179 insertions(+), 157 deletions(-)

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



diff --git 
a/app/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java 
b/app/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
index 63ba0e4..161c086 100644
--- a/app/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
+++ b/app/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
@@ -7,26 +7,27 @@
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.media.FaceDetector;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.widget.ImageView;
 
 import com.squareup.picasso.Picasso;
 import com.squareup.picasso.Target;
 
 import org.wikipedia.concurrency.SaneAsyncTask;
+import org.wikipedia.util.log.L;
 
 public class ImageViewWithFace extends ImageView implements Target {
-    private static final String TAG = "ImageViewWithFace";
+    public interface OnImageLoadListener {
+        void onImageLoaded(Bitmap bitmap, @Nullable PointF faceLocation);
+        void onImageFailed();
+    }
 
-    // Width to which to reduce image copy on which face detection is 
performed in onBitMapLoaded()
-    // (with height reduced proportionally there).  Performing face detection 
on a scaled-down
-    // image copy improves speed and memory use.
-    //
-    // Also, note that the face detector requires that the image width be even.
-    private static final int BITMAP_COPY_WIDTH = 200;
+    @NonNull private OnImageLoadListener listener = new DefaultListener();
 
     public ImageViewWithFace(Context context) {
         super(context);
@@ -40,91 +41,121 @@
         super(context, attrs, defStyle);
     }
 
+    public void setOnImageLoadListener(@Nullable OnImageLoadListener listener) 
{
+        this.listener = listener == null ? new DefaultListener() : listener;
+    }
+
+    @Nullable public Bitmap getImageBitmap() {
+        return getDrawable() instanceof BitmapDrawable
+             ? ((BitmapDrawable) getDrawable()).getBitmap()
+             : null;
+    }
+
     @Override
-    public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
-        (new SaneAsyncTask<Void>(SaneAsyncTask.LOW_CONCURRENCY) {
-            @Override
-            public Void performTask() throws Throwable {
-                final int minSize = 64;
-                // don't load the image if it's terrible resolution
-                // (or indeed, if it's zero width or height)
-                if (bitmap.getWidth() < minSize || bitmap.getHeight() < 
minSize) {
-                    if (onImageLoadListener != null) {
-                        onImageLoadListener.onImageFailed();
-                    }
-                    return null;
-                }
-                // boost this thread's priority a bit
-                Thread.currentThread().setPriority(Thread.MAX_PRIORITY - 1);
-                long millis = System.currentTimeMillis();
-                // create a new bitmap onto which we'll draw the original 
bitmap,
-                // because the FaceDetector requires it to be a 565 bitmap, 
and it
-                // must also be even width. Reduce size of copy for 
performance.
-                Bitmap tempbmp = Bitmap.createBitmap(BITMAP_COPY_WIDTH,
-                        ((bitmap.getHeight() * BITMAP_COPY_WIDTH) / 
bitmap.getWidth()), Bitmap.Config.RGB_565);
-                Canvas canvas = new Canvas(tempbmp);
-                Rect srcRect = new Rect(0, 0, bitmap.getWidth(), 
bitmap.getHeight());
-                Rect destRect = new Rect(0, 0, BITMAP_COPY_WIDTH, 
tempbmp.getHeight());
-                Paint paint = new Paint();
-                paint.setColor(Color.BLACK);
-                canvas.drawBitmap(bitmap, srcRect, destRect, paint);
+    public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
+        if (isBitmapEligibleForFaceDetection(bitmap)) {
+            spawnFaceDetectionTask(bitmap);
+        } else {
+            listener.onImageFailed();
+        }
 
-                // initialize the face detector, and look for only one face...
-                FaceDetector fd = new FaceDetector(tempbmp.getWidth(), 
tempbmp.getHeight(), 1);
-                FaceDetector.Face[] faces = new FaceDetector.Face[1];
-                PointF facePos = new PointF();
-                int numFound = fd.findFaces(tempbmp, faces);
-
-                if (numFound > 0) {
-                    faces[0].getMidPoint(facePos);
-                    // scale back to proportions of original image
-                    facePos.x = (facePos.x * bitmap.getWidth() / 
BITMAP_COPY_WIDTH);
-                    facePos.y = (facePos.y * bitmap.getHeight() / 
tempbmp.getHeight());
-                    Log.d(TAG, "Found face at " + facePos.x + ", " + 
facePos.y);
-                }
-                // free our temporary bitmap
-                tempbmp.recycle();
-
-                Log.d(TAG, "Face detection took " + 
(System.currentTimeMillis() - millis) + "ms");
-
-                if (onImageLoadListener != null) {
-                    onImageLoadListener.onImageLoaded(bitmap, facePos);
-                }
-                return null;
-            }
-            @Override
-            public void onCatch(Throwable caught) {
-                // it's not super important to do anything if face detection 
fails,
-                // but let our listener know about it anyway:
-                if (onImageLoadListener != null) {
-                    onImageLoadListener.onImageFailed();
-                }
-            }
-        }).execute();
         // and, of course, set the original bitmap as our image
-        this.setImageBitmap(bitmap);
+        setImageBitmap(bitmap);
     }
 
     @Override
     public void onBitmapFailed(Drawable errorDrawable) {
-        if (onImageLoadListener != null) {
-            onImageLoadListener.onImageFailed();
+        listener.onImageFailed();
+    }
+
+    @Override public void onPrepareLoad(Drawable placeHolderDrawable) { }
+
+    private boolean isBitmapEligibleForFaceDetection(Bitmap bitmap) {
+        final int minSize = 64;
+        return bitmap.getWidth() >= minSize && bitmap.getHeight() >= minSize;
+    }
+
+    private void spawnFaceDetectionTask(@NonNull final Bitmap bitmap) {
+        new FaceDetectionTask(SaneAsyncTask.LOW_CONCURRENCY, bitmap) {
+            @Override
+            public PointF performTask() {
+                PointF facePos = super.performTask();
+                listener.onImageLoaded(bitmap, facePos);
+                return facePos;
+            }
+
+            @Override
+            public void onCatch(Throwable caught) {
+                // it's not super important to do anything if face detection 
fails,
+                // but let our listener know about it anyway:
+                listener.onImageFailed();
+            }
+        }.execute();
+    }
+
+    private static class FaceDetectionTask extends SaneAsyncTask<PointF> {
+        // Width to which to reduce image copy on which face detection is 
performed in onBitMapLoaded()
+        // (with height reduced proportionally there).  Performing face 
detection on a scaled-down
+        // image copy improves speed and memory use.
+        //
+        // Also, note that the face detector requires that the image width be 
even.
+        private static final int BITMAP_COPY_WIDTH = 200;
+
+        @NonNull private final Bitmap srcBitmap;
+
+        public FaceDetectionTask(int concurrency, @NonNull Bitmap bitmap) {
+            super(concurrency);
+            srcBitmap = bitmap;
+        }
+
+        @Override
+        @Nullable
+        public PointF performTask() {
+            // boost this thread's priority a bit
+            Thread.currentThread().setPriority(Thread.MAX_PRIORITY - 1);
+            long millis = System.currentTimeMillis();
+            // create a new bitmap onto which we'll draw the original bitmap,
+            // because the FaceDetector requires it to be a 565 bitmap, and it
+            // must also be even width. Reduce size of copy for performance.
+            Bitmap testBmp = new565ScaledBitmap(srcBitmap);
+
+            // initialize the face detector, and look for only one face...
+            FaceDetector fd = new FaceDetector(testBmp.getWidth(), 
testBmp.getHeight(), 1);
+            FaceDetector.Face[] faces = new FaceDetector.Face[1];
+            int numFound = fd.findFaces(testBmp, faces);
+
+            PointF facePos = null;
+            if (numFound > 0) {
+                facePos = new PointF();
+                faces[0].getMidPoint(facePos);
+                // scale back to proportions of original image
+                facePos.x = (facePos.x * srcBitmap.getWidth() / 
BITMAP_COPY_WIDTH);
+                facePos.y = (facePos.y * srcBitmap.getHeight() / 
testBmp.getHeight());
+                L.d("Found face at " + facePos.x + ", " + facePos.y);
+            }
+            // free our temporary bitmap
+            testBmp.recycle();
+
+            L.d("Face detection took " + (System.currentTimeMillis() - millis) 
+ "ms");
+
+            return facePos;
+        }
+
+        @NonNull private Bitmap new565ScaledBitmap(Bitmap src) {
+            Bitmap copy =  Bitmap.createBitmap(BITMAP_COPY_WIDTH,
+                    (src.getHeight() * BITMAP_COPY_WIDTH) / src.getWidth(), 
Bitmap.Config.RGB_565);
+            Canvas canvas = new Canvas(copy);
+            Rect srcRect = new Rect(0, 0, src.getWidth(), src.getHeight());
+            Rect destRect = new Rect(0, 0, BITMAP_COPY_WIDTH, 
copy.getHeight());
+            Paint paint = new Paint();
+            paint.setColor(Color.BLACK);
+            canvas.drawBitmap(src, srcRect, destRect, paint);
+            return copy;
         }
     }
 
-    @Override
-    public void onPrepareLoad(Drawable placeHolderDrawable) {
-    }
-
-
-    public interface OnImageLoadListener {
-        void onImageLoaded(Bitmap bitmap, PointF faceLocation);
-        void onImageFailed();
-    }
-
-    private OnImageLoadListener onImageLoadListener;
-
-    public void setOnImageLoadListener(OnImageLoadListener listener) {
-        onImageLoadListener = listener;
+    private static class DefaultListener implements OnImageLoadListener {
+        @Override public void onImageLoaded(Bitmap bitmap, PointF 
faceLocation) { }
+        @Override public void onImageFailed() { }
     }
 }
\ No newline at end of file
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 ea15954..d7f43ae 100755
--- a/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
+++ b/app/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
@@ -2,8 +2,8 @@
 
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.ColorInt;
 import android.support.annotation.ColorRes;
@@ -130,6 +130,7 @@
         findViewsById(imageContainer);
 
         pageTitleGradient = 
GradientUtil.getCubicGradient(getColor(R.color.lead_gradient_start), 
Gravity.BOTTOM);
+        pageTitleText.setTypeface(Typeface.create(Typeface.SERIF, 
Typeface.NORMAL));
 
         initDisplayDimensions();
 
@@ -152,26 +153,12 @@
         imageContainer.setVisibility(View.INVISIBLE);
     }
 
-    @Nullable
-    public Bitmap getLeadImageBitmap() {
-        return leadImagesEnabled ? getBitmapFromView(image) : null;
+    @Nullable public Bitmap getLeadImageBitmap() {
+        return image.getImageBitmap();
     }
 
     public boolean isLeadImageEnabled() {
         return leadImagesEnabled;
-    }
-
-    // ideas from:
-    // 
http://stackoverflow.com/questions/2801116/converting-a-view-to-bitmap-without-displaying-it-in-android
-    // View has to be already displayed
-    private Bitmap getBitmapFromView(ImageView view) {
-        // Define a bitmap with the same size as the view
-        Bitmap returnedBitmap
-                = Bitmap.createBitmap(view.getWidth(), view.getHeight(), 
Bitmap.Config.ARGB_8888);
-        // Bind a canvas to it
-        Canvas canvas = new Canvas(returnedBitmap);
-        view.draw(canvas);
-        return returnedBitmap;
     }
 
     /**
@@ -225,6 +212,8 @@
                 // mathematical diagrams or animations that won't look good as 
a lead image.
                 // TODO: retrieve the MIME type of the lead image, instead of 
relying on file name.
                 leadImagesEnabled = !thumbUrl.endsWith(".gif");
+
+                // TODO: what's the harm in always setting the background to 
white unconditionally.
                 // also, if the image is not a JPG (i.e. it's a PNG or SVG) 
and might have
                 // transparency, give it a white background.
                 if (!thumbUrl.endsWith(".jpg")) {
@@ -306,6 +295,7 @@
         if (!isFragmentAdded()) {
             return;
         }
+
         int titleContainerHeight;
 
         if (isMainPage()) {
@@ -466,6 +456,57 @@
         displayHeightDp = (int) (displayHeightPx / displayDensity);
     }
 
+    private void detectFace(int bmpHeight, float aspect, @Nullable PointF 
faceLocation) {
+        int newWidth = image.getWidth();
+        int newHeight = (int) (newWidth * aspect);
+
+        // give our image an offset based on the location of the face,
+        // relative to the image container
+        float scale = (float) newHeight / (float) bmpHeight;
+        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;
+        }
+
+        // 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(LinearLayout.LayoutParams.MATCH_PARENT, 
imagePlaceholder.getHeight());
+            imageBaseYOffset = 0;
+        } else {
+            setImageLayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 
newHeight);
+        }
+
+        forceRefreshWebView();
+
+        // fade in the new image!
+        ViewAnimations.crossFade(imagePlaceholder, image);
+
+        startKenBurnsAnimation();
+    }
+
+    private void setImageLayoutParams(int width, int height) {
+        image.setLayoutParams(new LinearLayout.LayoutParams(width, height));
+    }
+
     private void loadLeadImage() {
         loadLeadImage(getLeadImageUrl());
     }
@@ -555,7 +596,6 @@
         return parentFragment.isAdded();
     }
 
-
     private float getDimension(@DimenRes int id) {
         return getResources().getDimension(id);
     }
@@ -597,64 +637,15 @@
 
     private class ImageLoadListener implements 
ImageViewWithFace.OnImageLoadListener {
         @Override
-        public void onImageLoaded(Bitmap bitmap, final PointF faceLocation) {
+        public void onImageLoaded(Bitmap bitmap, @Nullable final PointF 
faceLocation) {
             final int bmpHeight = bitmap.getHeight();
             final float aspect = (float) bitmap.getHeight() / (float) 
bitmap.getWidth();
             imageContainer.post(new Runnable() {
                 @Override
                 public void run() {
-                    if (!isFragmentAdded()) {
-                        return;
+                    if (isFragmentAdded()) {
+                        detectFace(bmpHeight, aspect, faceLocation);
                     }
-                    int newWidth = image.getWidth();
-                    int newHeight = (int) (newWidth * aspect);
-
-                    // give our image an offset based on the location of the 
face,
-                    // relative to the image container
-                    float scale = (float) newHeight / (float) bmpHeight;
-                    if (faceLocation.y > 0.0f) {
-                        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*...
-                        final int faceBoost = 24;
-                        imageBaseYOffset -= (faceBoost * displayDensity);
-                        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;
-                    }
-
-                    // 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.
-                        image.setLayoutParams(
-                                new 
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
-                                        imagePlaceholder.getHeight()));
-                        imageBaseYOffset = 0;
-                    } else {
-                        image.setLayoutParams(
-                                new 
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
-                                        newHeight));
-                    }
-
-                    forceRefreshWebView();
-
-                    // fade in the new image!
-                    ViewAnimations.crossFade(imagePlaceholder, image);
-
-                    startKenBurnsAnimation();
                 }
             });
         }
diff --git a/app/src/main/res/layout/fragment_page.xml 
b/app/src/main/res/layout/fragment_page.xml
index 2f25146..ffe3c46 100644
--- a/app/src/main/res/layout/fragment_page.xml
+++ b/app/src/main/res/layout/fragment_page.xml
@@ -67,7 +67,6 @@
                             android:layout_width="match_parent"
                             android:layout_height="wrap_content"
                             style="@style/RtlAwareTextView"
-                            android:fontFamily="serif"
                             android:paddingTop="16dp"
                             
android:paddingRight="@dimen/activity_horizontal_margin"
                             
android:paddingLeft="@dimen/activity_horizontal_margin"/>
@@ -78,7 +77,6 @@
                             style="@style/RtlAwareTextView"
                             android:layout_gravity="bottom"
                             android:visibility="invisible"
-                            android:fontFamily="serif"
                             android:textSize="@dimen/descriptionTextSize"
                             android:paddingBottom="16dp"
                             
android:paddingRight="@dimen/activity_horizontal_margin"
diff --git a/app/src/main/res/values/dimens.xml 
b/app/src/main/res/values/dimens.xml
index 90de7fe..e2f72c8 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -54,4 +54,6 @@
     <dimen name="floating_action_button_margin_right">8dp</dimen>
     <dimen name="floating_action_button_margin_bottom">0dp</dimen>
 
-</resources>
+    <dimen name="face_detection_nose_y_offset">-24dp</dimen>
+
+</resources>
\ No newline at end of file

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

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

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

Reply via email to