ImageView escalado TOP_CROP

88

Tengo un ImageViewque muestra un png que tiene una relación de aspecto más grande que la del dispositivo (verticalmente hablando, lo que significa que es más largo). Quiero mostrar esto manteniendo la relación de aspecto, haciendo coincidir el ancho del padre y fijando la vista de la imagen en la parte superior de la pantalla.

El problema que tengo con el uso CENTER_CROPcomo tipo de escala es que (comprensible) centrará la imagen escalada en lugar de alinear el borde superior con el borde superior de la vista de imagen.

El problema FIT_STARTes que la imagen se ajustará a la altura de la pantalla y no llenará el ancho.

He resuelto este problema utilizando un ImageView personalizado y anulando onDraw(Canvas) y manejando esto manualmente usando el lienzo; el problema con este enfoque es que 1) me preocupa que pueda haber una solución más simple, 2) super(AttributeSet)obtengo una excepción VM mem cuando llamo al constructor cuando intento establecer un src img de 330kb cuando el montón tiene 3 mb libres (con un tamaño de pila de 6 mb) y no puedo entender por qué.

Cualquier idea / sugerencia / solución es bienvenida :)

Gracias

PD: pensé que una solución podría ser usar un tipo de escala matricial y hacerlo yo mismo, ¡pero parece ser el mismo o más trabajo que mi solución actual!

Dori
fuente
1
¿Intentó con CENTER_CROP y estableció la propiedad AdjustViewBounds como verdadera con ImageView?
PravinCG
2
Sí, lo he intentado gracias, no tuve éxito, me temo, ya que expandirá la vista hasta que se amplíe su padre, que no será más grande que la pantalla, y luego centrará la imagen en la pantalla con el exceso de altura / 2 sobresaliendo de la parte superior y abajo
Dori

Respuestas:

84

Ok, tengo una solución que funciona. El mensaje de Darko me hizo mirar nuevamente a la clase ImageView (gracias) y aplicar la transformación usando una Matriz (como sospeché originalmente, ¡pero no tuve éxito en mi primer intento!). En mi clase imageView personalizada, llamo setScaleType(ScaleType.MATRIX)después super()en el constructor y tengo el siguiente método.

    @Override
    protected boolean setFrame(int l, int t, int r, int b)
    {
        Matrix matrix = getImageMatrix(); 
        float scaleFactor = getWidth()/(float)getDrawable().getIntrinsicWidth();    
        matrix.setScale(scaleFactor, scaleFactor, 0, 0);
        setImageMatrix(matrix);
        return super.setFrame(l, t, r, b);
    }

He colocado int en el setFrame()método como en ImageView, la llamada a configureBounds()está dentro de este método, que es donde se llevan a cabo todas las cosas de escala y matriz, por lo que me parece lógico (diga si no está de acuerdo)

A continuación se muestra el método super.setFrame () de AOSP

 @Override
    protected boolean setFrame(int l, int t, int r, int b) {
        boolean changed = super.setFrame(l, t, r, b);
        mHaveFrame = true;
        configureBounds();
        return changed;
    }

Encuentra la clase completa src aquí

Dori
fuente
¡Gracias por el código, @doridori! ¡Funcionó bien! Simplemente no entiendo por qué repitió el método "setFrame" en su explicación ... Usé solo el primero con éxito (e ignoré completamente el segundo xD)
Alesqui
3
Después de luchar con esto a través del diseño xml durante dos horas, funcionó. Ojalá pudiera darte más botes.
Mark Beaton
5
Tuve que llamar a super () antes del cuerpo, de lo contrario, la imagen no se mostraría sin un repintado
sherpya
1
@VitorHugoSchwaab tienes que usar thms como matrix.postTranslate (..)
Anton Kizema
1
¿No se puede usar el fondo? ¿Solo usar src?
Egos Zhang
43

aquí está mi código para centrarlo en la parte inferior. Por cierto. en el Código de Dori hay un pequeño error: dado que super.frame()se llama al final, el getWidth()método puede devolver un valor incorrecto. Si desea centrarlo en la parte superior, simplemente elimine la línea postTranslate y listo. Lo bueno es que con este código puedes moverlo a donde quieras. (derecha, centro => no hay problema;)

    public class CenterBottomImageView extends ImageView {

        public CenterBottomImageView(Context context) {
            super(context);
            setup();
        }

        public CenterBottomImageView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setup();
        }

        public CenterBottomImageView(Context context, AttributeSet attrs,
                int defStyle) {
            super(context, attrs, defStyle);
            setup();
        }

        private void setup() {
            setScaleType(ScaleType.MATRIX);
        }

        @Override
        protected boolean setFrame(int frameLeft, int frameTop, int frameRight, int frameBottom) {
            if (getDrawable() == null) {
                return super.setFrame(frameLeft, frameTop, frameRight, frameBottom);
            }
            float frameWidth = frameRight - frameLeft;
            float frameHeight = frameBottom - frameTop;

            float originalImageWidth = (float)getDrawable().getIntrinsicWidth();
            float originalImageHeight = (float)getDrawable().getIntrinsicHeight();

            float usedScaleFactor = 1;

            if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) {
                // If frame is bigger than image
                // => Crop it, keep aspect ratio and position it at the bottom and center horizontally

                float fitHorizontallyScaleFactor = frameWidth/originalImageWidth;
                float fitVerticallyScaleFactor = frameHeight/originalImageHeight;

                usedScaleFactor = Math.max(fitHorizontallyScaleFactor, fitVerticallyScaleFactor);
            }

            float newImageWidth = originalImageWidth * usedScaleFactor;
            float newImageHeight = originalImageHeight * usedScaleFactor;

            Matrix matrix = getImageMatrix();
            matrix.setScale(usedScaleFactor, usedScaleFactor, 0, 0); // Replaces the old matrix completly
//comment matrix.postTranslate if you want crop from TOP
            matrix.postTranslate((frameWidth - newImageWidth) /2, frameHeight - newImageHeight);
            setImageMatrix(matrix);
            return super.setFrame(frameLeft, frameTop, frameRight, frameBottom);
        }

    }
Mayor Desayuno
fuente
Genial, gracias por señalar el error. No he tocado este código durante un tiempo, por lo que puede ser una sugerencia estúpida, pero para corregir el error de setWidth que señala, ¿podría uno no usarlo (r-l)en su lugar?
Dori
¿Seguramente la línea if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight))debería invertirse? En otras palabras, ¿no debería probar si la imagen es más grande que el marco? Sugiero reemplazarlo conif((originalImageWidth > frameWidth ) || (originalImageHeight > frameHeight ))
Carlos P
((View) getParent()).getWidth()ImageViewMATCH_PARENT
Elegí el
@ Jay funciona muy bien. Estoy haciendo cálculos matriciales en el método onLayout (), que también se llama en rotación, donde setFrame no lo hace.
Box
Gracias por esto, funciona perfectamente, y también gracias por dar una manera de cambiar rápidamente el cultivo inferior al cultivo superior comentando 1 línea de código
Moonbloom
39

No es necesario que escriba una vista de imagen personalizada para obtener la TOP_CROPfuncionalidad. Solo necesita modificar el matrixde ImageView.

  1. Establezca scaleTypeen matrixpara ImageView:

    <ImageView
          android:id="@+id/imageView"
          android:contentDescription="Image"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:src="@drawable/image"
          android:scaleType="matrix"/>
    
  2. Establezca una matriz personalizada para ImageView:

    final ImageView imageView = (ImageView) findViewById(R.id.imageView);
    final Matrix matrix = imageView.getImageMatrix();
    final float imageWidth = imageView.getDrawable().getIntrinsicWidth();
    final int screenWidth = getResources().getDisplayMetrics().widthPixels;
    final float scaleRatio = screenWidth / imageWidth;
    matrix.postScale(scaleRatio, scaleRatio);
    imageView.setImageMatrix(matrix);
    

Hacer esto le dará la TOP_CROPfuncionalidad.

Enrique
fuente
2
Esto funcionó para mí. Sin embargo, tengo que verificar scaleRation si es <1, luego simplemente cambio scaleTypea, de lo centerCropcontrario, veré un espacio en blanco en los bordes.
2017
¿Cómo hacer que esto funcione para los casos en los que se necesitan imágenes alineadas en la parte inferior?
Sagar
26

Este ejemplo funciona con imágenes que se cargan después de la creación del objeto + alguna optimización. Agregué algunos comentarios en el código que explican lo que está sucediendo.

Recuerda llamar:

imageView.setScaleType(ImageView.ScaleType.MATRIX);

o

android:scaleType="matrix"

Fuente de Java:

import com.appunite.imageview.OverlayImageView;

public class TopAlignedImageView extends ImageView {
    private Matrix mMatrix;
    private boolean mHasFrame;

    @SuppressWarnings("UnusedDeclaration")
    public TopAlignedImageView(Context context) {
        this(context, null, 0);
    }

    @SuppressWarnings("UnusedDeclaration")
    public TopAlignedImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    @SuppressWarnings("UnusedDeclaration")
    public TopAlignedImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mHasFrame = false;
        mMatrix = new Matrix();
        // we have to use own matrix because:
        // ImageView.setImageMatrix(Matrix matrix) will not call
        // configureBounds(); invalidate(); because we will operate on ImageView object
    }

    @Override
    protected boolean setFrame(int l, int t, int r, int b)
    {
        boolean changed = super.setFrame(l, t, r, b);
        if (changed) {
            mHasFrame = true;
            // we do not want to call this method if nothing changed
            setupScaleMatrix(r-l, b-t);
        }
        return changed;
    }

    private void setupScaleMatrix(int width, int height) {
        if (!mHasFrame) {
            // we have to ensure that we already have frame
            // called and have width and height
            return;
        }
        final Drawable drawable = getDrawable();
        if (drawable == null) {
            // we have to check if drawable is null because
            // when not initialized at startup drawable we can
            // rise NullPointerException
            return;
        }
        Matrix matrix = mMatrix;
        final int intrinsicWidth = drawable.getIntrinsicWidth();
        final int intrinsicHeight = drawable.getIntrinsicHeight();

        float factorWidth = width/(float) intrinsicWidth;
        float factorHeight = height/(float) intrinsicHeight;
        float factor = Math.max(factorHeight, factorWidth);

        // there magic happen and can be adjusted to current
        // needs
        matrix.setTranslate(-intrinsicWidth/2.0f, 0);
        matrix.postScale(factor, factor, 0, 0);
        matrix.postTranslate(width/2.0f, 0);
        setImageMatrix(matrix);
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        // We have to recalculate image after chaning image
        setupScaleMatrix(getWidth(), getHeight());
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        // We have to recalculate image after chaning image
        setupScaleMatrix(getWidth(), getHeight());
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        // We have to recalculate image after chaning image
        setupScaleMatrix(getWidth(), getHeight());
    }

    // We do not have to overide setImageBitmap because it calls 
    // setImageDrawable method

}
Jacek Marchwicki
fuente
¿Cómo hacer que esto funcione para los casos en los que se necesitan imágenes alineadas en la parte inferior?
Sagar
13

Basado en Dori, estoy usando una solución que escala la imagen según el ancho o la altura de la imagen para llenar siempre el contenedor circundante. Esto permite escalar una imagen para llenar todo el espacio disponible usando el punto superior izquierdo de la imagen en lugar del centro como origen (CENTER_CROP):

@Override
protected boolean setFrame(int l, int t, int r, int b)
{

    Matrix matrix = getImageMatrix(); 
    float scaleFactor, scaleFactorWidth, scaleFactorHeight;
    scaleFactorWidth = (float)width/(float)getDrawable().getIntrinsicWidth();
    scaleFactorHeight = (float)height/(float)getDrawable().getIntrinsicHeight();    

    if(scaleFactorHeight > scaleFactorWidth) {
        scaleFactor = scaleFactorHeight;
    } else {
        scaleFactor = scaleFactorWidth;
    }

    matrix.setScale(scaleFactor, scaleFactor, 0, 0);
    setImageMatrix(matrix);

    return super.setFrame(l, t, r, b);
}

Espero que esto ayude, funciona como un regalo en mi proyecto.

Mate
fuente
11
Esta es la mejor solución ... Y agregue: float width = r - l; altura del flotador = b - t;
Geltrude
9

Ninguna de estas soluciones funcionó para mí, porque quería una clase que admitiera un recorte arbitrario desde la dirección horizontal o vertical, y quería que me permitiera cambiar el recorte de forma dinámica. También necesitaba compatibilidad con Picasso , y Picasso establece los dibujables de imágenes con pereza.

Mi implementación está adaptada directamente de ImageView.java en el AOSP. Para usarlo, declare así en XML:

    <com.yourapp.PercentageCropImageView
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="matrix"/>

Desde la fuente, si desea tener una cosecha superior, llame a:

imageView.setCropYCenterOffsetPct(0f);

Si desea tener un cultivo de fondo, llame a:

imageView.setCropYCenterOffsetPct(1.0f);

Si desea tener un recorte de 1/3 del camino hacia abajo, llame a:

imageView.setCropYCenterOffsetPct(0.33f);

Además, si elige utilizar otro método de recorte, como fit_center, puede hacerlo y no se activará ninguna de esta lógica personalizada. (Otras implementaciones SOLAMENTE le permiten usar sus métodos de recorte).

Por último, agregué un método, redraw (), por lo que si elige cambiar su método de recorte / scaleType dinámicamente en el código, puede forzar que la vista se vuelva a dibujar. Por ejemplo:

fullsizeImageView.setScaleType(ScaleType.FIT_CENTER);
fullsizeImageView.redraw();

Para volver a su tercer cultivo personalizado en la parte superior central, llame a:

fullsizeImageView.setScaleType(ScaleType.MATRIX);
fullsizeImageView.redraw();

Aquí está la clase:

/* 
 * Adapted from ImageView code at: 
 * http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/android/widget/ImageView.java
 */
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;

public class PercentageCropImageView extends ImageView{

    private Float mCropYCenterOffsetPct;
    private Float mCropXCenterOffsetPct;

    public PercentageCropImageView(Context context) {
        super(context);
    }

    public PercentageCropImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public PercentageCropImageView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    public float getCropYCenterOffsetPct() {
        return mCropYCenterOffsetPct;
    }

    public void setCropYCenterOffsetPct(float cropYCenterOffsetPct) {
        if (cropYCenterOffsetPct > 1.0) {
            throw new IllegalArgumentException("Value too large: Must be <= 1.0");
        }
        this.mCropYCenterOffsetPct = cropYCenterOffsetPct;
    }

    public float getCropXCenterOffsetPct() {
        return mCropXCenterOffsetPct;
    }

    public void setCropXCenterOffsetPct(float cropXCenterOffsetPct) {
        if (cropXCenterOffsetPct > 1.0) {
            throw new IllegalArgumentException("Value too large: Must be <= 1.0");
        }
        this.mCropXCenterOffsetPct = cropXCenterOffsetPct;
    }

    private void myConfigureBounds() {
        if (this.getScaleType() == ScaleType.MATRIX) {
            /*
             * Taken from Android's ImageView.java implementation:
             * 
             * Excerpt from their source:
    } else if (ScaleType.CENTER_CROP == mScaleType) {
       mDrawMatrix = mMatrix;

       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;
       }

       mDrawMatrix.setScale(scale, scale);
       mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
    }
             */

            Drawable d = this.getDrawable();
            if (d != null) {
                int dwidth = d.getIntrinsicWidth();
                int dheight = d.getIntrinsicHeight();

                Matrix m = new Matrix();

                int vwidth = getWidth() - this.getPaddingLeft() - this.getPaddingRight();
                int vheight = getHeight() - this.getPaddingTop() - this.getPaddingBottom();

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

                if (dwidth * vheight > vwidth * dheight) {
                    float cropXCenterOffsetPct = mCropXCenterOffsetPct != null ? 
                            mCropXCenterOffsetPct.floatValue() : 0.5f;
                    scale = (float) vheight / (float) dheight;
                    dx = (vwidth - dwidth * scale) * cropXCenterOffsetPct;
                } else {
                    float cropYCenterOffsetPct = mCropYCenterOffsetPct != null ? 
                            mCropYCenterOffsetPct.floatValue() : 0f;

                    scale = (float) vwidth / (float) dwidth;
                    dy = (vheight - dheight * scale) * cropYCenterOffsetPct;
                }

                m.setScale(scale, scale);
                m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));

                this.setImageMatrix(m);
            }
        }
    }

    // These 3 methods call configureBounds in ImageView.java class, which
    // adjusts the matrix in a call to center_crop (android's built-in 
    // scaling and centering crop method). We also want to trigger
    // in the same place, but using our own matrix, which is then set
    // directly at line 588 of ImageView.java and then copied over
    // as the draw matrix at line 942 of ImageVeiw.java
    @Override
    protected boolean setFrame(int l, int t, int r, int b) {
        boolean changed = super.setFrame(l, t, r, b);
        this.myConfigureBounds();
        return changed;
    }
    @Override
    public void setImageDrawable(Drawable d) {          
        super.setImageDrawable(d);
        this.myConfigureBounds();
    }
    @Override
    public void setImageResource(int resId) {           
        super.setImageResource(resId);
        this.myConfigureBounds();
    }

    public void redraw() {
        Drawable d = this.getDrawable();

        if (d != null) {
            // Force toggle to recalculate our bounds
            this.setImageDrawable(null);
            this.setImageDrawable(d);
        }
    }
}
esilver
fuente
5

Tal vez vaya al código fuente para la vista de imagen en Android y vea cómo dibuja el recorte central, etc. y tal vez copie algo de ese código en sus métodos. Realmente no conozco una mejor solución que hacer esto. Tengo experiencia en cambiar el tamaño y recortar manualmente el mapa de bits (búsqueda de transformaciones de mapa de bits), lo que reduce su tamaño real, pero aún crea un poco de sobrecarga en el proceso.

DArkO
fuente
3
public class ImageViewTopCrop extends ImageView {
public ImageViewTopCrop(Context context) {
    super(context);
    setScaleType(ScaleType.MATRIX);
}

public ImageViewTopCrop(Context context, AttributeSet attrs) {
    super(context, attrs);
    setScaleType(ScaleType.MATRIX);
}

public ImageViewTopCrop(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    setScaleType(ScaleType.MATRIX);
}

@Override
protected boolean setFrame(int l, int t, int r, int b) {
    computMatrix();
    return super.setFrame(l, t, r, b);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    computMatrix();
}

private void computMatrix() {
    Matrix matrix = getImageMatrix();
    float scaleFactor = getWidth() / (float) getDrawable().getIntrinsicWidth();
    matrix.setScale(scaleFactor, scaleFactor, 0, 0);
    setImageMatrix(matrix);
}

}

tianxia
fuente
setFrame && onLayout
tianxia
computMatrix: puedes hacer cualquier matriz aquí.
Tianxia
onLayoutme salva mucho! ¡Gracias! Encontré un problema en el que calcula la matriz pero no muestra la imagen inmediatamente y agregar onLayoutel código resuelve mi problema.
natsumiyu
1

Si está utilizando Fresco (SimpleDraweeView) , puede hacerlo fácilmente con:

 PointF focusPoint = new PointF(0.5f, 0f);
 imageDraweeView.getHierarchy().setActualImageFocusPoint(focusPoint);

Este sería para una cosecha superior.

Más información en Enlace de referencia

fxhereng
fuente
0

Hay 2 problemas con las soluciones aquí:

  • No se procesan en el editor de diseño de Android Studio (por lo que puede obtener una vista previa en varios tamaños de pantalla y relaciones de aspecto)
  • Solo se escala por ancho, por lo que, dependiendo de las relaciones de aspecto del dispositivo y la imagen, puede terminar con una tira vacía en la parte inferior.

Esta pequeña modificación soluciona el problema (coloque el código en onDraw y verifique los factores de escala de ancho y alto):

@Override
protected void onDraw(Canvas canvas) {

    Matrix matrix = getImageMatrix();

    float scaleFactorWidth = getWidth() / (float) getDrawable().getIntrinsicWidth();
    float scaleFactorHeight = getHeight() / (float) getDrawable().getIntrinsicHeight();

    float scaleFactor = (scaleFactorWidth > scaleFactorHeight) ? scaleFactorWidth : scaleFactorHeight;

    matrix.setScale(scaleFactor, scaleFactor, 0, 0);
    setImageMatrix(matrix);

    super.onDraw(canvas);
}
OldSchool4664
fuente
-1

Solución más simple: recorta la imagen

 @Override
    public void draw(Canvas canvas) {
        if(getWidth() > 0){
            int clipHeight = 250;
            canvas.clipRect(0,clipHeight,getWidth(),getHeight());
         }
        super.draw(canvas);
    }
M. Usman Khan
fuente
Esto no aumentará la escala de la imagen si es más pequeña que la vista, por lo que las otras soluciones no son tan simples.
OldSchool4664