Масштабирование изображения TOP_CROP

#android #imageview

#Android #просмотр изображения

Вопрос:

У меня есть ImageView , который отображает png, который имеет большее соотношение сторон, чем у устройства (в вертикальном положении — это означает, что он длиннее). Я хочу отобразить это с сохранением соотношения сторон, соответствия ширине родительского изображения и закрепления изображения в верхней части экрана.

Проблема, с которой я сталкиваюсь при использовании CENTER_CROP в качестве типа масштаба, заключается в том, что он будет (понятно) центрировать масштабированное изображение вместо выравнивания верхнего края по верхнему краю изображения.

Проблема с FIT_START заключается в том, что изображение будет соответствовать высоте экрана, а не заполнять ширину.

Я решил эту проблему, используя пользовательский ImageView и переопределяя onDraw(Canvas) и обрабатывая это вручную с помощью canvas; проблема с этим подходом заключается в том, что 1) Я беспокоюсь, что может быть более простое решение, 2) Я получаю исключение виртуальной памяти при вызове super(AttributeSet) в конструкторе при попытке установить src img размером 330 кб, когда в куче 3 МБ свободного места (при размере кучи 6 мб) и не могу понять, почему.

Любые идеи / предложения / решения приветствуются 🙂

Спасибо

p.s. я думал, что решением может быть использование типа матричного масштаба и выполнение этого самостоятельно, но, похоже, это такая же или большая работа, чем мое текущее решение!

Комментарии:

1. Вы пробовали использовать CENTER_CROP и устанавливали свойство adjustViewBounds как true для ImageView?

2. Да, я пробовал это, спасибо, боюсь, безуспешно, так как это расширит представление до тех пор, пока оно не увеличит ширину своего родительского элемента, который не будет больше экрана, а затем центрирует изображение на экране с избыточной высотой / 2, выступающей сверху и снизу

Ответ №1:

Хорошо, у меня есть рабочее решение. Подсказка от Darko заставила меня еще раз взглянуть на класс ImageView (спасибо) и применить преобразование с использованием матрицы (как я изначально подозревал, но с первой попытки не добился успеха!). В моем пользовательском классе ImageView я вызываю setScaleType(ScaleType.MATRIX) after super() в конструкторе и получаю следующий метод.

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

Я поместил int в setFrame() метод, поскольку в ImageView вызов configureBounds() находится внутри этого метода, где происходит все масштабирование и матричные операции, поэтому мне кажется логичным (скажите, если вы не согласны)

Ниже приведен super.setFrame() метод из AOSP (проект с открытым исходным кодом для Android)

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

Найдите полный класс src здесь

Комментарии:

1. Спасибо за код, @doridori! Все сработало нормально! Я просто не понимаю, почему вы повторили метод «setFrame» в своем объяснении… Я успешно использовал только первый (и полностью проигнорировал второй xD)

2. После двухчасовой борьбы с этим с помощью xml-макета это сработало. Хотел бы я предоставить вам больше возможностей.

3. Мне пришлось вызвать super () перед телом, иначе изображение не отображалось бы без перерисовки

4. @VitorHugoSchwaab вы должны использовать thm, такие как matrix.postTranslate(..)

5. не можете использовать background? использовать только src?

Ответ №2:

Вот мой код для центрирования его внизу.

Кстати, в коде Dori есть небольшая ошибка: поскольку super.frame() метод вызывается в самом конце, он getWidth() может возвращать неправильное значение.

Если вы хотите расположить его по центру вверху, просто удалите строку postTranslate, и все готово.

Приятно то, что с помощью этого кода вы можете перемещать его куда угодно. (справа, по центру => нет проблем 😉

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

Совет для начинающих: Если это просто не работает, вам, вероятно, придется extends androidx.appcompat.widget.AppCompatImageView вместо ImageView

Комментарии:

1. Отлично, спасибо, что указали на ошибку. Я некоторое время не касался этого кода, так что это может быть глупым предложением, но для исправления ошибки setWidth, на которую вы указали, нельзя ли просто использовать (r-l) вместо этого?

2. Конечно, строка if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) должна быть перевернута? Другими словами, разве вы не должны проверять, больше ли изображение, чем рамка? Я предлагаю заменить его на if((originalImageWidth > frameWidth ) || (originalImageHeight > frameHeight ))

3. Я выбрал ширину из родительского использования, ((View) getParent()).getWidth() поскольку ImageView имеет MATCH_PARENT

4. @Jay это отлично работает. Я выполняю матричные вычисления в методе onLayout (), который также вызывается при вращении, где setFrame этого не делает.

5. Спасибо за это, работает отлично, а также спасибо за предоставление способа быстрого изменения нижнего кадрирования на верхний, прокомментировав 1 строку кода

Ответ №3:

Вам не нужно создавать пользовательский вид изображения для получения TOP_CROP функциональности. Вам просто нужно изменить matrix из ImageView .

  1. Установите scaleType значение matrix для 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. Задайте пользовательскую матрицу для 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);
      

Выполнение этого даст вам TOP_CROP функциональность.

Комментарии:

1. У меня это сработало. Тем не менее, я должен проверить масштабирование, если оно < 1, тогда я просто меняю scaleType на centerCrop , иначе я увижу пустое пространство по краям.

2. Как заставить это работать в случаях, когда требуется выравнивание изображений по низу?

Ответ №4:

Этот пример работает с изображениями, которые загружаются после создания объекта некоторая оптимизация. Я добавил несколько комментариев в код, которые объясняют, что происходит.

Не забудьте вызвать:

 imageView.setScaleType(ImageView.ScaleType.MATRIX);
  

или

 android:scaleType="matrix"
  

Исходный код 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

}
  

Комментарии:

1. Как заставить это работать в случаях, когда требуется выравнивание изображений по низу?

Ответ №5:

На основе Dori я использую решение, которое либо масштабирует изображение на основе ширины или высоты изображения, чтобы всегда заполнять окружающий контейнер. Это позволяет масштабировать изображение, чтобы заполнить все доступное пространство, используя верхнюю левую точку изображения, а не центр в качестве источника (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);
}
  

Я надеюсь, что это поможет — работает как лакомство в моем проекте.

Комментарии:

1. Это лучшее решение… И добавьте: ширина с плавающей точкой = r — l; высота с плавающей точкой = b — t;

Ответ №6:

Ни одно из этих решений не сработало для меня, потому что я хотел класс, который поддерживал бы произвольную обрезку в горизонтальном или вертикальном направлении, и я хотел, чтобы это позволяло мне динамически изменять обрезку. Мне также нужна была совместимость с Picasso, а Picasso лениво настраивает чертежи изображений.

Моя реализация адаптирована непосредственно из ImageView.java в AOSP. Чтобы использовать его, объявите вот так в XML:

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

Из исходного кода, если вы хотите получить верхний кадр, вызовите:

 imageView.setCropYCenterOffsetPct(0f);
  

Если вы хотите получить обрезку снизу, вызовите:

 imageView.setCropYCenterOffsetPct(1.0f);
  

Если вы хотите выполнить обрезку на 1/3 пути вниз, вызовите:

 imageView.setCropYCenterOffsetPct(0.33f);
  

Кроме того, если вы решите использовать другой метод обрезки, например fit_center, вы можете сделать это, и ни одна из этих пользовательских логик не будет запущена. (Другие реализации позволяют использовать только их методы обрезки).

Наконец, я добавил метод redraw (), поэтому, если вы решите динамически изменять свой метод обрезки / scaleType в коде, вы можете принудительно перерисовать представление. Например:

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

Чтобы вернуться к пользовательской обрезке верхней центральной трети, позвоните:

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

Вот класс:

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

Комментарии:

1. Если скопировать и вставить этот код и позволить Android Studio автоматически преобразовать его в Kotlin, он будет отлично работать в 2023 году.

Ответ №7:

Возможно, зайдите в исходный код для просмотра изображений на Android и посмотрите, как он рисует центральную обрезку и т.д. И, Возможно, скопируйте часть этого кода в свои методы. я действительно не знаю лучшего решения, чем делать это. у меня есть опыт ручного изменения размера и обрезки растрового изображения (поиск преобразований растрового изображения), что уменьшает его фактический размер, но все равно создает небольшие накладные расходы в процессе.

Ответ №8:

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

}

Комментарии:

1. computMatrix: здесь вы можете создать любую матрицу.

2. onLayout это меня очень спасает! Спасибо! Я столкнулся с проблемой, когда он вычисляет матрицу, но не отображает изображение сразу, и добавление onLayout в код решает мою проблему.

3. Это еще один хороший ответ здесь, даже в 2023 году. Можно позволить Android Studio преобразовать этот код в Kotlin, и он будет работать просто отлично. Единственным необходимым изменением было бы расширение AppCompatImageView вместо обычного ImageView.

Ответ №9:

Если вы используете Fresco (SimpleDraweeView), вы можете легко сделать это с:

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

Это было бы для верхнего кадрирования.

Дополнительная информация по справочной ссылке

Ответ №10:

Здесь есть 2 проблемы с решениями:

  • Они не отображаются в редакторе макетов Android Studio (поэтому вы можете выполнять предварительный просмотр на разных размерах экрана и соотношениях сторон)
  • Масштабируется только по ширине, поэтому в зависимости от соотношения сторон устройства и изображения внизу может получиться пустая полоса

Это небольшое изменение устраняет проблему (поместите код в onDraw и проверьте масштабные коэффициенты ширины и высоты):

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

Ответ №11:

Самое простое решение: обрезать изображение

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

Комментарии:

1. Это не приведет к увеличению масштаба изображения, если оно меньше, чем вид, поэтому другие решения не такие простые.