ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 1. 前言 很多时候都需要图片的缩放效果。在ImageView中就支持这个属性,比如有下面这个图像: ![](https://img.kancloud.cn/0b/e0/0be0387aadb4cc933d2327812a501b3e_536x402.png) 其详细信息为: ![](https://img.kancloud.cn/b7/37/b73729fb40f5cc09b68462e5521a15eb_244x59.png) 但是,我们可以在ImageView中很容易将其载入到一个正方形的ImageView容器中,比如: ~~~ <ImageView android:layout_gravity="center_horizontal" android:layout_marginTop="4dp" android:src="@drawable/a" android:layout_width="200dp" android:layout_height="200dp" android:scaleType="fitCenter" android:background="@drawable/border" /> ~~~ 为了观察方便,这里为ImageView设置了一个边框border.xml: ~~~ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <!--设置边框颜色--> <stroke android:width="4dp" android:color="@color/teal_700"/> <!--设置填充颜色--> <solid android:color="@android:color/transparent"/> <padding android:left="4dp" android:top="4dp" android:right="4dp" android:bottom="4dp"/> </shape> ~~~ 效果: ![](https://img.kancloud.cn/ee/eb/eeeb4fb533ca5c2fc4cd9ec9e90c63dc_251x214.png) 可见其便捷性。所以这里大致将所遇到的图片缩放方法做一个总结。 # 2. 方法案例 ## 2.1 使用Bitmap的createScaledBitmap ~~~ class ScaleBitmapDemo: View { constructor(context: Context?) : super(context) {init()} constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs){init()} constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ){init()} private lateinit var targetBitmap: Bitmap private lateinit var mPaint: Paint private lateinit var rect: Rect private var startX = 0 private var targetWidth = 0 private var targetHeight = 0 private var paddingSize = 0 private fun init(){ // 关闭硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null) // 载入图像 val decodeResource = BitmapFactory.decodeResource(resources, R.drawable.a) // 因为这里没有裁剪,所以计算一下宽度的缩放值即可 targetWidth = dp2px(200) val scale = targetWidth * 1f / decodeResource.width targetHeight = (decodeResource.height * 1f * scale).toInt() mPaint = Paint(Paint.ANTI_ALIAS_FLAG) mPaint.color = resources.getColor(R.color.teal_700, null) mPaint.style = Paint.Style.FILL // 左边X坐标 startX = (resources.displayMetrics.widthPixels - targetWidth) / 2 rect = Rect(startX, 300, startX + targetWidth, 300 + targetHeight) paddingSize = dp2px(4) // 从当前存在的位图,按一定的比例创建一个新的位图。 targetBitmap = Bitmap.createScaledBitmap(decodeResource, targetWidth - paddingSize * 2, (targetHeight - scale * paddingSize * 5).toInt(), false) } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { drawRect(rect, mPaint) drawBitmap(targetBitmap, startX.toFloat() + paddingSize, 300f + paddingSize, mPaint) } } private fun dp2px(value: Int) : Int{ return (resources.displayMetrics.density * value).toInt() } } ~~~ 关键部分也就是: ~~~ // 从当前存在的位图,按一定的比例创建一个新的位图。 targetBitmap = Bitmap.createScaledBitmap ~~~ 结果: ![](https://img.kancloud.cn/8b/09/8b098ad8d8e75a99eea961b4a6a8eff4_247x190.png) 因为这里没有裁剪,所以这里保持图片长方形形状进行缩放。 ## 2.2 缩放图片(createScaledBitmap),裁剪画布 大致思路:首先判断下图像的大小和容器的大小,然后进行缩放图像,需要确保图像的宽高大于容器的宽高。然后将这个图像所在的画布进行裁剪,从中心裁剪出容器大小的图像即可。代码如下: ~~~ class ShaderScaleBitmapDemo : View { constructor(context: Context?) : super(context) { init() } constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() } constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init() } private lateinit var mPaint: Paint private lateinit var mBitmap: Bitmap private lateinit var mRect: Rect private var translateCanvasX = 0f private fun init() { // 关闭硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null) // 载入图像 mBitmap = BitmapFactory.decodeResource(resources, R.drawable.a) mPaint = Paint(Paint.ANTI_ALIAS_FLAG) mPaint.color = resources.getColor(R.color.teal_700, null) mPaint.style = Paint.Style.FILL_AND_STROKE // 判断图像是否大于容器大小 val imagewidth = dp2px(200 - 4) // 1 表示图像的宽或者高比容器小,需要放大 // 2 表示图像大小合适,直接裁剪即可 // 3 表示图像大小过大,需要缩小 var flag = 1 if (mBitmap.width > imagewidth && mBitmap.height > imagewidth) { if( mBitmap.height - imagewidth < 500 ) flag = 2 else flag = 3 } // 缩放图像 when(flag){ 1, 3 -> { mBitmap = scaleBitmapImage(mBitmap, imagewidth) } } // 计算裁剪画布的坐标 val clipRectLeft = (mBitmap.width - imagewidth) / 2 val clipRectTop = (mBitmap.width - imagewidth) / 2 val clipRectRight = left + imagewidth val clipRectBottom = top + imagewidth translateCanvasX = (resources.displayMetrics.widthPixels - imagewidth) * 1f/ 2 // 裁剪画布的矩形 mRect = Rect(clipRectLeft, clipRectTop, clipRectRight, clipRectBottom) } private fun scaleBitmapImage(bitmap: Bitmap, w: Int): Bitmap { var scale = 1f scale = Math.min(scale, w * 1f / bitmap.width) scale = Math.min(scale, w * 1f / bitmap.height) return createBitmapByOriginAndScaleValue(bitmap, scale) } private fun createBitmapByOriginAndScaleValue(bitmap: Bitmap, scale:Float): Bitmap{ return Bitmap.createScaledBitmap( bitmap, (scale * bitmap.width + 0.5f).toInt(), (scale * bitmap.height + 0.5f).toInt(), false ) } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { val saveLayer = saveLayer(0f, 0f, translateCanvasX + mBitmap.width.toFloat(), mBitmap.height.toFloat(), mPaint ) translate(translateCanvasX, 0f) clipRect(mRect) drawBitmap(mBitmap, dp2px(2).toFloat(), dp2px(2).toFloat(), mPaint) restoreToCount(saveLayer) } } private fun dp2px(value: Int): Int { return (resources.displayMetrics.density * value).toInt() } } ~~~ 结果也就是下面的第二个图像,第一个是上面直接使用ImageView的效果: ![](https://img.kancloud.cn/32/7d/327dc09426168d8a5e60c362ad545bf5_251x383.png) 看着有点像是缩放的大小差不多,但是因为在这个自定义View中没有处理onMeasure方法,所以这里的容器的宽高是存在问题的,当然,这里不再处理。比如如果我们在使用的时候也指定border.xml: ~~~ <com.weizu.ShaderScaleBitmapDemo android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/border" /> ~~~ 结果: ![](https://img.kancloud.cn/59/c5/59c5a3662cbab35277f5d63f6a3798b8_422x621.png) 很明显,达不到预期效果。但是这里如果仅仅是缩放和裁剪画布已经达到的练习效果。之后会就如何自定义View来实现ImageView的效果进行实践,所以这里不再介绍如何修复这里的bug。 ## 2.3 另一种裁剪方式(createBitmap) 在上面的案例中继续使用另一种裁剪效果,这里使用: ``` createBitmap(Bitmap source, int x, int y, int width, int height) ``` 而不是裁剪画布。也就是直接裁剪图像,source是待裁剪的源图像,x,y是裁剪位置的坐标,width和height是裁剪的高度。比如在《Android自定义控件开发入门与实战》一文中使用: ``` Bitmap.createBitmap(src,src.getWidth()/3,src.getHeight()/3, src.getWidth()/3,src.getHeight()/3); ``` 源图像,其中红色为辅助线: ![](https://img.kancloud.cn/0d/b5/0db52703351a72d9ed390c7b1330a030_610x505.png) 最后得到的: ![](https://img.kancloud.cn/ba/ba/baba487d4995847d4a54bf079be24c45_294x226.png) 所以这里也可以不裁剪画布,直接裁剪图像即可。如下面的代码: ~~~ private fun init() { // 关闭硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null) // 载入图像 mBitmap = BitmapFactory.decodeResource(resources, R.drawable.a) mPaint = Paint(Paint.ANTI_ALIAS_FLAG) mPaint.color = resources.getColor(R.color.teal_700, null) mPaint.style = Paint.Style.FILL_AND_STROKE // 判断图像是否大于容器大小 val imagewidth = dp2px(200 - 4) // 1 表示图像的宽或者高比容器小,需要放大 // 2 表示图像大小合适,直接裁剪即可 // 3 表示图像大小过大,需要缩小 var flag = 1 if (mBitmap.width > imagewidth && mBitmap.height > imagewidth) { if( mBitmap.height - imagewidth < 500 ) flag = 2 else flag = 3 } // 缩放图像 when(flag){ 1, 3 -> { mBitmap = scaleBitmapImage(mBitmap, imagewidth) } } // 计算裁剪画布的坐标 val clipRectLeft = (mBitmap.width - imagewidth) / 2 val clipRectTop = (mBitmap.width - imagewidth) / 2 val clipRectRight = left + imagewidth val clipRectBottom = top + imagewidth translateCanvasX = (resources.displayMetrics.widthPixels - imagewidth) * 1f/ 2 // 裁剪画布的矩形 mRect = Rect(clipRectLeft, clipRectTop, clipRectRight, clipRectBottom) } private fun scaleBitmapImage(bitmap: Bitmap, w: Int): Bitmap { var scale = 1f scale = Math.min(scale, w * 1f / bitmap.width) scale = Math.min(scale, w * 1f / bitmap.height) return createBitmapByOriginAndScaleValue(bitmap, scale) } private fun createBitmapByOriginAndScaleValue(bitmap: Bitmap, scale:Float): Bitmap{ return Bitmap.createScaledBitmap( bitmap, (scale * bitmap.width + 0.5f).toInt(), (scale * bitmap.height + 0.5f).toInt(), false ) } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { val saveLayer = saveLayer(0f, 0f, translateCanvasX + mBitmap.width.toFloat(), mBitmap.height.toFloat(), mPaint ) clipRect(mRect) drawBitmap(mBitmap, dp2px(2).toFloat(), dp2px(2).toFloat(), mPaint) restoreToCount(saveLayer) } } ~~~ 然后在xml中指定大小和居中: ~~~ <com.weizu.ShaderScaleBitmapDemo android:layout_width="200dp" android:layout_height="200dp" android:layout_gravity="center_horizontal" /> ~~~ 效果: ![](https://img.kancloud.cn/7a/42/7a42cbbeef4fa73ae979c38d18b3c9b8_249x376.png) 主要方法也就是: ~~~ private fun createBitmapByOriginAndScaleValue(bitmap: Bitmap, scale:Float): Bitmap{ return Bitmap.createScaledBitmap( bitmap, // src (scale * bitmap.width + 0.5f).toInt(), // dstWidth (scale * bitmap.height + 0.5f).toInt(), // dstHeight false // filter ) } ~~~ 值得注意的是,在上面的Boolean类型变量filter,这里直接设置为了false。该参数的意义是即是否给图像添加滤波效果。如果设 置为 true,则能够减少图像中由于噪声引起的突兀的孤立像素点或像素块。 ## 2.4 使用density来进行图像缩放 Density 用于表示该Bitmap合适的屏幕dpi,可以分为inDensity和inTargetDensity两个值,当着两个值不等的时候,它会缩放图像。比如:先获取 Bitmap 的原始 Density,然后将 Density 放大两倍,这样在 显示屏幕分辨率不变的情况下,显示出来的图片就应该缩小一半。对应代码: ~~~ class Scale2BitmapDemo : View { constructor(context: Context?) : super(context) { init() } constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() } constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init() } private lateinit var mPaint: Paint private lateinit var mBitmap: Bitmap private fun init() { // 关闭硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null) // 加载图片 mBitmap = BitmapFactory.decodeResource(resources, R.drawable.lf) mPaint = Paint(Paint.ANTI_ALIAS_FLAG) mPaint.color = resources.getColor(R.color.teal_700, null) mPaint.style = Paint.Style.FILL_AND_STROKE // 缩小图片,这里使用inDensity // 获取 Bitmap 的原始 Density,然后将 Density 放大两倍,这样在 // 显示屏幕分辨率不变的情况下,显示出来的图片就应该缩小一半。 mBitmap.density *= 2 } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.apply { drawBitmap(mBitmap,0f, 0f, mPaint) } } } ~~~ 效果: ![](https://img.kancloud.cn/43/67/43671b8d45f68347b9d9b1f2af18bcc8_225x375.png) 上面为原图,下面为缩放后的图像。 需要注意的是:Bitmap 在内存中的尺寸是没有变化的,这种设置 Bitmap Density 的方式只会影响显示缩放,而不会改变 Bitmap 本身在内存中的大。