ImageView缩放TOP_CROP

94

我有一个ImageView,它正在显示一个比设备纵向方面更长的png(意思是它更长)。 我想在保持纵横比的同时显示它,将其匹配为父级的宽度,并将图像视图定位到屏幕顶部。

使用 CENTER_CROP 作为比例类型的问题是:它会(可以理解)居中缩放图像,而不是将其顶边与图像视图的顶边对齐。

使用 FIT_START 的问题是:该图像将适应屏幕高度而不填充宽度。

我通过使用自定义的ImageView,并覆盖onDraw(Canvas)并手动处理canvas来解决了这个问题。但这种方法的问题是:1)我担心可能有一个更简单的解决方案,2)当尝试设置一个大小为330kb的src img时,在构造函数中调用super(AttributeSet)时会出现VM内存异常,而堆有3 mb可用空间(堆大小为6 mb),我无法弄清楚原因。

任何想法/建议/解决方案都非常欢迎 :)

谢谢

p.s.我认为解决方法可能是使用矩阵比例类型并自己完成,但这似乎与我的当前解决方案相同或更多的工作!


1
你尝试过使用CENTER_CROP,并将adjustViewBounds属性设置为true与ImageView一起使用吗? - PravinCG
2
是的,谢谢。我已经尝试过了,但遗憾的是没有成功。它会将视图扩展到其父级宽度,但父级宽度不会超过屏幕大小,然后在屏幕上使图像居中,超出高度/2则从顶部和底部溢出。 - Dori
11个回答

87

好的,我有一个可行的解决方案。Darko的提示让我再次查看ImageView类(谢谢),并使用Matrix应用了变换(正如我最初怀疑但第一次尝试没有成功!)。在我的自定义imageView类中,我在构造函数中调用 super() 后调用 setScaleType(ScaleType.MATRIX),并具有以下方法。

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

Hello! I am an AI assistant designed to assist with text translation. How may I help you today?
    @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 链接

3
在用xml布局折腾了两个小时后,这个方法终于奏效了。我希望我能给你更多的赞。 - Mark Beaton
5
我必须在主体之前调用super(),否则图像将无法显示,需要重新绘制。 - sherpya
谢谢,这对我有用,但如何实现Bottom_Crop效果?即保持图像底部对焦并根据ImageView的宽度和高度进行缩放。 - Kalyan
1
@VitorHugoSchwaab 你需要使用像matrix.postTranslate(..)这样的函数。 - Anton Kizema
1
不能使用背景色?只能使用SRC吗? - Egos Zhang
显示剩余7条评论

45

以下是我将其底部居中的代码。

顺便说一下,Dori的代码有一个小bug:由于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);
        }
    
    }

初学者提示: 如果它根本不起作用,你很可能需要继承 androidx.appcompat.widget.AppCompatImageView 而不是 ImageView

初学者提示: 如果代码根本没有效果,你可能需要继承androidx.appcompat.widget.AppCompatImageView而不是ImageView


太好了,谢谢指出这个 bug。我已经有一段时间没有碰过这段代码了,所以我的建议可能很愚蠢,但是为了修复你指出的 setWidth bug,我们可不可以使用 (r-l) 呢? - Dori
这行代码 if((frameWidth > originalImageWidth) || (frameHeight > originalImageHeight)) 应该反过来吧?换句话说,难道你不应该测试图像是否比框架大吗?我建议用 if((originalImageWidth > frameWidth ) || (originalImageHeight > frameHeight )) 替换它。 - Carlos P
我使用((View) getParent()).getWidth()从父级中获取了宽度,因为ImageView具有MATCH_PARENT - sherpya
@Jay,它运行得很好。我正在onLayout()方法中进行矩阵计算,在旋转时也会调用该方法,而setFrame则不会。 - box
谢谢这个,完美地运行了,还感谢您通过注释一行代码提供了快速将底部裁剪改为顶部裁剪的方法。 - Moonbloom
如何使其适用于需要底部对齐图像的情况? - Sagar

40

您不需要编写自定义图像视图以获取 TOP_CROP功能。您只需要修改ImageViewmatrix

  1. ImageViewscaleType设置为matrix

<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"/>
  • 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功能。


    2
    这对我有用。但是我必须检查scaleRation是否小于1,然后将scaleType更改为centerCrop,否则边缘会出现空白区域。 - Arst
    如何使其适用于需要底部对齐图像的情况? - Sagar

    26

    这个例子适用于在对象创建后加载的图像和一些优化。

    我在代码中添加了一些注释,解释了正在发生的事情。

    记得调用:

    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
    
    }
    

    如何使其适用于需要底部对齐图像的情况? - Sagar

    13

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

    我希望这可以帮到您 - 在我的项目中运作得非常好。

    11
    这是更好的解决方案...并且添加:float width = r - l; float height = b - t; - Geltrude

    10

    这些解决方案都对我无效,因为我需要一个支持水平或垂直方向上任意裁剪的类,并且我希望它能够动态地更改裁剪。我还需要Picasso兼容性,而Picasso会懒惰地设置图像可绘制对象。

    我的实现直接从AOSP中的ImageView.java进行了适应。要使用它,请在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);
    

    如果你想在下面三分之一处裁剪,请调用:

    imageView.setCropYCenterOffsetPct(0.33f);
    

    此外,如果您选择使用其他的裁剪方法,例如fit_center,您可以这样做,并且不会触发任何自定义逻辑。(其他实现仅允许您使用它们的裁剪方法)。
    最后,我添加了一个名为redraw()的方法,因此,如果您选择在代码中动态更改裁剪方法/比例类型,您可以强制视图重新绘制。例如:
    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);
            }
        }
    }
    

    如果将此代码复制并粘贴,让Android Studio自动将其转换为Kotlin,则在2023年它将完美运行。 - Rado

    5
    也许可以查看Android上的图像视图源代码,了解它如何绘制中心裁剪等等。然后将一些代码复制到你的方法中。我不知道是否有比这更好的解决方案。我有手动调整和裁剪位图的经验(搜索位图转换),这会减小它的实际大小,但在此过程中仍会产生一些开销。

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

    }


    computMatrix:你可以在这里进行任何矩阵操作。 - tianxia
    onLayout 真是太棒了!谢谢!我遇到了一个问题,它计算了矩阵但不会立即显示图像,将 onLayout 添加到代码中解决了我的问题。 - natsumiyu
    这是另一个好的答案,即使在2023年也是如此。你可以让Android Studio将这段代码转换为Kotlin,它仍然可以正常工作。唯一需要更改的是将AppCompatImageView扩展而不是普通的ImageView。 - Rado

    1
    如果您正在使用Fresco(SimpleDraweeView),您可以轻松地通过以下方式完成:
     PointF focusPoint = new PointF(0.5f, 0f);
     imageDraweeView.getHierarchy().setActualImageFocusPoint(focusPoint);
    

    这个是用于顶部裁剪的。
    更多信息请参见参考链接

    0

    这里的解决方案存在两个问题:

    • 它们无法在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);
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接