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

参考

相关推荐
百锦再17 分钟前
Android Studio开发 SharedPreferences 详解
android·ide·android studio
青春给了狗29 分钟前
Android 14 修改侧滑手势动画效果
android
CYRUS STUDIO36 分钟前
Android APP 热修复原理
android·app·frida·hotfix·热修复
火柴就是我1 小时前
首次使用Android Studio时,http proxy,gradle问题解决
android
limingade2 小时前
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段
android·智能手机·电脑·蓝牙电话·电脑打电话
浩浩测试一下2 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
喵手2 小时前
从 Java 到 Kotlin:在现有项目中迁移的最佳实践!
java·python·kotlin
青春给了狗4 小时前
Android 14 系统统一修改app启动时图标大小和圆角
android
pengyu4 小时前
【Flutter 状态管理 - 柒】 | InheritedWidget:藏在组件树里的"魔法"✨
android·flutter·dart
居然是阿宋6 小时前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin