Android 自定义缩放的View

在 Android 开发中,我们常常需要对图片进行缩放的处理,对于这种需求我们可以直接使用 ImageView 的 scaleType 属性来实现。但是,对于指定高度或者宽度来适配的情况,scaleType 就无法精细控制了。

比如 FIT_STARTFIT_CENTERFIT_ENDCENTER_INSIDE 虽然会缩放图片,但是其是根据长边来缩放的,目的是确保完整显示;CENTER 则是只居中不缩放; CENTER_CROP 是根据短边来缩放的。这时候就需要我们来自定义View来实现了。

指定宽高适配的 ImageView

实现方式也很简单,核心步骤如下:

  • 第一步:确定是按照 高度适配 还是 宽度适配,计算出缩放比例
kotlin 复制代码
// 定义不同的缩放类型枚举
enum class ScaleType {
    NONE, // 不进行任何缩放
    WIDTH_FIT, // 根据宽度缩放
    HEIGHT_FIT, // 根据高度缩放
    ASPECT_FIT // 根据比例缩放
}
// 原图的Rect信息
private val srcRect = Rect(0, 0, 0, 0)
// 目标的Rect信息
private val targetRect = Rect(0, 0, 0, 0)


// 计算原始图片宽度相对于目标宽度的缩放比例
val scaleX = targetWidth.toFloat() / bitmapWidth
// 计算原始图片高度相对于目标高度的缩放比例
val scaleY = targetHeight.toFloat() / bitmapHeight
srcRect.set(0, 0, bitmapWidth, bitmapHeight)
when (scaleType) {
    ScaleType.NONE -> {
        targetRect.set(srcRect)
    }
    ScaleType.WIDTH_FIT -> {
        targetRect.set(0, 0, targetWidth, (bitmapHeight * scaleX).toInt())
    }
    ScaleType.HEIGHT_FIT -> {
        targetRect.set(0, 0, (bitmapWidth * scaleY).toInt(), targetHeight)
    }
    ScaleType.ASPECT_FIT -> {
        val scale = max(scaleX, scaleY)
        targetRect.set(0, 0, (bitmapWidth * scale).toInt(), (bitmapHeight * scale).toInt())
    }
    else -> {}
}
  • 第二步:确定裁剪方式,是居中裁剪、还是左上角裁剪等等,计算出偏移位置
kotlin 复制代码
enum class CropType {
    CENTER, // 居中裁剪
    TOP_LEFT, // 从左上角开始裁剪
    TOP_RIGHT, // 从右上角开始裁剪
    BOTTOM_LEFT, // 从左下角开始裁剪
    BOTTOM_RIGHT, // 从右下角开始裁剪
}

var left = 0f
var top = 0f
when (cropType) {
    CropType.CENTER -> {
        left = (width - targetRect.width()) / 2f
        top = (height - targetRect.height()) / 2f
    }
    CropType.TOP_LEFT -> {
        left = 0f
        top = 0f
    }
    CropType.TOP_RIGHT -> {
        left = (width - targetRect.width()).toFloat()
        top = 0f
    }
    CropType.BOTTOM_LEFT -> {
        left = 0f
        top = (height - targetRect.height()).toFloat()
    }
    CropType.BOTTOM_RIGHT -> {
        left = (width - targetRect.width()).toFloat()
        top = (height - targetRect.height()).toFloat()
    }
    else -> {}
}
  • 最后根据缩放比例和偏移位置绘制图片
kotlin 复制代码
// 保存当前画布状态
canvas.save()
// 平移画布到指定位置
canvas.translate(left, top)
// 在画布上绘制图片
canvas.drawBitmap(bitmap, srcRect, targetRect, null)
// 恢复画布状态
canvas.restore()

效果如下图所示:

完整代码

kotlin 复制代码
class FitWidthAndHeightImageView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attributeSet, defStyleAttr) {


    // 存储要显示的图片
    private var bitmap: Bitmap? = null

    // 目标宽度
    private var targetWidth = 0

    // 目标高度
    private var targetHeight = 0

    // 缩放类型
    private var scaleType: ScaleType = ScaleType.NONE

    // 裁剪类型
    private var cropType: CropType = CropType.CENTER
    

    private val srcRect = Rect(0, 0, 0, 0)
    private val targetRect = Rect(0, 0, 0, 0)

    // 定义不同的缩放类型枚举
    enum class ScaleType {
        NONE, // 不进行任何缩放
        WIDTH_FIT, // 根据宽度缩放
        HEIGHT_FIT, // 根据高度缩放
        ASPECT_FIT // 根据比例缩放
    }

    enum class CropType {
        CENTER, // 居中裁剪
        TOP_LEFT, // 从左上角开始裁剪
        TOP_RIGHT, // 从右上角开始裁剪
        BOTTOM_LEFT, // 从左下角开始裁剪
        BOTTOM_RIGHT, // 从右下角开始裁剪
    }


    // 设置图片资源的方法
    fun setImageResource(@DrawableRes resId: Int) {
        bitmap = BitmapFactory.decodeResource(resources, resId)
        // 触发重绘
        invalidate()
    }

    fun setImageBitmap(srcBitmap: Bitmap) {
        bitmap = srcBitmap
        invalidate()
    }
    
    
    fun setScaleType(type: ScaleType) {
        setScaleAndCropType(type, cropType)
    }
    
    fun setCropType(crop: CropType) {
        setScaleAndCropType(scaleType, crop)
    }

    // 设置缩放类型的方法
    fun setScaleAndCropType(type: ScaleType, crop: CropType) {
        scaleType = type
        cropType = crop
        // 触发重绘
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        targetWidth = w
        targetHeight = h
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
            // 如果没有指定当前View的size
            val bitmap = bitmap
            val measuredWidth = when {
                scaleType == ScaleType.NONE && bitmap!= null -> bitmap.width
                bitmap == null && targetWidth > 0 -> targetWidth
                else -> 0 // 默认大小,可以根据实际需求调整
            }
            val measuredHeight = when {
                scaleType == ScaleType.NONE && bitmap!= null -> bitmap.height
                bitmap == null && targetHeight > 0 -> targetHeight
                else -> 0 // 默认大小,可以根据实际需求调整
            }
            setMeasuredDimension(measuredWidth, measuredHeight)
        } else {
            setMeasuredDimension(widthSize, heightSize)
        }
    }

    // 重写 onDraw 方法进行绘制操作
    override fun onDraw(canvas: Canvas) {
        // 如果图片为空或者目标宽度、高度为 0,则不进行绘制
        val bitmap = bitmap
        val targetHeight = targetHeight
        val targetWidth = targetWidth
        val bitmapWidth = bitmap?.width ?: 0
        val bitmapHeight = bitmap?.height ?: 0
        if (bitmap == null || targetWidth <= 0 || targetHeight <= 0 || bitmapWidth <= 0 || bitmapHeight <= 0) {
            return
        }
        val viewWidth = width
        val viewHeight = height
        println("bitmapWidth $bitmapWidth bitmapHeight $bitmapHeight viewWidth = $viewWidth viewHeight $viewHeight")
        // 计算原始图片宽度相对于目标宽度的缩放比例
        val scaleX = targetWidth.toFloat() / bitmapWidth
        // 计算原始图片高度相对于目标高度的缩放比例
        val scaleY = targetHeight.toFloat() / bitmapHeight
        println("scaleX = $scaleX scaleY = $scaleY")

        srcRect.set(0, 0, bitmapWidth, bitmapHeight)
        when (scaleType) {
            ScaleType.NONE -> {
                targetRect.set(srcRect)
            }
            ScaleType.WIDTH_FIT -> {
                targetRect.set(0, 0, targetWidth, (bitmapHeight * scaleX).toInt())
            }
            ScaleType.HEIGHT_FIT -> {
                targetRect.set(0, 0, (bitmapWidth * scaleY).toInt(), targetHeight)
            }
            ScaleType.ASPECT_FIT -> {
                val scale = max(scaleX, scaleY)
                targetRect.set(0, 0, (bitmapWidth * scale).toInt(), (bitmapHeight * scale).toInt())
            }
            else -> {}
        }

        var left = 0f
        var top = 0f
        when (cropType) {
            CropType.CENTER -> {
                left = (width - targetRect.width()) / 2f
                top = (height - targetRect.height()) / 2f
            }
            CropType.TOP_LEFT -> {
                left = 0f
                top = 0f
            }
            CropType.TOP_RIGHT -> {
                left = (width - targetRect.width()).toFloat()
                top = 0f
            }
            CropType.BOTTOM_LEFT -> {
                left = 0f
                top = (height - targetRect.height()).toFloat()
            }
            CropType.BOTTOM_RIGHT -> {
                left = (width - targetRect.width()).toFloat()
                top = (height - targetRect.height()).toFloat()
            }
            else -> {}
        }

        // 保存当前画布状态
        canvas.save()
        // 平移画布到指定位置
        canvas.translate(left, top)
        // 在画布上绘制图片
        canvas.drawBitmap(bitmap, srcRect, targetRect, null)
        // 恢复画布状态
        canvas.restore()
    }
}

参考

相关推荐
千里马学框架12 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台12 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone12 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc13 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo15 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770015 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work16 小时前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼16 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work16 小时前
ch21 签名、校验与发版
kotlin
AFinalStone17 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui