javaandroidandroid-layoutandroid-imageviewbackground-drawable

How to have a Fullscreen Background Image (with center-crop) that doesn't Resize


I'm trying to set a photo as the background of an Activity. I want the background to be a full screen image (no borders).

As I want the image to fill the entire activity's background without stretching/squeezing (i.e. unproportional scalings for X and Y) and I don't mind if the photo has to be cropped, I'm using a RelativeLayout with an ImageView (with android:scaleType="centerCrop") and the rest of my layout consisting of a ScrollView and its children.

<!-- Tried this with a FrameLayout as well... -->
<RelativeLayout> 
   <!-- Background -->
   <ImageView  
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:scaleType="centerCrop"/>
   <!-- Form -->
   <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <ScrollView>
        <LinearLayout>
          ...
          <EditText/>
        </LinearLayout>
      </ScrollView>
   </LinearLayout>
</RelativeLayout>

The problem is that the rest of the layout has some EditText views and when the softkeyboard shows up, the ImageView gets re-sized. I would like the background to remain the same, irregardless of whether the softkeyboard is visible or not.

I have seen plenty of questions on SO about ImageViews being re-sized but (imo) no satisfactory answers. Most of them just consist of setting the android:windowSoftInputMode="adjustPan" - which is not always practical, especially if you want the user to be able to scroll in the activity - or using getWindow().setBackgroundDrawable() which doesn't crop the image.

I've managed to sub-class ImageView and override its onMeasure() (see my answer here: ImageView resizes when keyboard open) so that it can force a fixed height & width - equal to the device's screen dimensions - according to a flag but I'm wondering if there's a better way of achieving the result I want.

So, to sum up, my question is: How can I set an Activity's background to be a full-screen photo


ANSWER:

I ended up following @pskink's advice and subclassed BitmapDrawable (see his answer bellow). I had to do some adjustments to make sure that the BackgroundBitmapDrawable is always scaled and cropped in a way that fills the screen.

Here's my final class, adapted from his answer:

public class BackgroundBitmapDrawable extends BitmapDrawable {
    private Matrix mMatrix = new Matrix();
    private int moldHeight;
    boolean simpleMapping = false;

    public BackgroundBitmapDrawable(Resources res, Bitmap bitmap) {
        super(res, bitmap);
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        if (bounds.height() > moldHeight) {
            moldHeight = bounds.height();
            Bitmap b = getBitmap();
            RectF src = new RectF(0, 0, b.getWidth(), b.getHeight());
            RectF dst;

            if (simpleMapping) {
                dst = new RectF(bounds);
                mMatrix.setRectToRect(src, dst, ScaleToFit.CENTER);
            } else {
                // Full Screen Image -> Always scale and center-crop in order to fill the screen
                float dwidth = src.width();
                float dheight = src.height();

                float vwidth = bounds.width(); 
                float vheight = bounds.height();

                float scale;
                float dx = 0, dy = 0;

                if (dwidth * vheight > vwidth * dheight) {
                    scale = (float) vheight / (float) dheight; 
                    dx = (vwidth - dwidth * scale) * 0.5f;
                } else {
                    scale = (float) vwidth / (float) dwidth;
                    dy = (vheight - dheight * scale) * 0.5f;
                }

                mMatrix.setScale(scale, scale);
                mMatrix.postTranslate(dx, dy);

            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawColor(0xaa00ff00);
        canvas.drawBitmap(getBitmap(), mMatrix, null);
    }
}

Then its just a matter of creating a BackgroundBitmapDrawable and setting it as the root View's background.


Solution

  • try this LayerDrawable (res/drawable/backlayer.xml):

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    >
        <item>
            <shape>
            <solid android:color="#f00" />
            </shape>
        </item>
    <item>
            <bitmap
            android:gravity="top" android:src="@drawable/back1">
            </bitmap>
        </item>
    </layer-list>
    

    and set it to your top level layout: android:background="@drawable/backlayer"

    UPDATE: try this BitmapDrawadle, set it to top level layout (setBackgroundDrawable()), if simpleMapping == true is good enough you can remove "else" branch:

    class D extends BitmapDrawable {
        private Matrix mMatrix = new Matrix();
        private int moldHeight;
    
        public D(Resources res, Bitmap bitmap) {
            super(res, bitmap);
        }
    
        @Override
        protected void onBoundsChange(Rect bounds) {
            if (bounds.height() > moldHeight) {
                moldHeight = bounds.height();
                Bitmap b = getBitmap();
                RectF src = new RectF(0, 0, b.getWidth(), b.getHeight());
                RectF dst;
    
                // if simpleMapping is good enough then remove "else" branch and
                // declare "dst" as:
                // RectF dst = new RectF(bounds);
                boolean simpleMapping = true;
                if (simpleMapping) {
                    dst = new RectF(bounds);
                } else {
                    float x = bounds.exactCenterX();
                    dst = new RectF(x, 0, x, bounds.height());
                    float scale = bounds.height() / src.height();
                    float dx = scale * src.width() / 2;
                    dst.inset(-dx, 0);
                }
                mMatrix.setRectToRect(src, dst, ScaleToFit.CENTER);
            }
        }
    
        @Override
        public void draw(Canvas canvas) {
            canvas.drawColor(0xaa00ff00);
            canvas.drawBitmap(getBitmap(), mMatrix, null);
        }
    }