实现可以缩放的ImageView

主要用到了Matrix,ScaleGestureDetector,GestureDetector 

介绍

  • Matrix ,中文里叫矩阵,高等数学里有介绍。Android中的Matrix类是一个3x3的位置坐标矩阵,在图像处理方面,主要是用于平面的缩放、平移、旋转等操作。更详细的介绍参考:android matrix 最全方法详解与进阶(完整篇)-CSDN博客
  • ScaleGestureDetector,是专门用来检测两个手指在屏幕上做缩放的手势用的,最简单的应用就是用来缩放图片或者缩放网页。
  • GestureDetector ,实现滑动和双击监听

通过ScaleGestureDetector监听多指操作实现放大缩小,监听GestureDetector实现滑动与双击放大,具体的缩放、平移效果则是由Matrix实现,下面是效果图及代码展示

效果展示

代码实现

代码里有详细的注释,可以直接复制使用

class ZoomImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    androidx.appcompat.widget.AppCompatImageView(context, attrs),
    OnGlobalLayoutListener {

    private var mIsOneLoad = true

    //初始化的比例
    private var mInitScale = 0f

    //最小比例
    private var mMinScale = 0f

    //图片最大比例
    private var mMaxScale = 0f

    //双击能达到的最大比例
    private var mMidScale = 0f
    private var mScaleMatrix: Matrix? = null

    //捕获用户多点触控
    private var mScaleGestureDetector: ScaleGestureDetector? = null

    //移动
    private var gestureDetector: GestureDetector? = null

    //双击
    private var isEnlarge = false //是否放大
    private var mAnimator: ValueAnimator? = null //双击缩放动画

    //滚动
    private var scroller: OverScroller? = null
    private var mCurrentX = 0
    private var mCurrentY: Int = 0
    private var translationAnimation: ValueAnimator? = null //惯性移动动画

    //单击
    private var onClickListener: OnClickListener? = null //单击监听

    init {
        //记住,一定要把ScaleType设置成ScaleType.MATRIX,否则无法缩放
        scaleType = ScaleType.MATRIX

        scroller = OverScroller(context)
        mScaleMatrix = Matrix()
        //手势缩放
        mScaleGestureDetector =
            ScaleGestureDetector(context, object : SimpleOnScaleGestureListener() {
                override fun onScale(detector: ScaleGestureDetector): Boolean {
                    scale(detector)
                    return true
                }

                override fun onScaleEnd(detector: ScaleGestureDetector) {
                    scaleEnd(detector)
                }
            })
        //滑动和双击监听
        gestureDetector = GestureDetector(context, object : SimpleOnGestureListener() {
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                //滑动监听
                onTranslationImage(-distanceX, -distanceY)
                return true
            }

            override fun onDoubleTap(e: MotionEvent): Boolean {
                //双击监听
                onDoubleDrawScale(e.x, e.y)
                return true
            }

            override fun onFling(
                e1: MotionEvent,
                e2: MotionEvent,
                velocityX: Float,
                velocityY: Float
            ): Boolean {
                //滑动惯性处理
                mCurrentX = e2.x.toInt()
                mCurrentY = e2.y.toInt()
                val rectF = getMatrixRectF() ?: return false
                //startX为当前图片左边界的x坐标
                val startX = mCurrentX
                val startY = mCurrentY
                val vX = velocityX.roundToInt()
                val vY = velocityY.roundToInt()
                val maxX: Int = rectF.width().roundToInt()
                val maxY: Int = rectF.height().roundToInt()
                if (startX != maxX || startY != maxY) {
                    //调用fling方法,然后我们可以通过调用getCurX和getCurY来获得当前的x和y坐标
                    //这个坐标的计算是模拟一个惯性滑动来计算出来的,我们根据这个x和y的变化可以模拟出图片的惯性滑动
                    scroller?.fling(startX, startY, vX, vY, 0, maxX, 0, maxY, maxX, maxY)
                }
                if (translationAnimation != null && translationAnimation!!.isStarted)
                    translationAnimation!!.end()
                translationAnimation = ObjectAnimator.ofFloat(0f, 1f)
                translationAnimation?.setDuration(500)
                translationAnimation?.addUpdateListener {
                    if (scroller != null && scroller!!.computeScrollOffset()) {
                        //获得当前的x坐标
                        val newX = scroller!!.currX
                        val dx = newX - mCurrentX
                        mCurrentX = newX
                        //获得当前的y坐标
                        val newY = scroller!!.currY
                        val dy = newY - mCurrentY
                        mCurrentY = newY
                        //进行平移操作
                        if (dx != 0 && dy != 0) onTranslationImage(dx.toFloat(), dy.toFloat())
                    }
                }
                translationAnimation?.start()
                return super.onFling(e1, e2, velocityX, velocityY)
            }

            override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
                //单击事件
                onClickListener?.onClick(this@ZoomImageView)
                return true
            }
        })
    }

    override fun setOnClickListener(onClickListener: OnClickListener?) {
        this.onClickListener = onClickListener
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        viewTreeObserver.addOnGlobalLayoutListener(this)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        viewTreeObserver.removeOnGlobalLayoutListener(this)
    }

    override fun onGlobalLayout() {
        if (mIsOneLoad) {
            //得到控件的宽和高
            val width = width
            val height = height
            //获取图片,如果没有图片则直接退出
            val d = drawable ?: return
            //获取图片的宽和高
            val dw = d.intrinsicWidth
            val dh = d.intrinsicHeight
            var scale = 1.0f
            if (dw > width && dh <= height) {
                scale = width * 1.0f / dw
            }
            if (dw <= width && dh > height) {
                scale = height * 1.0f / dh
            }
            if (dw <= width && dh <= height || dw >= width && dh >= height) {
                scale = min(width * 1.0f / dw, height * 1.0f / dh)
            }
            //最小比例
            mMinScale = scale / 2
            //图片原始比例,图片回复原始大小时使用
            mInitScale = scale
            //图片双击后放大的比例
            mMidScale = scale * 2
            //手势放大时最大比例
            mMaxScale = scale * 4
            //设置移动数据,把改变比例后的图片移到中心点
            val translationX = width * 1.0f / 2 - dw / 2f
            val translationY = height * 1.0f / 2 - dh / 2f
            mScaleMatrix?.postTranslate(translationX, translationY)
            mScaleMatrix?.postScale(mInitScale, mInitScale, width * 1.0f / 2, height * 1.0f / 2)
            imageMatrix = mScaleMatrix
            mIsOneLoad = false
        }
    }

    //手势操作(缩放)
    fun scale(detector: ScaleGestureDetector) {
        drawable ?: return
        //获取手势操作的值,scaleFactor>1说明放大,<1则说明缩小
        val scaleFactor = detector.scaleFactor
        //获取手势操作后的比例,当放操作后比例在[mMinScale,mMaxScale]区间时允许放大
        mScaleMatrix!!.postScale(scaleFactor, scaleFactor, detector.focusX, detector.focusY)
        imageMatrix = mScaleMatrix
        removeBorderAndTranslationCenter()
    }

    //手势操作结束
    fun scaleEnd(detector: ScaleGestureDetector) {
        var scale: Float = getScale()
        scale *= detector.scaleFactor
        if (scale < mMinScale) {
            scaleAnimation(mMinScale, width / 2f, height / 2f)
        } else if (scale > mMaxScale) {
            scaleAnimation(mMaxScale, width / 2f, height / 2f)
        }
    }

    //手势操作(移动)
    private fun onTranslationImage(dx: Float, dy: Float) {
        var moveX = dx
        var moveY = dy
        if (drawable == null) return
        val rect: RectF? = getMatrixRectF()

        //图片宽度小于控件宽度时不允许左右移动
        if (rect != null && rect.width() <= width) moveX = 0.0f
        //图片高度小于控件宽度时,不允许上下移动
        if (rect != null && rect.height() <= height) moveY = 0.0f

        //移动距离等于0,那就不需要移动了
        if (moveX == 0.0f && moveY == 0.0f) return
        mScaleMatrix!!.postTranslate(moveX, moveY)
        imageMatrix = mScaleMatrix
        //去除移动边界
        removeBorderAndTranslationCenter()
    }

    //消除控件边界和把图片移动到中间
    private fun removeBorderAndTranslationCenter() {
        val rectF: RectF = getMatrixRectF() ?: return
        val width = width
        val height = height
        val widthF = rectF.width()
        val heightF = rectF.height()
        val left = rectF.left
        val right = rectF.right
        val top = rectF.top
        val bottom = rectF.bottom
        var translationX = 0.0f
        var translationY = 0.0f
        val centerTranslationX = width * 1.0f / 2f - (widthF / 2f + left)
        if (left > 0) {
            //左边有边界
            translationX = if (widthF > width) {
                //图片宽度大于控件宽度,移动到左边贴边
                -left
            } else {
                //图片宽度小于控件宽度,移动到中间
                centerTranslationX
            }
        } else if (right < width) {
            //右边有边界
            translationX = if (widthF > width) {
                //图片宽度大于控件宽度,移动到右边贴边
                width - right
            } else {
                //图片宽度小于控件宽度,移动到中间
                centerTranslationX
            }
        }
        val centerTranslationY = height * 1.0f / 2f - (top + heightF / 2f)
        if (top > 0) {
            //顶部有边界
            translationY = if (heightF > height) {
                //图片高度大于控件高度,去除顶部边界
                -top
            } else {
                //图片高度小于控件宽度,移动到中间
                centerTranslationY
            }
        } else if (bottom < height) {
            //底部有边界
            translationY = if (heightF > height) {
                //图片高度大于控件高度,去除顶部边界
                height - bottom
            } else {
                //图片高度小于控件宽度,移动到中间
                centerTranslationY
            }
        }
        mScaleMatrix!!.postTranslate(translationX, translationY)
        imageMatrix = mScaleMatrix
    }

    /**
     * 双击改变大小
     *
     * @param x 点击的中心点
     * @param y 点击的中心点
     */
    private fun onDoubleDrawScale(x: Float, y: Float) {
        //如果缩放动画已经在执行,那就不执行任何事件
        if (mAnimator != null && mAnimator!!.isRunning) return
        val drawScale: Float = getDoubleDrawScale()
        //执行动画缩放,不然太难看了
        scaleAnimation(drawScale, x, y)
    }

    /**
     * 缩放动画
     *
     * @param drawScale 缩放的比例
     * @param x         中心点
     * @param y         中心点
     */
    private fun scaleAnimation(drawScale: Float, x: Float, y: Float) {
        if (mAnimator != null && mAnimator!!.isRunning) return
        mAnimator = ObjectAnimator.ofFloat(getScale(), drawScale)
        mAnimator?.setDuration(500)
        mAnimator?.interpolator = AccelerateInterpolator()
        mAnimator?.addUpdateListener { animation: ValueAnimator ->
            val value: Float = animation.animatedValue as Float / getScale()
            mScaleMatrix!!.postScale(value, value, x, y)
            imageMatrix = mScaleMatrix
            removeBorderAndTranslationCenter()
        }
        mAnimator?.start()
    }

    //返回双击后改变的大小比例(我们希望缩放误差在deviation范围内)
    private fun getDoubleDrawScale(): Float {
        val deviation = 0.05f
        val drawScale: Float
        var scale: Float = getScale()
        if (abs(mMinScale - scale) < deviation) scale = mMinScale
        if (abs(mInitScale - scale) < deviation) scale = mInitScale
        if (abs(mMidScale - scale) < deviation) scale = mMidScale
        if (abs(mMaxScale - scale) < deviation) scale = mMaxScale
        if (scale != mMidScale) {
            //当前大小不等于mMidScale,则调整到mMidScale
            drawScale = mMidScale
            isEnlarge = scale < mMidScale
        } else {
            //如果等于mMidScale,则判断放大或者缩小
            //判断是放大或者缩小,如果上次是放大,则继续放大,缩小则继续缩小
            drawScale = if (isEnlarge) {
                //放大
                mMaxScale
            } else {
                //缩小
                mMinScale
            }
        }
        return drawScale
    }

    //获取图片宽高以及左右上下边界
    private fun getMatrixRectF(): RectF? {
        val drawable = drawable ?: return null
        val rectF = RectF(0f, 0f, drawable.minimumWidth.toFloat(), drawable.minimumHeight.toFloat())
        val matrix = imageMatrix
        matrix.mapRect(rectF)
        return rectF
    }

    /**
     * 获取当前图片的缩放值
     */
    private fun getScale(): Float {
        val values = FloatArray(9)
        mScaleMatrix!!.getValues(values)
        return values[Matrix.MSCALE_X]
    }

    /**
     * 解决和父控件滑动冲突 只要图片边界超过控件边界,返回true
     * @return true 禁止父控件滑动
     */
    override fun canScrollHorizontally(direction: Int): Boolean {
        val rect = getMatrixRectF()
        if (rect == null || rect.isEmpty) return false
        return if (direction > 0) {
            rect.right >= width + 1
        } else {
            rect.left <= -1
        }
    }

    override fun canScrollVertically(direction: Int): Boolean {
        val rect = getMatrixRectF()
        if (rect == null || rect.isEmpty) return false
        return if (direction > 0) {
            rect.bottom >= height + 1
        } else {
            rect.top <= -1
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return mScaleGestureDetector!!.onTouchEvent(event) or
                gestureDetector!!.onTouchEvent(event)
    }

}
<com.zhou.common.view.image.ZoomImageView
        android:id="@+id/iv_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@mipmap/icon_add"/>

源码地址:GitHub - zhoulei519/MyTest

demo中还有很多其他小工具,欢迎查看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值