Skalowanie ImageView TOP_CROP


88

Mam plik, ImageViewktóry wyświetla png, który ma większy współczynnik proporcji niż urządzenie (w pionie - co oznacza, że ​​jest dłuższy). Chcę to wyświetlić, zachowując proporcje, dopasowując szerokość elementu nadrzędnego i przypinając widok obrazu do góry ekranu.

Problem, który mam z używaniem CENTER_CROPjako typu skali, polega na tym, że (zrozumiałe) wyśrodkowuje skalowany obraz zamiast wyrównywać górną krawędź do górnej krawędzi widoku obrazu.

Problem FIT_STARTpolega na tym, że obraz będzie pasował do wysokości ekranu i nie wypełni szerokości.

Rozwiązałem ten problem, używając niestandardowego ImageView oraz zastępując onDraw(Canvas)i obsługując to ręcznie za pomocą płótna; Problem z tym podejściem polega na tym, że 1) obawiam się, że może być prostsze rozwiązanie, 2) otrzymuję wyjątek VM mem podczas wywoływania super(AttributeSet)konstruktora podczas próby ustawienia src img na 330kb, gdy sterta ma 3 MB wolnego miejsca (przy wielkości sterty 6 mb) i nie mogę zrozumieć, dlaczego.

Wszelkie pomysły / sugestie / rozwiązania są mile widziane :)

Dzięki

ps Myślałem, że rozwiązaniem może być użycie skali matrycowej i zrobienie tego samodzielnie, ale wydaje mi się, że to taka sama lub więcej pracy niż moje obecne rozwiązanie!


1
Czy próbowałeś z CENTER_CROP i ustawiłeś właściwość adjustViewBounds na true z ImageView?
PravinCG

2
Tak, wypróbowałem to thx, obawiam się, że nie powiodło się, ponieważ rozszerzy on widok aż do szerokości swojego rodzica, który nie będzie większy niż ekran, a następnie wyśrodkuj obraz na ekranie z nadmierną wysokością / 2 wystającą z góry i na dole
Dori

Odpowiedzi:


84

Ok, mam działające rozwiązanie. Podpowiedź Darko sprawiła, że ​​ponownie spojrzałem na klasę ImageView (dzięki) i zastosowałem transformację za pomocą Matrixa (jak początkowo podejrzewałem, ale nie powiodło się przy mojej pierwszej próbie!). W mojej niestandardowej klasie imageView wywołuję setScaleType(ScaleType.MATRIX)po super()w konstruktorze i mam następującą metodę.

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

Umieściłem int w setFrame()metodzie, tak jak w ImageView, wywołanie configureBounds()jest w tej metodzie, w której odbywa się całe skalowanie i macierz, więc wydaje mi się to logiczne (powiedz, jeśli się nie zgadzasz)

Poniżej znajduje się metoda super.setFrame () z 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;
    }

Znajdź tutaj src pełnej klasy


Dzięki za kod, @doridori! Udało się! Po prostu nie rozumiem, dlaczego powtórzyłeś metodę "setFrame" w swoim wyjaśnieniu ... Użyłem tylko pierwszej z sukcesem (i całkowicie zignorowałem drugą xD)
Alesqui

3
Po dwóch godzinach walki z tym przez układ XML, to zadziałało. Chciałbym móc dać ci więcej upboats.
Mark Beaton,

5
Musiałem zadzwonić do super () przed ciałem, inaczej obraz nie wyświetlałby się bez przemalowania
sherpya

1
@VitorHugoSchwaab musisz używać thms jak matrix.postTranslate (..)
Anton Kizema

1
nie możesz używać tła? używać tylko src?
Egos Zhang

43

oto mój kod do wyśrodkowania go na dole. Przy okazji. w kodzie Dori jest mały błąd: ponieważ metoda super.frame()jest wywoływana na samym końcu, getWidth()metoda może zwrócić nieprawidłową wartość. Jeśli chcesz wyśrodkować go u góry, po prostu usuń wiersz postTranslate i gotowe. Fajną rzeczą jest to, że za pomocą tego kodu możesz go przenieść w dowolne miejsce. (po prawej, środek => nie ma problemu;)

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

    }

Świetnie, dziękuję za wskazanie błędu. Nie dotykałem tego kodu przez jakiś czas, więc może to być głupia sugestia, ale aby naprawić błąd setWidth, który wskazałeś, czy nie można po prostu użyć (r-l)zamiast tego?
Dori

Na pewno linia if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight))powinna zostać odwrócona? Innymi słowy, czy nie powinieneś testować, czy obraz jest większy niż ramka? Proponuję zastąpić goif((originalImageWidth > frameWidth ) || (originalImageHeight > frameHeight ))
Carlos P

Wybrałem szerokość od rodziców, ((View) getParent()).getWidth()od kiedy ImageViewmaMATCH_PARENT
sherpya

@Jay, to działa świetnie. Robię obliczenia macierzowe w metodzie onLayout (), która jest również wywoływana przy rotacji, gdzie setFrame nie.
pudełko

Dzięki za to, działa idealnie, a także dziękuję za umożliwienie szybkiej zmiany dolnego kadrowania na górny kadr, komentując 1 linię kodu
Moonbloom

39

Nie musisz pisać niestandardowego widoku obrazu, aby uzyskać dostęp do tej TOP_CROPfunkcji. Wystarczy zmodyfikować matrixz ImageView.

  1. Ustaw scaleTypesię matrixdla 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. Ustaw niestandardową macierz dla 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);
    

W ten sposób uzyskasz TOP_CROPfunkcjonalność.


2
To zadziałało dla mnie. Muszę jednak sprawdzić scaleRation, jeśli jest <1, po prostu zmieniam na scaleTypew centerCropprzeciwnym razie na krawędziach pojawi się puste miejsce.
Arst

Jak to działa w przypadkach, w których potrzebne są obrazy wyrównane do dołu?
Sagar,

26

Ten przykład działa z obrazami ładowanymi po utworzeniu obiektu i pewnej optymalizacji. Dodałem kilka komentarzy w kodzie, które wyjaśniają, co się dzieje.

Pamiętaj, aby zadzwonić:

imageView.setScaleType(ImageView.ScaleType.MATRIX);

lub

android:scaleType="matrix"

Źródło 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

}

Jak to działa w przypadkach, w których potrzebne są obrazy wyrównane do dołu?
Sagar,

13

W oparciu o Dori używam rozwiązania, które skaluje obraz na podstawie szerokości lub wysokości obrazu, aby zawsze wypełniał otaczający pojemnik. Pozwala to na skalowanie obrazu w celu wypełnienia całej dostępnej przestrzeni przy użyciu lewego górnego punktu obrazu zamiast środka jako początku (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);
}

Mam nadzieję, że to pomoże - działa jak gratka w moim projekcie.


11
To jest lepsze rozwiązanie ... I dodaj: float width = r - l; wysokość pływaka = b - t;
Geltruda

9

Żadne z tych rozwiązań nie zadziałało dla mnie, ponieważ chciałem klasy, która wspierałaby dowolną uprawę z kierunku poziomego lub pionowego i chciałem, aby umożliwiała mi dynamiczną zmianę upraw. Potrzebowałem również kompatybilności z Picassem , a Picasso leniwie ustawia rysunki obrazów.

Moja implementacja jest dostosowywana bezpośrednio z ImageView.java w AOSP. Aby z niego skorzystać, zadeklaruj tak w XML:

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

Ze źródła, jeśli chcesz uzyskać najlepsze plony, zadzwoń:

imageView.setCropYCenterOffsetPct(0f);

Jeśli chcesz mieć najniższe plony, zadzwoń:

imageView.setCropYCenterOffsetPct(1.0f);

Jeśli chcesz mieć uprawę 1/3 drogi w dół, zadzwoń:

imageView.setCropYCenterOffsetPct(0.33f);

Co więcej, jeśli zdecydujesz się użyć innej metody przycinania, takiej jak fit_center, możesz to zrobić i żadna z tej niestandardowej logiki nie zostanie wyzwolona. (Inne implementacje TYLKO pozwalają ci używać ich metod przycinania).

Na koniec dodałem metodę redraw (), więc jeśli zdecydujesz się zmienić metodę przycinania / typ skali dynamicznie w kodzie, możesz zmusić widok do przerysowania. Na przykład:

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

Aby wrócić do niestandardowego trzeciego przycięcia w górnej środkowej części, zadzwoń:

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

Oto klasa:

/* 
 * 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

Może przejdź do kodu źródłowego widoku obrazu na Androidzie i zobacz, jak rysuje środkowy kadr itp., A może skopiuj część tego kodu do swoich metod. Naprawdę nie znam lepszego rozwiązania niż zrobienie tego. Mam doświadczenie w ręcznym zmienianiu rozmiaru i przycinaniu mapy bitowej (wyszukiwanie transformacji bitmapy), co zmniejsza jej rzeczywisty rozmiar, ale nadal powoduje pewne obciążenie w procesie.


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: możesz tutaj zrobić dowolną macierz.
tianxia

onLayoutdużo mnie oszczędza! Dzięki! Napotkałem problem polegający na tym, że oblicza matrycę, ale nie wyświetla obrazu od razu, a dodanie onLayoutkodu rozwiązuje mój problem.
natsumiyu

1

Jeśli korzystasz z Fresco (SimpleDraweeView), możesz to łatwo zrobić za pomocą:

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

Ten byłby dla najlepszych roślin.

Więcej informacji na stronie odnośnika


0

Istnieją 2 problemy z rozwiązaniami tutaj:

  • Nie są renderowane w edytorze układu Android Studio (dzięki czemu można wyświetlać podgląd na różnych rozmiarach ekranu i proporcjach)
  • Skaluje się tylko według szerokości, więc w zależności od proporcji urządzenia i obrazu, możesz skończyć z pustym paskiem na dole

Ta niewielka modyfikacja rozwiązuje problem (umieść kod w onDraw i sprawdź współczynniki skali szerokości i wysokości):

@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

Najprostsze rozwiązanie: przytnij obraz

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

Nie spowoduje to powiększenia obrazu, jeśli jest mniejszy niż widok, dlatego inne rozwiązania nie są tak proste.
OldSchool4664
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.