查看原文
其他

大图预览追求极致,对PhotoView进行一波优化

dafay 鸿洋
2024-08-24

本文作者


作者:dafay

链接:

https://www.dafaycoding.com/article/android-basic-zoomimageview

本文由作者授权发布。


1背景


ZoomImageView 是一个自定义的 ImageView 控件,用于实现对图片的手势缩放、双击缩放以及放大后的平移查看等功能。在我之前的 MeetPhoto 项目中,图片预览功能使用了一个开源 ZoomImageView  控件(这个控件基于 PhotoView 实现,因其代码量较少而选择它)。但我发现这个控件在某些方面的用户体验并不理想,所以我决定对其进行优化,便是这篇博客的由来。

https://github.com/idea007/MeetPhoto

https://github.com/tenthbitinc/ZoomImageView?tab=readme-ov-file

https://github.com/Baseflow/PhotoView


其中两个体验问题见下图:

问题一:高清图切换底清图时被重置

问题二:双击缩放到最小动画不自然


2实现步骤


思路


本篇博客中 ZoomImageView 的实现思路可以简化为下面这个算式:
imageMatrix=suppMatrix∗originMatrix
1. 实现放大和拖动的功能
首先设置 scaleType="matrix" ,这样 ImageView 绘图时才使用图像矩阵 imageMatrix ,添加手势检测器 GestureDetector,帮助我们捕获到双击和拖动事件,然后处理这些事件。
/**
 * 实现功能
 * 1. scaleType = MATRIX
 * 1. 图片双击放大或缩小,点击点为缩放中心点(支点)
 * 2. 拖动
 */

class Zoom01ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    private val gestureDetector: GestureDetector
    private val originMatrix = Matrix() // 默认单位矩阵
    private val suppMatrix = Matrix()  // 默认单位矩阵
    private var minZoom = DEFAULT_MIN_ZOOM
    private var maxZoom = DEFAULT_MAX_ZOOM
    private var zoomAnim = ValueAnimator().apply { duration = DEFAULT_ANIM_DURATION }
    private val pivotPointF = PointF(0f, 0f)

    init {
        scaleType = ScaleType.MATRIX
        gestureDetector = GestureDetector(context, MySimpleOnGestureListener())
        setOnTouchListener(object : OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                return gestureDetector.onTouchEvent(event)
            }
        })
    }

    /**
     * 处理双击事件,双击执行缩放动画
     */

    private fun dealOnDoubleTap(e: MotionEvent) {
        zoomAnim.removeAllUpdateListeners()
        zoomAnim.cancel()
        // 点击的点设置为缩放的中心点
        pivotPointF.set(e.x, e.y)
        val animatorUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator) {
                val tempValue = animation.animatedValue as Float
                suppMatrix.zoomTo(tempValue, pivotPointF.x, pivotPointF.y)
                applyToImageMatrix()
            }
        }
        val currZoom = suppMatrix.scaleX()
        val endZoom = if (Math.abs(currZoom - maxZoom) > Math.abs(currZoom - minZoom)) maxZoom else minZoom
        zoomAnim.setFloatValues(currZoom, endZoom)
        zoomAnim.addUpdateListener(animatorUpdateListener)
        zoomAnim.start()
    }

    /**
     * 处理拖动(平移)事件
     */

    private fun dealOnScroll(distanceX: Float, distanceY: Float) {
        suppMatrix.translateBy(-distanceX, -distanceY)
        applyToImageMatrix()
    }

    /**
     * 应用于 ImageView 的 matrix,为了思路清晰,这里先频繁创建对象 drawMatrix
     */

    fun applyToImageMatrix() {
        val drawMatrix = Matrix()
        drawMatrix.set(originMatrix)
        // drawMatrix = suppMatrix * originMatrix
        drawMatrix.postConcat(suppMatrix)
        imageMatrix = drawMatrix
    }

    inner class MySimpleOnGestureListener : GestureDetector.SimpleOnGestureListener() {
        override fun onDown(e: MotionEvent)Boolean {
            // 返回 true,GestureDetector 一系列手势才能响应
            return true
        }

        override fun onDoubleTap(e: MotionEvent)Boolean {
            dealOnDoubleTap(e)
            return super.onDoubleTap(e)
        }

        override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float)Boolean {
            dealOnScroll(distanceX, distanceY)
            return super.onScroll(e1, e2, distanceX, distanceY)
        }
    }
}


方便矩阵操作,对其添加扩展函数。
// 为了思路清晰,这里数组没有提取处理
fun Matrix.scaleX()Float {
    val values = FloatArray(9)
    this.getValues(values)
    return values[Matrix.MSCALE_X]
}

fun Matrix.scaleY()Float {
    val values = FloatArray(9)
    this.getValues(values)
    return values[Matrix.MSCALE_Y]
}

fun Matrix.translateBy(dx: Float, dy: Float) {
    this.postTranslate(dx, dy)
}

fun Matrix.translateTo(x: Float, y: Float) {
    this.postTranslate(-this.transX() + x, -this.transY() + y)
}

fun Matrix.zoomBy(factor: Float, pivotX: Float, pivotY: Float) {
    this.postScale(factor, factor, pivotX, pivotY)
}

fun Matrix.zoomTo(zoom: Float, pivotX: Float, pivotY: Float) {
    this.postScale(zoom / this.scaleX(), zoom / this.scaleY(), pivotX, pivotY)
}


 2. 添加双指捏合操作
添加 ScaleGestureDetector ,修改 ImageView#onTouch ,同时响应 GestureDetector ScaleGestureDetector,在缩放事件里添加矩阵缩放的操作。
/**
 * 实现功能
 * 1. 支持双指缩放
 * 问题:
 * 1. scaleGestureDetector、gestureDetector 响应
 * 2. 可添加 onScroll、onScale 的触发 scaledTouchSlop 限定
 */

class Zoom02ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    // 手势检测器
    private val gestureDetector: GestureDetector
    private val scaleGestureDetector: ScaleGestureDetector
      ...

    init {
            // scaleType 图像边界缩放到此视图边界的选项,MATRIX 绘图时使用图像矩阵进行缩放
        scaleType = ScaleType.MATRIX
        val multiGestureDetector = MultiGestureDetector()
        gestureDetector = GestureDetector(context, multiGestureDetector)
        scaleGestureDetector = ScaleGestureDetector(context, multiGestureDetector)
        setOnTouchListener(object : View.OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                if (gestureDetector.onTouchEvent(event)) {
                    return true
                }
                return scaleGestureDetector.onTouchEvent(event)
            }
        })
    }
        ...

    /**
     * 处理双指缩放事件
     */

    private fun dealOnScale(detector: ScaleGestureDetector) {
        val currScale: Float = suppMatrix.scaleX()
        var scaleFactor = detector.scaleFactor
        if ((currScale >= maxZoom && scaleFactor > 1f) || (currScale <= minZoom && scaleFactor < 1f)) {
            return
        }
        suppMatrix.zoomBy(scaleFactor, detector.focusX, detector.focusY)
        applyToImageMatrix()
    }
        ...

        /**
         * 同时实现 SimpleOnGestureListener、OnScaleGestureListener 接口
         */

    inner class MultiGestureDetector : GestureDetector.SimpleOnGestureListener(),
        ScaleGestureDetector.OnScaleGestureListener {

        override fun onDoubleTap(e: MotionEvent)Boolean {
            dealOnDoubleTap(e)
            return super.onDoubleTap(e)
        }

        override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float)Boolean {
            dealOnScroll(distanceX, distanceY)
            return super.onScroll(e1, e2, distanceX, distanceY)
        }

        override fun onScale(detector: ScaleGestureDetector)Boolean {
            dealOnScale(detector)
            return true
        }

        override fun onScaleBegin(detector: ScaleGestureDetector)Boolean {
            return true
        }

        override fun onScaleEnd(detector: ScaleGestureDetector) {
        }
    }
}


 3. 初始化处理
ImageView 初始化过程中,以类似 fitCenter (保证图片完整显示,图片高或宽按比例放缩到 View 的高或宽,居中显示)显示模式初始化 originMatrix 矩阵。
/**
 * 实现功能
 * 1. 初始化处理
 * 问题:
 * 1. 同一张图片的高清、低清切换的问题(图片宽高比一样),例如执行放大过程中切换
 */

class Zoom03ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    // 手势检测器
    private val gestureDetector: GestureDetector
    private val scaleGestureDetector: ScaleGestureDetector

    // 默认类似 fitCenter 显示效果时的矩阵
    private val originMatrix = Matrix()
    private val suppMatrix = Matrix()
    private var minZoom = DEFAULT_MIN_ZOOM
    private var maxZoom = DEFAULT_MAX_ZOOM
    private var zoomAnim = ValueAnimator().apply { duration = DEFAULT_ANIM_DURATION }
    private val pivotPointF = PointF(0f, 0f)

    init {
        val multiGestureDetector = MultiGestureDetector()
        gestureDetector = GestureDetector(context, multiGestureDetector)
        scaleGestureDetector = ScaleGestureDetector(context, multiGestureDetector)
        setOnTouchListener(object : View.OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                if (gestureDetector.onTouchEvent(event)) {
                    return true
                }
                return scaleGestureDetector.onTouchEvent(event)
            }
        })
    }

    override fun setImageDrawable(drawable: Drawable?) {
        super.setImageDrawable(drawable)
        updateOriginMatrix(drawable)
    }

    override fun setImageBitmap(bm: Bitmap?) {
        super.setImageBitmap(bm)
        updateOriginMatrix(drawable)
    }

    override fun setImageResource(resId: Int) {
        super.setImageResource(resId)
        updateOriginMatrix(drawable)
    }

    override fun setImageURI(uri: Uri?) {
        super.setImageURI(uri)
        updateOriginMatrix(drawable)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        updateOriginMatrix(drawable)
    }

    /**
     * 计算图片显示类似 fitCenter 效果时的矩阵(忽视 pading,只处理 fitCenter 这一种显示模式)
     */

    private fun updateOriginMatrix(drawable: Drawable?) {
        drawable ?: return
        if (width <= 0) {
            return
        }
        val viewWidth = width.toFloat()
        val viewHeight = height.toFloat()
        val drawableWidth = drawable.intrinsicWidth
        val drawableHeight = drawable.intrinsicHeight
        originMatrix.reset()
        val tempSrc = RectF(0f, 0f, drawableWidth.toFloat(), drawableHeight.toFloat())
        val tempDst = RectF(0f, 0f, viewWidth, viewHeight)
        originMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER)
        applyToImageMatrix()
    }

   ...
}


由于图片切换时只会切换 originMatrix,对应缩放、平移的操作都记录在 suppMatrix,初始显示效果是一样的 “fitCenter”——图片尺寸放大相应的 MSCALE_X 值同比例变小,最终的 imageMatrix 显示效果并不会改变,基于此便解决了第一个体验问题。如下图,在图片放大过程中切换同比例的高清图,显示效果是一致的。


 4. 边界处理
在把 suppMatrix* originMatrix 赋值给 imageMatrix 之前,先对边界进行矫正,左上右下移动超出边界时通过 suppMatrix 平移抵消掉,矫正之后再应用于 imageMatrix 更新绘图。
/**
 * 实现功能
 * 边界处理,左上右下移动超出边界时进行矫正
 */

class Zoom04ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    ...

    /**
     * 在显示之前,进行边界矫正,对 suppMatrix 进行调整
     */

    private fun correctSuppMatrix() {
        // 目标 matrix
        val tempMatrix = Matrix(originMatrix).apply { postConcat(suppMatrix) }
        // 得到 matrix 的 rect
        val tempRectF = getDrawMatrixRect(tempMatrix)
        tempRectF ?: return
        var deltaX = 0f
        var deltaY = 0f

        if (tempRectF.height() < height) {
            deltaY = ((height - tempRectF.height()) / 2) - tempRectF.top
        } else if (tempRectF.top > 0) {
            deltaY = -tempRectF.top
        } else if (tempRectF.bottom < height) {
            deltaY = height - tempRectF.bottom
        }

        if (tempRectF.width() <= width) {
            deltaX = ((width - tempRectF.width()) / 2) - tempRectF.left
        } else if (tempRectF.left > 0) {
            deltaX = -tempRectF.left
        } else if (tempRectF.right < width) {
            deltaX = width - tempRectF.right
        }
        suppMatrix.translateBy(deltaX, deltaY)
    }

    private fun getDrawMatrixRect(matrix: Matrix): RectF? {
        val d = drawable
        if (null != d) {
            val tempRect = RectF()
            // 什么新奇的写法
            tempRect[0f, 0f, d.intrinsicWidth.toFloat()] = d.intrinsicHeight.toFloat()
            matrix.mapRect(tempRect)
            return tempRect
        }
        return null
    }

    /**
     * 应用于 ImageView 的 matrix,为了思路清晰,这里先频繁创建对象 drawMatrix
     */

    private fun applyToImageMatrix() {
        // 在应用之前,进行边界矫正
        correctSuppMatrix()
        val drawMatrix = Matrix()
        drawMatrix.set(originMatrix)
        // drawMatrix = suppMatrix * originMatrix
        drawMatrix.postConcat(suppMatrix)
        debug("originMatrix:${originMatrix} suppMatrix:${suppMatrix} drawMatrix:${drawMatrix}")
        imageMatrix = drawMatrix
    }

    ...
}


 5. 添加 fling 效果

图片放大可拖动时,快速滑动响应 onFling 事件,基于 Android Scroller 滚动特性进行处理。

https://www.dafaycoding.com/article/android-basic-scroller


/**
 * 实现功能
 * 快速滑动 fling 处理
 */

class Zoom05ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    ...

    // 用来执行 onFling 动画
    private var overScroller: OverScroller
    private var startTime: Long = 0
    private val choreographer = Choreographer.getInstance()
    private var currentX = 0
    private var currentY = 0

    init {
        overScroller = OverScroller(context)
        scaleType = ScaleType.MATRIX
        val multiGestureDetector = MultiGestureDetector()
        gestureDetector = GestureDetector(context, multiGestureDetector)
        scaleGestureDetector = ScaleGestureDetector(context, multiGestureDetector)
        setOnTouchListener(object : View.OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                if (gestureDetector.onTouchEvent(event)) {
                    return true
                }
                return scaleGestureDetector.onTouchEvent(event)
            }
        })
    }

    ...

    private fun dealOnFling(e2: MotionEvent, velocityX: Float, velocityY: Float) {
        val rect = getDrawMatrixRect(imageMatrix) ?: return
        val startX = Math.round(-rect.left)
        val minX: Int
        val maxX: Int
        val minY: Int
        val maxY: Int
        if (width < rect.width()) {
            minX = 0
            maxX = Math.round(rect.width() - width)
        } else {
            maxX = startX
            minX = maxX
        }
        val startY = Math.round(-rect.top)
        if (height < rect.height()) {
            minY = 0
            maxY = Math.round(rect.height() - height)
        } else {
            maxY = startY
            minY = maxY
        }
        currentX = startX
        currentY = startY

        if (!((startX != maxX) || (startY != maxY))) {
            return
        }
        overScroller.fling(startX, startY, -velocityX.toInt(), -velocityY.toInt(), minX, maxX, minY, maxY, 00)
        startFlingAnim()
    }

    private fun startFlingAnim() {
        startTime = AnimationUtils.currentAnimationTimeMillis()
        postNextFrame()
    }

    private fun postNextFrame() {
        if (overScroller.computeScrollOffset()) {
            val currX = overScroller.currX
            val currY = overScroller.currY
            suppMatrix.translateBy((currentX - currX).toFloat(), (currentY - currY).toFloat())
            applyToImageMatrix()
            currentX = currX
            currentY = currY
            choreographer.postFrameCallback {
                postNextFrame()
            }
        }
    }

    ...

    inner class MultiGestureDetector : GestureDetector.SimpleOnGestureListener(),
        ScaleGestureDetector.OnScaleGestureListener {

        override fun onDoubleTap(e: MotionEvent)Boolean {
            dealOnDoubleTap(e)
            return super.onDoubleTap(e)
        }

        override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float)Boolean {
            dealOnScroll(distanceX, distanceY)
            return super.onScroll(e1, e2, distanceX, distanceY)
        }

        override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float)Boolean {
            dealOnFling(e2, velocityX, velocityY)
            return super.onFling(e1, e2, velocityX, velocityY)
        }

        override fun onScale(detector: ScaleGestureDetector)Boolean {
            dealOnScale(detector)
            return true
        }

        override fun onScaleBegin(detector: ScaleGestureDetector)Boolean {
            return true
        }

        override fun onScaleEnd(detector: ScaleGestureDetector) {
        }
    }

}


 6. 解决缩放动画过渡不自然的问题
动画执行前后 pivot 支点位置变化和边界矫正的原因,导致开头第二个体验不好的问题,修改动画的实现来解决这个问题。
动画开始前,计算终止时的 endMatrix,对 endMatrix 矫正后求得 endPivotPointF 支点的位置,这样动画行进过程中,便可同步改变  pivot 的位置,同时动画执行过程中,跳过边界矫正。
这样做的副作用很明显,我们已经计算出了可用来绘图的 Matrix,却又要反回去计算出 suppMatrix,导致额外的计算量,如果过程不优雅,那结果一定不优雅,这种实现思路并不好。


/**
 * 实现功能
 * 1. 解决双击缩放到最小过程中,动画过渡不自然的问题(边界矫正太过生硬),
 * 解决思路:缩放动画,起始 matrix 和 终止 matrix 过程中,povit 支点也要随着动画进度做改变
 */

class Zoom06ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

   ...

    /**
     * 处理双击事件,双击执行缩放动画
     */

    private fun dealOnDoubleTap(e: MotionEvent) {
        playZoomAnim(e.x, e.y)
    }

    /**
     * 改变动画实现方式
     */

    fun playZoomAnim(pivotX: Float, pivotY: Float) {
        zoomAnim.removeAllUpdateListeners()
        zoomAnim.cancel()

        // 点击的点设置为缩放的支点
        pivotPointF.set(pivotX, pivotY)
        val startZoom = suppMatrix.scaleX()
        val endZoom = if (Math.abs(startZoom - maxZoom) > Math.abs(startZoom - minZoom)) maxZoom else minZoom

        val startMatrix = Matrix(imageMatrix)
        val endMatrix = Matrix(originMatrix).apply {
            val tempSuppMatrix = Matrix(suppMatrix)
            tempSuppMatrix.zoomTo(endZoom, pivotPointF.x, pivotPointF.y)
            this.postConcat(tempSuppMatrix)
        }
        // 边界矫正
        correctByViewBound(endMatrix).let {
            endMatrix.translateBy(it.x, it.y)
        }

        val tmpPointArr = floatArrayOf(pivotX, pivotY)
        MathUtils.computeNewPosition(tmpPointArr, imageMatrix, endMatrix)
        val endPivotPointF = PointF(tmpPointArr[0], tmpPointArr[1])

        val animatorUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator) {
                val tempValue = animation.animatedValue as Float
                val factor = (tempValue - startZoom) / (endZoom - startZoom)
                debug("playZoomAnim factor=${factor}")
                val currMatrix = MathUtils.interpolate(
                    startMatrix,
                    pivotPointF.x,
                    pivotPointF.y,
                    endMatrix,
                    endPivotPointF.x,
                    endPivotPointF.y,
                    factor
                )
                // suppMatrix * originMatrix = currMatrix;  suppMatrix = currMatrix *(originMatrix 的逆矩阵)
                val tmpMatrix = Matrix()
                originMatrix.invert(tmpMatrix)
                tmpMatrix.postConcat(currMatrix)
                suppMatrix.set(tmpMatrix)
                applyToImageMatrix(true)
            }
        }
        zoomAnim.setFloatValues(startZoom, endZoom)
        zoomAnim.addUpdateListener(animatorUpdateListener)
        zoomAnim.start()
    }

    ...

    /**
     * 对输入矩阵,依据 View 宽高进行调整,输出需要调整的平移量
     */

    private fun correctByViewBound(srcMatrix: Matrix): PointF {
        val tempPointF = PointF()
        // 得到 matrix 的 rect
        val tempRectF = getDrawMatrixRect(srcMatrix)
        tempRectF ?: return tempPointF
        var deltaX = 0f
        var deltaY = 0f
        if (tempRectF.height() < height) {
            deltaY = ((height - tempRectF.height()) / 2) - tempRectF.top
        } else if (tempRectF.top > 0) {
            deltaY = -tempRectF.top
        } else if (tempRectF.bottom < height) {
            deltaY = height - tempRectF.bottom
        }
        if (tempRectF.width() <= width) {
            deltaX = ((width - tempRectF.width()) / 2) - tempRectF.left
        } else if (tempRectF.left > 0) {
            deltaX = -tempRectF.left
        } else if (tempRectF.right < width) {
            deltaX = width - tempRectF.right
        }
        tempPointF.set(deltaX, deltaY)
        return tempPointF
    }

    private fun getDrawMatrixRect(matrix: Matrix): RectF? {
        val d = drawable
        if (null != d) {
            val tempRect = RectF()
            // 新奇的写法
            tempRect[0f, 0f, d.intrinsicWidth.toFloat()] = d.intrinsicHeight.toFloat()
            debug("getDrawMatrixRect tempRect=${tempRect}")
            matrix.mapRect(tempRect)
            debug("getDrawMatrixRect tempRect=${tempRect}")
            return tempRect
        }
        return null
    }

    /**
     * 在显示之前,进行边界矫正,对 suppMatrix 进行调整
     */

    private fun correctSuppMatrix() {
        // 目标 matrix
        val tempMatrix = Matrix(originMatrix).apply { postConcat(suppMatrix) }
        correctByViewBound(tempMatrix).let {
            suppMatrix.translateBy(it.x, it.y)
        }
    }

    /**
     * 应用于 ImageView 的 matrix,为了思路清晰,这里先频繁创建对象 drawMatrix
     */

    private fun applyToImageMatrix(skipCorrect: Boolean = false) {
        if (!skipCorrect) {
            // 在应用之前,进行边界矫正
            correctSuppMatrix()
        }
        val drawMatrix = Matrix()
        drawMatrix.set(originMatrix)
        // 即当前 Matrix 会乘以传入的 Matrix。 suppMatrix*originMatrix
        drawMatrix.postConcat(suppMatrix)
        imageMatrix = drawMatrix
    }

    ...
}


 7. Over Zoom 处理
在双指捏是允许 zoom < minZoom ,并在手指抬起时,通过动画还原到 minZoom 。
/**
 * 实现功能
 * over zoom 处理(< minZoom)
 */

class Zoom07ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    ...

    private var overMinZoomRadio = 0.75f

    init {
        overScroller = OverScroller(context)
        scaleType = ScaleType.MATRIX
        val multiGestureDetector = MultiGestureDetector()
        gestureDetector = GestureDetector(context, multiGestureDetector)
        scaleGestureDetector = ScaleGestureDetector(context, multiGestureDetector)
        setOnTouchListener(object : View.OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                if (gestureDetector.onTouchEvent(event)) {
                    return true
                }
                scaleGestureDetector.onTouchEvent(event)
                if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
                    dealUpOrCancel(event)
                }
                return true
            }
        })
    }

    ...
        /**
         * up or cancel 事件时执行还原缩放动画
         */

    private fun dealUpOrCancel(event: MotionEvent) {
        val currZoom = suppMatrix.scaleX()
        if (currZoom < minZoom) {
            val rect = getDrawMatrixRect(imageMatrix)
            rect?.let { playZoomAnim(currZoom, minZoom, it.centerX(), it.centerY()) }
        }
    }

    private fun playZoomAnim(startZoom: Float, endZoom: Float, pivotX: Float, pivotY: Float) {
        zoomAnim.removeAllUpdateListeners()
        zoomAnim.cancel()

        // 点击的点设置为缩放的支点
        pivotPointF.set(pivotX, pivotY)
        val startMatrix = Matrix(imageMatrix)
        val endMatrix = Matrix(originMatrix).apply {
            val tempSuppMatrix = Matrix(suppMatrix)
            tempSuppMatrix.zoomTo(endZoom, pivotPointF.x, pivotPointF.y)
            this.postConcat(tempSuppMatrix)
        }
        // 边界矫正
        correctByViewBound(endMatrix).let {
            endMatrix.translateBy(it.x, it.y)
        }

        val tmpPointArr = floatArrayOf(pivotX, pivotY)
        MathUtils.computeNewPosition(tmpPointArr, imageMatrix, endMatrix)
        val endPivotPointF = PointF(tmpPointArr[0], tmpPointArr[1])

        val animatorUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator) {
                val tempValue = animation.animatedValue as Float
                val factor = (tempValue - startZoom) / (endZoom - startZoom)
                val currMatrix = MathUtils.interpolate(
                    startMatrix,
                    pivotPointF.x,
                    pivotPointF.y,
                    endMatrix,
                    endPivotPointF.x,
                    endPivotPointF.y,
                    factor
                )
                // suppMatrix * originMatrix = currMatrix;  suppMatrix = currMatrix *(originMatrix 的逆矩阵)
                val tmpMatrix = Matrix()
                originMatrix.invert(tmpMatrix)
                tmpMatrix.postConcat(currMatrix)
                suppMatrix.set(tmpMatrix)
                applyToImageMatrix(true)
            }
        }
        zoomAnim.setFloatValues(startZoom, endZoom)
        zoomAnim.addUpdateListener(animatorUpdateListener)
        zoomAnim.start()
    }

      ... 

    /**
     * 处理双指缩放
     */

    private fun dealOnScale(detector: ScaleGestureDetector) {
        val currScale: Float = suppMatrix.scaleX()
        var scaleFactor = detector.scaleFactor
        if ((currScale >= maxZoom && scaleFactor > 1f) || (currScale <= minZoom * overMinZoomRadio && scaleFactor < 1f)) {
            return
        }
        suppMatrix.zoomBy(scaleFactor, detector.focusX, detector.focusY)
        applyToImageMatrix()
    }
    ...
}


 8. 嵌套在滚动控件内时事件冲突处理
事件冲突总体处理起来很轻松,down 事件时设置parent.requestDisallowInterceptTouchEvent(true) 优先由 ZoomImageView 响应处理,当 ZoomImageView 滑动到边界时设置parent.requestDisallowInterceptTouchEvent(false),在交由父级控件处理。
另外双指捏和与父级滚动控件谁先响应的冲突,这里直接简单处理,触屏手指数 >1 都由 ZoomImageView 响应。
/**
 * 实现功能
 * 嵌套冲突处理
 */

class Zoom08ImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    // 手势检测器
    private val gestureDetector: GestureDetector
    private val scaleGestureDetector: ScaleGestureDetector

    ...

    private var scrollEdge = Edge.EDGE_NONE
    private var allowParentInterceptOnEdge = true
    private var pointerCount = 0
    var viewTapListener: OnViewTapListener? = null

    init {
        overScroller = OverScroller(context)
        scaleType = ScaleType.MATRIX
        val multiGestureDetector = MultiGestureDetector()
        gestureDetector = GestureDetector(context, multiGestureDetector)
        scaleGestureDetector = ScaleGestureDetector(context, multiGestureDetector)
        setOnTouchListener(object : View.OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                if (gestureDetector.onTouchEvent(event)) {
                    return true
                }
                scaleGestureDetector.onTouchEvent(event)
                pointerCount = event.pointerCount
                when (event.actionMasked) {
                    MotionEvent.ACTION_DOWN -> {
                        parent?.requestDisallowInterceptTouchEvent(true)
                    }

                    MotionEvent.ACTION_UP,
                    MotionEvent.ACTION_CANCEL -> {
                        dealUpOrCancel(event)
                    }
                }
                return true
            }
        })
    }

   ...

    /**
     * 处理拖动(平移)事件
     */

    private fun dealOnScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float) {
        if (allowParentInterceptOnEdge && !scaleGestureDetector.isInProgress) {
            if (pointerCount < 2) {
                if (scrollEdge == Edge.EDGE_BOTH || (scrollEdge == Edge.EDGE_LEFT && -distanceX >= 1f) || (scrollEdge == Edge.EDGE_RIGHT && -distanceX <= -1f)) {
                    parent?.requestDisallowInterceptTouchEvent(false)
                }
            }
        }
        suppMatrix.translateBy(-distanceX, -distanceY)
        applyToImageMatrix()
    }

   ... 
    /**
     * 对输入矩阵,依据 View 宽高进行调整,输出需要调整的平移量
     */

    private fun correctByViewBound(srcMatrix: Matrix): PointF {
        val tempPointF = PointF()
        // 得到 matrix 的 rect
        val tempRectF = getDrawMatrixRect(srcMatrix)
        tempRectF ?: return tempPointF
        var deltaX = 0f
        var deltaY = 0f
        if (tempRectF.height() < height) {
            deltaY = ((height - tempRectF.height()) / 2) - tempRectF.top
        } else if (tempRectF.top > 0) {
            deltaY = -tempRectF.top
        } else if (tempRectF.bottom < height) {
            deltaY = height - tempRectF.bottom
        }
        if (tempRectF.width() <= width) {
            deltaX = ((width - tempRectF.width()) / 2) - tempRectF.left
            scrollEdge = Edge.EDGE_BOTH
        } else if (tempRectF.left > 0) {
            deltaX = -tempRectF.left
            scrollEdge = Edge.EDGE_LEFT
        } else if (tempRectF.right < width) {
            deltaX = width - tempRectF.right
            scrollEdge = Edge.EDGE_RIGHT
        } else {
            scrollEdge = Edge.EDGE_NONE
        }
        tempPointF.set(deltaX, deltaY)
        return tempPointF
    }

    ...
}


9. 规范代码,细节调优


对一些变量命名、代码注释等进行规范,最终完整代码如下:
class ZoomImageView @kotlin.jvm.JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    // 手势检测器
    private val gestureDetector: GestureDetector
    private val scaleGestureDetector: ScaleGestureDetector

    // 默认类似 fitCenter 显示模式时的矩阵
    private val originMatrix = Matrix()
    private val suppMatrix = Matrix()

    // 绘画前辅助计算用
    private val tempMatrix = Matrix()
    private var minZoom = DEFAULT_MIN_ZOOM
    private var midZoom = DEFAULT_MID_ZOOM
    private var maxZoom = DEFAULT_MAX_ZOOM
    private var zoomAnim = ValueAnimator().apply { duration = DEFAULT_ANIM_DURATION }
    private val pivotPointF = PointF(0f, 0f)

    // 用来执行 onFling 动画
    private var overScroller: OverScroller
    private var startTime: Long = 0
    private val choreographer = Choreographer.getInstance()
    private var currentX = 0
    private var currentY = 0
    private var overMinZoomRadio = 0.75f

    private var scrollEdge = Edge.EDGE_NONE
    private var allowParentInterceptOnEdge = true
    private var pointerCount = 0
    var viewTapListener: OnViewTapListener? = null

    init {
        overScroller = OverScroller(context)
        scaleType = ScaleType.MATRIX
        val multiGestureDetector = MultiGestureDetector()
        gestureDetector = GestureDetector(context, multiGestureDetector)
        scaleGestureDetector = ScaleGestureDetector(context, multiGestureDetector)
        setOnTouchListener(object : View.OnTouchListener {
            override fun onTouch(v: View, event: MotionEvent)Boolean {
                if (gestureDetector.onTouchEvent(event)) {
                    return true
                }
                scaleGestureDetector.onTouchEvent(event)
                pointerCount = event.pointerCount
                when (event.actionMasked) {
                    MotionEvent.ACTION_DOWN -> {
                        parent?.requestDisallowInterceptTouchEvent(true)
                    }

                    MotionEvent.ACTION_UP,
                    MotionEvent.ACTION_CANCEL -> {
                        dealUpOrCancel(event)
                    }
                }
                return true
            }
        })
    }

    override fun setImageDrawable(drawable: Drawable?) {
        super.setImageDrawable(drawable)
        updateOriginMatrix(drawable)
    }

    override fun setImageBitmap(bm: Bitmap?) {
        super.setImageBitmap(bm)
        updateOriginMatrix(drawable)
    }

    override fun setImageResource(resId: Int) {
        super.setImageResource(resId)
        updateOriginMatrix(drawable)
    }

    override fun setImageURI(uri: Uri?) {
        super.setImageURI(uri)
        updateOriginMatrix(drawable)
    }

    fun setMinZoom(minZoom: Float) {
        checkZoomLevels(minZoom, midZoom, maxZoom)
        this.minZoom = minZoom
    }

    fun setMidZoom(midZoom: Float) {
        checkZoomLevels(minZoom, midZoom, maxZoom)
        this.midZoom = midZoom
    }

    fun setMaxZoom(maxZoom: Float) {
        checkZoomLevels(minZoom, midZoom, maxZoom)
        this.maxZoom = maxZoom
    }

    private fun checkZoomLevels(minZoom: Float, midZoom: Float, maxZoom: Float) {
        if (minZoom >= midZoom) {
            throw IllegalArgumentException("MinZoom should be less than MidZoom")
        } else if (midZoom >= maxZoom) {
            throw IllegalArgumentException("MidZoom should be less than MaxZoom")
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val newHeight: Int = ScreenUtils.getFullScreenHeight()
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY))
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        updateOriginMatrix(drawable)
    }

    /**
     * 计算图片显示类似 fitCenter 效果时的矩阵(忽视 pading,只处理 fitCenter 这一种显示模式)
     */

    private fun updateOriginMatrix(drawable: Drawable?) {
        drawable ?: return
        if (width <= 0) {
            return
        }
        val viewWidth = width.toFloat()
        val viewHeight = height.toFloat()
        val drawableWidth = drawable.intrinsicWidth
        val drawableHeight = drawable.intrinsicHeight
        originMatrix.reset()
        val tempSrc = RectF(0f, 0f, drawableWidth.toFloat(), drawableHeight.toFloat())
        val tempDst = RectF(0f, 0f, viewWidth, viewHeight)
        originMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER)
        applyToImageMatrix()
    }

    /**
     * 处理双击事件,双击执行缩放动画
     */

    private fun dealOnDoubleTap(e: MotionEvent) {
        animZoom(e.x, e.y)
    }

    private fun animZoom(pivotX: Float, pivotY: Float) {
        val currZoom = suppMatrix.scaleX()
        val endZoom = if (currZoom < midZoom) {
            midZoom
        } else if (currZoom < maxZoom) {
            maxZoom
        } else {
            minZoom
        }
        playZoomAnim(currZoom, endZoom, pivotX, pivotY)
    }

    private fun dealUpOrCancel(event: MotionEvent) {
        val currZoom = suppMatrix.scaleX()
        if (currZoom < minZoom) {
            val rect = getDrawMatrixRect(imageMatrix)
            rect?.let { playZoomAnim(currZoom, minZoom, it.centerX(), it.centerY()) }
        }
    }

    private fun playZoomAnim(startZoom: Float, endZoom: Float, pivotX: Float, pivotY: Float) {
        zoomAnim.removeAllUpdateListeners()
        zoomAnim.cancel()

        // 点击的点设置为缩放的支点
        pivotPointF.set(pivotX, pivotY)
        val startMatrix = Matrix(imageMatrix)
        val endMatrix = Matrix(originMatrix).apply {
            val tempSuppMatrix = Matrix(suppMatrix)
            tempSuppMatrix.zoomTo(endZoom, pivotPointF.x, pivotPointF.y)
            this.postConcat(tempSuppMatrix)
        }
        // 边界矫正
        correctByViewBound(endMatrix).let {
            endMatrix.translateBy(it.x, it.y)
        }

        val tmpPointArr = floatArrayOf(pivotX, pivotY)
        MathUtils.computeNewPosition(tmpPointArr, imageMatrix, endMatrix)
        val endPivotPointF = PointF(tmpPointArr[0], tmpPointArr[1])
        val currMatrix = Matrix()
        val animatorUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
            override fun onAnimationUpdate(animation: ValueAnimator) {
                val tempValue = animation.animatedValue as Float
                val factor = (tempValue - startZoom) / (endZoom - startZoom)
                currMatrix.set(
                    MathUtils.interpolate(
                        startMatrix,
                        pivotPointF.x,
                        pivotPointF.y,
                        endMatrix,
                        endPivotPointF.x,
                        endPivotPointF.y,
                        factor
                    )
                )
                // suppMatrix * originMatrix = currMatrix;  suppMatrix = currMatrix *(originMatrix 的逆矩阵)
                tempMatrix.reset()
                originMatrix.invert(tempMatrix)
                tempMatrix.postConcat(currMatrix)
                suppMatrix.set(tempMatrix)
                applyToImageMatrix(true)
            }
        }
        zoomAnim.setFloatValues(startZoom, endZoom)
        zoomAnim.addUpdateListener(animatorUpdateListener)
        zoomAnim.start()
    }

    private fun dealOnFling(e2: MotionEvent, velocityX: Float, velocityY: Float) {
        val rect = getDrawMatrixRect(imageMatrix) ?: return
        val startX = Math.round(-rect.left)
        val minX: Int
        val maxX: Int
        val minY: Int
        val maxY: Int
        if (width < rect.width()) {
            minX = 0
            maxX = Math.round(rect.width() - width)
        } else {
            maxX = startX
            minX = maxX
        }
        val startY = Math.round(-rect.top)
        if (height < rect.height()) {
            minY = 0
            maxY = Math.round(rect.height() - height)
        } else {
            maxY = startY
            minY = maxY
        }
        currentX = startX
        currentY = startY
        if (!((startX != maxX) || (startY != maxY))) {
            return
        }
        overScroller.fling(startX, startY, -velocityX.toInt(), -velocityY.toInt(), minX, maxX, minY, maxY, 00)
        startFlingAnim()
    }

    private fun startFlingAnim() {
        startTime = AnimationUtils.currentAnimationTimeMillis()
        postNextFrame()
    }

    private fun postNextFrame() {
        if (overScroller.computeScrollOffset()) {
            val currX = overScroller.currX
            val currY = overScroller.currY
            suppMatrix.translateBy((currentX - currX).toFloat(), (currentY - currY).toFloat())
            applyToImageMatrix()
            currentX = currX
            currentY = currY
            choreographer.postFrameCallback {
                postNextFrame()
            }
        }
    }

    /**
     * 处理拖动(平移)事件
     */

    private fun dealOnScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float) {
        if (allowParentInterceptOnEdge && !scaleGestureDetector.isInProgress) {
            if (pointerCount < 2) {
                if (scrollEdge == Edge.EDGE_BOTH || (scrollEdge == Edge.EDGE_LEFT && -distanceX >= 1f) || (scrollEdge == Edge.EDGE_RIGHT && -distanceX <= -1f)) {
                    parent?.requestDisallowInterceptTouchEvent(false)
                }
            }
        }
        suppMatrix.translateBy(-distanceX, -distanceY)
        applyToImageMatrix()
    }

    /**
     * 处理双指缩放
     */

    private fun dealOnScale(detector: ScaleGestureDetector) {
        val currScale: Float = suppMatrix.scaleX()
        var scaleFactor = detector.scaleFactor
        if ((currScale >= maxZoom && scaleFactor > 1f) || (currScale <= minZoom * overMinZoomRadio && scaleFactor < 1f)) {
            return
        }
        suppMatrix.zoomBy(scaleFactor, detector.focusX, detector.focusY)
        applyToImageMatrix()
    }

    /**
     * 对输入矩阵,依据 View 宽高进行调整,输出需要调整的平移量
     */

    private fun correctByViewBound(srcMatrix: Matrix): PointF {
        val tempPointF = PointF()
        // 得到 matrix 的 rect
        val tempRectF = getDrawMatrixRect(srcMatrix)
        tempRectF ?: return tempPointF
        var deltaX = 0f
        var deltaY = 0f
        if (tempRectF.height() < height) {
            deltaY = ((height - tempRectF.height()) / 2) - tempRectF.top
        } else if (tempRectF.top > 0) {
            deltaY = -tempRectF.top
        } else if (tempRectF.bottom < height) {
            deltaY = height - tempRectF.bottom
        }
        if (tempRectF.width() <= width) {
            deltaX = ((width - tempRectF.width()) / 2) - tempRectF.left
            scrollEdge = Edge.EDGE_BOTH
        } else if (tempRectF.left > 0) {
            deltaX = -tempRectF.left
            scrollEdge = Edge.EDGE_LEFT
        } else if (tempRectF.right < width) {
            deltaX = width - tempRectF.right
            scrollEdge = Edge.EDGE_RIGHT
        } else {
            scrollEdge = Edge.EDGE_NONE
        }
        tempPointF.set(deltaX, deltaY)
        return tempPointF
    }

    private fun getDrawMatrixRect(matrix: Matrix): RectF? {
        val d = drawable
        if (null != d) {
            val tempRect = RectF()
            // 新奇的写法
            tempRect[0f, 0f, d.intrinsicWidth.toFloat()] = d.intrinsicHeight.toFloat()
            matrix.mapRect(tempRect)
            return tempRect
        }
        return null
    }


    /**
     * 在显示之前,进行边界矫正,对 suppMatrix 进行调整
     */

    private fun correctSuppMatrix() {
        // 目标 matrix
        tempMatrix.set(originMatrix)
        tempMatrix.postConcat(suppMatrix)
        correctByViewBound(tempMatrix).let {
            suppMatrix.translateBy(it.x, it.y)
        }
    }

    /**
     * 应用于 ImageView 的 matrix,为了思路清晰,这里先频繁创建对象 drawMatrix
     */

    private fun applyToImageMatrix(skipCorrect: Boolean = false) {
        if (!skipCorrect) {
            // 在应用之前,进行边界矫正
            correctSuppMatrix()
        }
        tempMatrix.set(originMatrix)
        // 即当前 Matrix 会乘以传入的 Matrix。 suppMatrix*originMatrix
        tempMatrix.postConcat(suppMatrix)
        imageMatrix = tempMatrix
    }

    inner class MultiGestureDetector : GestureDetector.SimpleOnGestureListener(),
        ScaleGestureDetector.OnScaleGestureListener {

        override fun onSingleTapConfirmed(e: MotionEvent)Boolean {
            viewTapListener?.onViewTap(this@ZoomImageView, e.x, e.y)
            return super.onSingleTapConfirmed(e)
        }

        override fun onLongPress(e: MotionEvent) {
            super.onLongPress(e)
            viewTapListener?.onLongClick(this@ZoomImageView)
        }

        override fun onDoubleTap(e: MotionEvent)Boolean {
            dealOnDoubleTap(e)
            return super.onDoubleTap(e)
        }

        override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float)Boolean {
            e1?.let {
                dealOnScroll(e1, e2, distanceX, distanceY)
            }
            return super.onScroll(e1, e2, distanceX, distanceY)
        }

        override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float)Boolean {
            dealOnFling(e2, velocityX, velocityY)
            return super.onFling(e1, e2, velocityX, velocityY)
        }

        override fun onScale(detector: ScaleGestureDetector)Boolean {
            dealOnScale(detector)
            return true
        }

        override fun onScaleBegin(detector: ScaleGestureDetector)Boolean {
            return true
        }

        override fun onScaleEnd(detector: ScaleGestureDetector) {
        }
    }

    interface OnViewTapListener {
        fun onViewTap(view: View?, x: Float, y: Float)
        fun onLongClick(view: View?)
    }
}


替换掉 MeetPhoto 中原来的控件,看下最终效果,撒花。


总结
我们像上面那样一步一步地实现了一个自己的 ZoomImageView 控件,包含原始控件的功能,并解决掉最开始提到的两个体验问题,但依然存在许多问题(如何解决就留给 ZoomImageView 下篇再来讲述吧)。我把这些问题总结如下:
  • 性能方面,空间、时间复杂度没有考虑(例如第 6 步中重复计算的问题),对大图可能导致内存溢出也未做处理。
  • 手势交互是否足够灵敏?可以调整触摸滑动的最小距离(通过 getTouchSlop() 获取)来改善用户体验。动画过程中拖动等并行操作未做处理。
  • 不支持 over scale (过度放大)、over scroll,因为整体思路和边界矫正并不合理,导致很难扩展 over scroll 功能。
  • 不支持关闭,不支持 scaleType 等等(不需要的时候就先不要吧)。
  • 双指捏合同时移动出现抖动的问题。
    对比下面四个图片预览的功能,第一个第二个都能明显感到抖动,iPhone 体验最好,OPPO 上体验还是不错的,Pixel4 真的没想到这么拉,这个功能体验如果排序的话:iPhone > OPPO > Pixel4 > Demo。

    • My Demo

      Pixel4 相册

      iPhone 照片

      OPPO 相册

      达到 iPhone 照片丝般顺滑的体验很有挑战,但我们至少可以实现媲美 OPPO 相册上的体验。然而,这篇博客介绍的方法已不再适用。

      3思考

分解


当我们遇到一个大问题的挑战时,可能当下并不具备解决它的能力,不妨试试分解它。我们把一个大问题拆分成若干个小问题,逐个击破,最终组合解决方案。就像通关游戏一样,我们需要先通过一个一个小的关卡,才能挑战最终的 boss。这种方式往往行之有效,并且在分解之后我们掌握了一个一个小的技能。
如果我们想了解发动机的工作原理,我们需要把发动机拆开,拆开需要那些工具?我们需要扳手、螺丝刀、需要起重设备和滑轮,然后我们把它拆开,不仅了解发动机的工作原理,在这个过程中,还会学会扳手、螺丝刀等工具的使用。

这篇博客也一样,为了解决 ZoomImageView 的问题,我们要弄清楚 Scroller 滑动特性、事件分发和 GestureDetector 、对 Matrix 运算也了解一二,学会了 geogebra 工具的使用。我们不只是得到了一个 ZoomImageView 控件,更有意义的是积木变成了粘土。现在这些粘土可以按照我们的意愿重新组合成新的积木了,我们可以拿来一个手势放大的视频控件,或是在鸿蒙、Flutter、QT 等上面实现一个 ”ZoomImageView”,在遇到头像裁剪、图片编辑等技术问题时,这些粘土一样可以派上用场。

https://www.geogebra.org/graphing


这些便是对分解的另一种解释。
技术的时效性
多年之前使用 PhotoView 时就曾想对其进行重构,这个念头终于有一天奇怪地又带点遗憾地实现了,不免让人唏嘘。某项技术随着时间的推移变得不再引入注目,放到时代背景下,连同它所依附的行业都变得无足轻重,开发者所具备的经验和技能其实作为知识资产,和金融资产何其相似。也许我们需要像金融投资者那样管理自己的技能组合。除了年终时盘算自己一年下来可怜的收入,是否也可以盘算一下,哪些技能和经验,未来潜在的收益率更大。
彩蛋
2007年乔布斯在初代 iPhone 发布会上演示图片手势放大的片段:

参考文档

GestureViews (待挖掘的宝库)

https://github.com/alexvasilkov/GestureViews

俗说矩阵(bilibili 视频)

https://www.bilibili.com/video/BV1Vs4y1Z7HB/?p=1

ZoomImageView(示例代码)

https://github.com/idea007/ZoomImageView


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

OpenHarmony源码系列: 鸿蒙页面背后的机制,打通 JS View 与C++世界
优雅实现网络请求:协程+Flow+Retrofit+OkHttp
Android 7 种方式实现自定义ViewGroup的滚动与惯性滚动



扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存