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 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王14 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡14 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库15 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道16 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe16 小时前
Android Hook - 动态加载so库
android
居居飒17 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He20 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗20 小时前
Android笔试面试题AI答之Android基础(1)
android