Mise à l'échelle ImageView TOP_CROP


88

J'ai un ImageViewqui affiche un png qui a un rapport hauteur / largeur plus grand que celui de l'appareil (verticalement, ce qui signifie qu'il est plus long). Je veux l'afficher tout en conservant le rapport hauteur / largeur, en faisant correspondre la largeur du parent et en épinglant la vue d'image en haut de l'écran.

Le problème que j'ai avec l'utilisation CENTER_CROPcomme type d'échelle est qu'il va (compréhensible) centrer l'image mise à l'échelle au lieu d'aligner le bord supérieur sur le bord supérieur de la vue de l'image.

Le problème avec FIT_STARTest que l'image s'adaptera à la hauteur de l'écran et ne remplira pas la largeur.

J'ai résolu ce problème en utilisant un ImageView personnalisé onDraw(Canvas)et en le remplaçant et en le gérant manuellement à l'aide du canevas; le problème avec cette approche est que 1) je crains qu'il n'y ait une solution plus simple, 2) j'obtiens une exception VM mem lors de l'appel super(AttributeSet)du constructeur en essayant de définir une img src de 330kb lorsque le tas a 3 mb libres (avec une taille de tas de 6 Mo) et je ne peux pas comprendre pourquoi.

Toutes les idées / suggestions / solutions sont les bienvenues :)

Merci

ps je pensais qu'une solution pourrait être d'utiliser un type d'échelle matricielle et de le faire moi-même, mais cela semble être le même ou plus de travail que ma solution actuelle!


1
Avez-vous essayé avec CENTER_CROP et défini la propriété AdjustViewBounds sur true avec ImageView?
PravinCG

2
Oui, j'ai essayé cela, pas de succès, j'ai peur car il élargira la vue jusqu'à ce qu'il élargisse son parent, qui ne sera pas plus grand que l'écran, puis centrera l'image sur l'écran avec la hauteur excessive / 2 poussant vers le haut et bas
Dori

Réponses:


84

Ok, j'ai une solution de travail. L'invite de Darko m'a fait revoir la classe ImageView (merci) et j'ai appliqué la transformation à l'aide d'une matrice (comme je le soupçonnais à l'origine, mais je n'ai pas réussi à ma première tentative!). Dans ma classe imageView personnalisée, j'appelle setScaleType(ScaleType.MATRIX)après super()dans le constructeur et j'ai la méthode suivante.

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

J'ai placé int dans la setFrame()méthode comme dans ImageView, l'appel à configureBounds()est dans cette méthode, qui est l'endroit où se déroule toute la mise à l'échelle et la matrice, cela me semble donc logique (disons si vous n'êtes pas d'accord)

Vous trouverez ci-dessous la méthode super.setFrame () d'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;
    }

Trouvez la classe complète src ici


Merci pour le code, @doridori! Cela a bien fonctionné! Je ne comprends tout simplement pas pourquoi avez-vous répété la méthode "setFrame" dans votre explication ... J'ai utilisé juste la première avec succès (et j'ai complètement ignoré le deuxième xD)
Alesqui

3
Après avoir combattu avec cela via une mise en page XML pendant deux heures, cela a fonctionné. J'aimerais pouvoir vous donner plus de upboats.
Mark Beaton

5
J'ai dû appeler super () avant le corps, sinon l'image ne s'afficherait pas sans repeindre
sherpya

1
@VitorHugoSchwaab vous devez utiliser des thms comme matrix.postTranslate (..)
Anton Kizema

1
ne peut pas utiliser l'arrière-plan? utiliser uniquement src?
Egos Zhang

43

voici mon code pour le centrer en bas. Btw. dans Dori's Code, il y a un petit bogue: puisque le super.frame()est appelé à la toute fin, la getWidth()méthode peut renvoyer une valeur incorrecte. Si vous voulez le centrer en haut, supprimez simplement la ligne postTranslate et vous avez terminé. La bonne chose est qu'avec ce code, vous pouvez le déplacer où vous le souhaitez. (droite, centre => pas de problème;)

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

    }

Super, merci d'avoir signalé le bogue. Je n'ai pas touché à ce code depuis un moment, donc cela peut être une suggestion stupide, mais pour corriger le bogue setWidth que vous indiquez, ne pourrait-on pas simplement utiliser à la (r-l)place?
Dori

La ligne if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight))devrait sûrement être inversée? En d'autres termes, ne devriez-vous pas tester si l'image est plus grande que le cadre? Je suggère de le remplacer parif((originalImageWidth > frameWidth ) || (originalImageHeight > frameHeight ))
Carlos P

J'ai choisi la largeur du parent en utilisant ((View) getParent()).getWidth()depuis le ImageViewhasMATCH_PARENT
sherpya

@Jay ça marche très bien. Je fais des calculs matriciels dans la méthode onLayout (), qui est également appelée lors de la rotation, contrairement à setFrame.
boîte du

Merci pour cela, fonctionne parfaitement, et merci aussi d'avoir donné un moyen de changer rapidement le recadrage du bas en recadrage supérieur en commentant 1 ligne de code
Moonbloom

39

Vous n'avez pas besoin d'écrire une vue d'image personnalisée pour obtenir la TOP_CROPfonctionnalité. Vous avez juste besoin de modifier le matrixdu ImageView.

  1. Définissez le scaleTypesur matrixpour 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. Définissez une matrice personnalisée pour 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);

Cela vous donnera la TOP_CROPfonctionnalité.


2
Cela a fonctionné pour moi. Cependant, je dois vérifier scaleRation si elle est <1, puis changez simplement le scaleTypeen centerCropsinon je verrai un espace vide sur les bords.
Arst

Comment faire fonctionner cela pour les cas où il y a un besoin d'images alignées en bas?
Sagar

26

Cet exemple fonctionne avec des images chargées après la création de l'objet + une certaine optimisation. J'ai ajouté quelques commentaires dans le code qui expliquent ce qui se passe.

N'oubliez pas d'appeler:

imageView.setScaleType(ImageView.ScaleType.MATRIX);

ou

android:scaleType="matrix"

Source 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

}

Comment faire fonctionner cela pour les cas où il y a un besoin d'images alignées en bas?
Sagar

13

Basé sur Dori, j'utilise une solution qui redimensionne l'image en fonction de la largeur ou de la hauteur de l'image pour toujours remplir le conteneur environnant. Cela permet de mettre à l'échelle une image pour remplir tout l'espace disponible en utilisant le point supérieur gauche de l'image plutôt que le centre comme origine (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);
}

J'espère que cela aide - fonctionne comme un régal dans mon projet.


11
C'est la meilleure solution ... Et ajoutez: float width = r - l; hauteur du flotteur = b - t;
Geltrude

9

Aucune de ces solutions n'a fonctionné pour moi, car je voulais une classe qui supporte un recadrage arbitraire dans la direction horizontale ou verticale, et je voulais qu'elle me permette de modifier le recadrage de manière dynamique. J'avais également besoin de la compatibilité Picasso , et Picasso définit paresseusement les dessinables d'image.

Mon implémentation est adaptée directement depuis ImageView.java dans l'AOSP. Pour l'utiliser, déclarez comme ceci en XML:

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

Depuis la source, si vous souhaitez avoir un top crop, appelez:

imageView.setCropYCenterOffsetPct(0f);

Si vous souhaitez avoir une récolte inférieure, appelez:

imageView.setCropYCenterOffsetPct(1.0f);

Si vous souhaitez avoir un recadrage 1/3 de la descente, appelez:

imageView.setCropYCenterOffsetPct(0.33f);

De plus, si vous choisissez d'utiliser une autre méthode de recadrage, comme fit_center, vous pouvez le faire et aucune de cette logique personnalisée ne sera déclenchée. (D'autres implémentations vous permettent UNIQUEMENT d'utiliser leurs méthodes de recadrage).

Enfin, j'ai ajouté une méthode, redraw (), donc si vous choisissez de changer votre méthode de recadrage / scaleType dynamiquement dans le code, vous pouvez forcer la vue à se redessiner. Par exemple:

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

Pour revenir à votre recadrage personnalisé en haut au centre du tiers, appelez:

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

Voici la classe:

/* 
 * 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);
        }
    }
}

5

Peut-être aller dans le code source de la vue image sur Android et voir comment il dessine le recadrage central etc. et peut-être copier une partie de ce code dans vos méthodes. Je ne sais pas vraiment pour une meilleure solution que de faire cela. J'ai de l'expérience en redimensionnant et en recadrant manuellement le bitmap (recherche de transformations bitmap), ce qui réduit sa taille réelle, mais cela crée toujours un peu de surcharge dans le processus.


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

}


setFrame && onLayout
tianxia

computMatrix: vous pouvez faire n'importe quelle matrice ici.
tianxia

onLayoutme sauve beaucoup! Merci! J'ai rencontré un problème où il calcule la matrice mais n'affiche pas l'image immédiatement et l'ajout onLayoutdu code résout mon problème.
natsumiyu

1

Si vous utilisez Fresco (SimpleDraweeView), vous pouvez facilement le faire avec:

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

Celui-ci serait pour une récolte supérieure.

Plus d'informations sur le lien de référence


0

Il y a 2 problèmes avec les solutions ici:

  • Ils ne s'affichent pas dans l'éditeur de mise en page Android Studio (vous pouvez donc prévisualiser sur différentes tailles d'écran et proportions)
  • Il est uniquement mis à l'échelle en fonction de la largeur, donc en fonction des proportions de l'appareil et de l'image, vous pouvez vous retrouver avec une bande vide en bas

Cette petite modification résout le problème (placez le code dans onDraw et vérifiez les facteurs d'échelle de largeur et de hauteur):

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

-1

Solution la plus simple: découper l'image

 @Override
    public void draw(Canvas canvas) {
        if(getWidth() > 0){
            int clipHeight = 250;
            canvas.clipRect(0,clipHeight,getWidth(),getHeight());
         }
        super.draw(canvas);
    }

Cela ne redimensionnera pas l'image si elle est plus petite que la vue, c'est pourquoi les autres solutions ne sont pas aussi simples.
OldSchool4664
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.