com.manuelpeinado.imagelayout.ImageLayout.java Source code

Java tutorial

Introduction

Here is the source code for com.manuelpeinado.imagelayout.ImageLayout.java

Source

/*
 * Copyright (C) 2013 Manuel Peinado
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * Changed by Tal Sapan on May 2015 - allow to use any type of image that can draw itself
 * instead of only Bitmap. Specific support for Picture was added.
 * This version is based on ImageLayout (https://github.com/ManuelPeinado/ImageLayout) version 1.1.0 from May 2015.
 */
package com.manuelpeinado.imagelayout;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Picture;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;

import amai.org.conventions.R;

/**
 * A layout that arranges its children in relation to a background image. The
 * layout of each child is specified in image coordinates (pixels), and the
 * conversion to screen coordinates is performed automatically.
 * <p>The background image is adjusted so that it fills the available space. The exact
 * details of this adjustment are controlled by the custom:fit and android:gravity 
 * attributes
 * <p>For some applications this might be a useful replacement for the now
 * deprecated AbsoluteLayout.
 */
public class ImageLayout extends ViewGroup {

    /**
     * The image is made to fill the available vertical space, and may be cropped 
     * horizontally if there is not enough space. 
     * <p>If there is too much horizontal space it is left blank. 
     * <p>The vertical position of the image is controlled by the android:gravity attribute
     */
    public static final int FIT_VERTICAL = 0;
    /**
     * The image is made to fill the available horizontal space, and may be cropped 
     * vertically if there is not enough space. 
     * <p>If there is too much vertical space it is left blank. 
     * <p>The vertical position of the image is controlled by the android:gravity attribute
     */
    public static final int FIT_HORIZONTAL = 1;

    /**
     * The image fills the available space both vertically and horizontally. 
     * <p>If the aspect ratio of the image does not match exactly the aspect ratio
     * of the available space, the image is cropped either vertically or horizontally,
     * depending on which provides the best fit
     */
    public static final int FIT_BOTH = 2;

    /**
     * The image is made to fill the available space vertically in portrait mode
     * and horizontally in landscape. 
     * <p>This is the default mode.
     * <p>Note that the library does not determine the orientation based on the 
     * actual device orientation, but on the relative aspect ratios of the image
     * and the view.
     */
    public static final int FIT_AUTO = 3;

    /**
     * The fit mode that will be used in case the user does not specify one
     */
    public static final int DEFAULT_FIT_MODE = FIT_AUTO;

    private ImageResource image;
    private Rect destRect;
    private float imageWidth;
    private float imageHeight;
    private Rect srcRect;
    private ImageFitter fitter;
    private int fitMode = DEFAULT_FIT_MODE;
    private int gravity = -1;

    public ImageLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setWillNotDraw(false);
        parseAttributes(attrs);
    }

    private void parseAttributes(AttributeSet attrs) {
        if (attrs == null) {
            return;
        }
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ImageLayout);
        Drawable drawable = a.getDrawable(R.styleable.ImageLayout_image);
        if (drawable != null) {
            if (!(drawable instanceof BitmapDrawable) && !(drawable instanceof PictureDrawable)) {
                throw new RuntimeException(
                        "Drawable resource in layout description file must be of type \"BitmapDrawable\" or \"PictureDrawable\"");
            }

            image = getImageResourceFromDrawable(drawable);
            srcRect = imageRect(image);
        }

        imageWidth = a.getInteger(R.styleable.ImageLayout_imageWidth, -1);
        imageHeight = a.getInteger(R.styleable.ImageLayout_imageHeight, -1);

        int fitMode = a.getInt(R.styleable.ImageLayout_fit, this.fitMode);
        setFitMode(fitMode);

        int gravity = a.getInt(R.styleable.ImageLayout_android_gravity, this.gravity);
        setGravity(gravity);
        a.recycle();
    }

    /**
     * Determines how the background image is drawn
     * @param newValue Accepted values are {@link ImageLayout#FIT_BOTH}, {@link ImageLayout#FIT_AUTO},
     *        {@link ImageLayout#FIT_VERTICAL} and {@link ImageLayout#FIT_HORIZONTAL} 
     */
    public void setFitMode(int newValue) {
        if (fitter != null && fitMode == newValue) {
            return;
        }
        fitMode = newValue;
        rebuildFitter();
    }

    public void setGravity(int newValue) {
        if (fitter != null && gravity == newValue) {
            return;
        }
        if ((newValue & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
            newValue |= Gravity.CENTER_HORIZONTAL;
        }
        if ((newValue & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
            newValue |= Gravity.CENTER_VERTICAL;
        }
        gravity = newValue;
        rebuildFitter();
    }

    public int getFitMode() {
        return fitMode;
    }

    /**
     * Changes the background image and its layout dimensions.
     */
    public void setImageResource(int imageResource, float imageWidth, float imageHeight) {
        setImageResourceFromDrawable(ContextCompat.getDrawable(getContext(), imageResource), imageWidth,
                imageHeight);
    }

    public void setImageResourceFromDrawable(Drawable image, float imageWidth, float imageHeight) {
        setImageResource(getImageResourceFromDrawable(image), imageWidth, imageHeight);
    }

    private ImageResource getImageResourceFromDrawable(Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return new BitmapImageResource(((BitmapDrawable) drawable).getBitmap());
        } else if (drawable instanceof PictureDrawable) {
            return new PictureImageResource(((PictureDrawable) drawable).getPicture());
        }
        throw new RuntimeException("Drawable must be of type \"BitmapDrawable\" or \"PictureDrawable\"");
    }

    public void setImageResource(ImageResource image, float imageWidth, float imageHeight) {
        this.image = image;
        srcRect = imageRect(image);

        this.imageWidth = imageWidth;
        this.imageHeight = imageHeight;

        rebuildFitter();
    }

    private static Rect imageRect(ImageResource image) {
        return new Rect(0, 0, image.getWidth(), image.getHeight());
    }

    private void rebuildFitter() {
        fitter = new ImageFitter(fitMode, gravity);
        requestLayout();
        invalidate();
    }

    private int transformWidthFromImageToView(float w) {
        float widthRatio = destRect.width() / imageWidth;
        return Math.round(w * widthRatio);
    }

    private int transformHeightFromImageToView(float h) {
        float heightRatio = destRect.height() / imageHeight;
        return Math.round(h * heightRatio);
    }

    private int transformXFromImageToView(float x) {
        float widthRatio = destRect.width() / imageWidth;
        return destRect.left + Math.round(x * widthRatio);
    }

    private int transformYFromImageToView(float y) {
        float heightRatio = destRect.height() / imageHeight;
        return destRect.top + Math.round(y * heightRatio);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (image != null) {
            image.draw(canvas, srcRect, destRect);
        }
        super.onDraw(canvas);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int N = getChildCount();
        for (int i = 0; i < N; ++i) {
            View child = getChildAt(i);
            LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
            child.layout(layoutParams.transformedRect.left, layoutParams.transformedRect.top,
                    layoutParams.transformedRect.right, layoutParams.transformedRect.bottom);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
        int width = widthSpec;
        int heightSpec = MeasureSpec.getSize(heightMeasureSpec);
        int height = heightSpec;
        boolean isExactWidth = widthMode == MeasureSpec.EXACTLY;
        boolean isExactHeight = heightMode == MeasureSpec.EXACTLY;
        int imageWidth = image == null ? 1 : image.getWidth();
        int imageHeight = image == null ? 1 : image.getHeight();
        float imageAspectRatio = (imageWidth + getPaddingLeft() + getPaddingRight())
                / ((float) imageHeight + getPaddingTop() + getPaddingBottom());
        if (isExactWidth && !isExactHeight) {
            height = (int) (width / imageAspectRatio);
            if (heightMode == MeasureSpec.AT_MOST && height > heightSpec) {
                height = heightSpec;
            }
        } else if (isExactHeight && !isExactWidth) {
            ViewGroup parent = (ViewGroup) getParent();
            int parentWidth = parent.getMeasuredWidth();
            switch (getLayoutParams().width) {
            case ViewGroup.LayoutParams.MATCH_PARENT:
                width = parentWidth;
                break;
            default:
                // If we have WRAP_CONTENT we want to make the width at least the available
                // space in the parent so it will appear centered (it could be more to keep the aspect ratio).
                // This is a workaround for this view being inside HorizontalScrollView (because it cannot be
                // centered there).
                width = Math.max(parentWidth, (int) (height * imageAspectRatio));
                break;
            }

            if (widthMode == MeasureSpec.AT_MOST && width > widthSpec) {
                width = widthSpec;
            }
        }

        setMeasuredDimension(width, height);

        int effectiveWidth = width - getPaddingLeft() - getPaddingRight();
        int effectiveHeight = height - getPaddingTop() - getPaddingBottom();
        destRect = fitter.fit(image, effectiveWidth, effectiveHeight);
        adjustDestRectForPadding();

        int N = getChildCount();
        for (int i = 0; i < N; ++i) {
            View child = getChildAt(i);
            measureChild(child);
        }
    }

    private void measureChild(View child) {
        LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
        checkChildLayoutParams(layoutParams);
        int wspec = makeWidthSpec(layoutParams);
        int hspec = makeHeightSpec(layoutParams);
        child.measure(wspec, hspec);
        int left = 0, width = child.getMeasuredWidth();
        if (layoutParams.left != -1) {
            left = transformXFromImageToView(layoutParams.left);
            if (layoutParams.right != -1) {
                int right = transformXFromImageToView(layoutParams.right);
                width = right - left;
            }
        } else if (layoutParams.right != -1) {
            int right = transformXFromImageToView(layoutParams.right);
            left = right - width;
        } else if (layoutParams.centerX != -1) {
            int cx = transformXFromImageToView(layoutParams.centerX);
            left = cx - width / 2;
        }

        int top = 0, height = child.getMeasuredHeight();
        if (layoutParams.top != -1) {
            top = transformYFromImageToView(layoutParams.top);
            if (layoutParams.bottom != -1) {
                int bottom = transformYFromImageToView(layoutParams.bottom);
                height = bottom - top;
            }
        } else if (layoutParams.bottom != -1) {
            int bottom = transformYFromImageToView(layoutParams.bottom);
            top = bottom - height;
        } else if (layoutParams.centerY != -1) {
            int cy = transformYFromImageToView(layoutParams.centerY);
            top = cy - height / 2;
        }
        layoutParams.transformedRect.set(left, top, left + width, top + height);
    }

    private void adjustDestRectForPadding() {
        destRect.left += getPaddingLeft();
        destRect.right += getPaddingLeft();
        destRect.top += getPaddingTop();
        destRect.bottom += getPaddingTop();
    }

    private int makeHeightSpec(LayoutParams layoutParams) {
        int hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        if (layoutParams.maxHeight != -1) {
            int height = transformHeightFromImageToView(layoutParams.maxHeight);
            hspec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
        } else if (layoutParams.height != LayoutParams.WRAP_CONTENT) {
            int height = transformHeightFromImageToView(layoutParams.height);
            ((ViewGroup.LayoutParams) layoutParams).height = height;
            hspec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        }
        return hspec;
    }

    private int makeWidthSpec(LayoutParams layoutParams) {
        int wspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        if (layoutParams.maxWidth != -1) {
            int maxWidth = transformWidthFromImageToView(layoutParams.maxWidth);
            wspec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
        } else if (layoutParams.width != LayoutParams.WRAP_CONTENT) {
            int width = transformWidthFromImageToView(layoutParams.width);
            ((ViewGroup.LayoutParams) layoutParams).width = width;
            wspec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        }
        return wspec;
    }

    private void checkChildLayoutParams(LayoutParams layoutParams) {
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        // In image coords
        public float maxWidth = -1, maxHeight = -1;
        public float left = -1, top = -1, right = -1, bottom = -1;
        public float centerX = -1, centerY = -1;
        public float width = -1, height = -1;
        // In view coords
        Rect transformedRect = new Rect();

        public LayoutParams() {
            this(null, null);
        }

        public LayoutParams(Context c, AttributeSet attrs) {
            // We don't call super(c, attrs) to prevent the layout_width and
            // layout_height
            // attributes from being mandatory
            super(WRAP_CONTENT, WRAP_CONTENT);

            if (c == null || attrs == null) {
                return;
            }

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ImageLayout_Layout);

            int N = a.getIndexCount();
            for (int i = 0; i < N; i++) {
                int attr = a.getIndex(i);
                switch (attr) {
                case R.styleable.ImageLayout_Layout_layout_left:
                    left = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_top:
                    top = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_right:
                    right = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_bottom:
                    bottom = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_centerX:
                    centerX = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_centerY:
                    centerY = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_width:
                    width = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_maxWidth:
                    maxWidth = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_height:
                    height = a.getInt(attr, -1);
                    break;
                case R.styleable.ImageLayout_Layout_layout_maxHeight:
                    maxHeight = a.getInt(attr, -1);
                    break;
                }
            }
            a.recycle();
        }

        LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams();
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    public interface ImageResource {
        int getHeight();

        int getWidth();

        void draw(Canvas canvas, Rect srcRect, Rect destRect);
    }

    public class BitmapImageResource implements ImageResource {
        private Bitmap bitmap;

        public BitmapImageResource(Bitmap bitmap) {
            this.bitmap = bitmap;
        }

        @Override
        public int getHeight() {
            return bitmap.getHeight();
        }

        @Override
        public int getWidth() {
            return bitmap.getWidth();
        }

        @Override
        public void draw(Canvas canvas, Rect srcRect, Rect destRect) {
            canvas.drawBitmap(bitmap, srcRect, destRect, null);
        }
    }

    public class PictureImageResource implements ImageResource {
        private Picture picture;

        public PictureImageResource(Picture picture) {
            this.picture = picture;
        }

        @Override
        public int getHeight() {
            return picture.getHeight();
        }

        @Override
        public int getWidth() {
            return picture.getWidth();
        }

        @Override
        public void draw(Canvas canvas, Rect srcRect, Rect destRect) {
            canvas.drawPicture(picture, destRect);
        }
    }
}