ScalableImageView简单实现

593 阅读2分钟

ScalableImageView 是一个可放大 可滑动的一个View,简单实现以下,主要涉及到自定义View,以及 GestureDetector 手势监听,还有属性动画的使用

class ScalableImageView(context: Context, attrs: AttributeSet?) : View(context, attrs),
    GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, Runnable {
    private var paint = Paint()

    /**
     * 是否是放大状态
     */
    private var isBig = false

    /**
     *  放大系数,当最大化的时候,可以让所有的都超出边界,当放大的时候可以上下左右都能滑动
     */

    private val scaleFactory = 1.5f
    private var scale = 0f
        set(value) {
            field = value
            invalidate()
        }

    /**
     * 一开始的时候直接在View的正中间为中心
     */
    private val centerX: Float by lazy { (width - avatar.width) / 2f }
    private val centerY: Float by lazy { (height - avatar.height) / 2f }

    /**
     * x轴的偏移
     */
    private var offsetX = 0f

    /**
     * y轴的偏移
     */
    private var offsetY = 0f

    /**
     * 初始的缩放大小
     */
    private var smallScale = 1f

    /**
     * 双击放大时的最大值
     */
    private val bigScale: Float by lazy {
        Math.max(height / avatar.height.toFloat(), width / avatar.width.toFloat()) * scaleFactory
    }

    /**
     * 双击的时候的动画 放大缩小
     */
    private val scaleAnimator: ObjectAnimator by lazy {
        ObjectAnimator.ofFloat(this, "scale", 0f, 1f)
    }

    /**
     * 计算滑动
     */
    private val scroller: OverScroller = OverScroller(context)


    /**
     * 收拾监听
     */
    private val gestureDetector: GestureDetectorCompat =
        GestureDetectorCompat(
            context,
            this
        ).apply {
            // 这行代码可以不用写
            // 在他的内部  ,所以直接 实现 GestureDetector.OnDoubleTapListener 即可
            //       if (listener instanceof OnDoubleTapListener) {
            //            setOnDoubleTapListener((OnDoubleTapListener) listener);
            //        }
            setOnDoubleTapListener(this@ScalableImageView)
        }
    private val avatar: Bitmap by lazy { getAvatar(this) }


    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 移动canvas
        canvas.translate(offsetX, offsetY)


        var currentScale = 1 + (bigScale - smallScale) * scale

        canvas.scale(currentScale, currentScale, width / 2f, height / 2f)

        // 绘制的时候使用offsetX 和 offsetY
        canvas.drawBitmap(avatar, centerX, centerY, paint)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return gestureDetector.onTouchEvent(event)
    }

    override fun onDown(e: MotionEvent?): Boolean {
        return true
    }

    /**
     * 是否为按下的状态
     */
    override fun onShowPress(e: MotionEvent?) {
    }

    /**
     * 当点击之后抬起的时候,可以认为是个click点击监听,
     * @return 是否消费了点击,对于开发者来说 无影响,true 和  false 都可以,主要是系统做记录用的
     */
    override fun onSingleTapUp(e: MotionEvent?): Boolean {
        return true
    }

    /**
     * 用户按下触摸屏 & 拖动
     *
     * @param distanceX 旧位置-新位置
     * @param distanceY 旧位置-新位置
     * @return 也没啥用
     */
    override fun onScroll(
        e1: MotionEvent?,
        current: MotionEvent?,
        distanceX: Float,
        distanceY: Float
    ): Boolean {
        // 放大状态才能 滑动。根据手指的移动算出来 offsetX ,offsetY
        // 算出边界值

        if (isBig) {

            offsetX -= distanceX
            offsetY -= distanceY
            fixXY()
            invalidate()
        }

        return true
    }

    private fun fixXY() {
        val borderX = (avatar.width * bigScale - width) / 2f
        val borderY = (avatar.height * bigScale - height) / 2f
        if (offsetX > borderX) {
            offsetX = borderX
        }
        if (offsetY > borderY) {
            offsetY = borderY
        }

        if (offsetX < -borderX) {
            offsetX = -borderX
        }

        if (offsetY < -borderY) {
            offsetY = -borderY
        }
    }


    /**
     * 长按
     */
    override fun onLongPress(e: MotionEvent?) {
    }

    /**
     *惯性滑动
     * velocityX 速度
     */
    override fun onFling(
        e1: MotionEvent?,
        e2: MotionEvent?,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        val borderX = (avatar.width * bigScale - width) / 2
        val borderY = (avatar.height * bigScale - height) / 2
        if (isBig) {
            // 前两个是 选的坐标系
            scroller.fling(
                offsetX.toInt(),
                offsetY.toInt(),
                velocityX.toInt(),
                velocityY.toInt(),
                -borderX.toInt(),
                borderX.toInt(),
                -borderY.toInt(),
                borderY.toInt(),
                50f.px.toInt(),
                50f.px.toInt()
            )

            /**
             * 一帧调用一次
             */
            postOnAnimation(this)

        }

        return false
    }



    /**
     * 如果 GestureDetectorCompat 设置了双击的监听,就不要使用 onSingleTapUp 这个方法做click处理了,
     * 用  onSingleTapConfirmed 这个方法处理
     */
    override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
        return true
    }

    /**
     * 双击
     */
    override fun onDoubleTap(e: MotionEvent): Boolean {
        isBig = !isBig
        if (isBig) {
            offsetX = (e.x - width / 2f) - (e.x - width / 2) * bigScale
            offsetY = (e.y - height / 2f) - (e.y - height / 2) * bigScale
            fixXY()
            scaleAnimator.start()
        } else {
            scaleAnimator.reverse()
        }
        return true
    }

    override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
        return true
    }

    override fun run() {
        // 计算 看看滑动结束了没
        var isCompelte =  scroller.computeScrollOffset()



        if(isCompelte){
            val currX = scroller.currX
            val currY = scroller.currY

            offsetX = currX.toFloat()
            offsetY = currY.toFloat()
            // 一帧执行一次
            postOnAnimation(this)
        }
    }

}





utils

val Float.px:Float
    get() = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        this,
        Resources.getSystem().displayMetrics
    )


fun getAvatar(view: View):Bitmap{

   var bitmap =  BitmapFactory.decodeResource(view.resources,R.drawable.avatar)
//    BitmapFactory.decodeFile("${context.cacheDir.absolutePath}/dog.jpg", opt)
    //拿到图片宽高
    val imageWidth = bitmap.width
    val imageHeight = bitmap.height

    //拿到View的宽高
    val viewWidth = view.width
    val viewHeigh = view.height

    //计算缩放比例
    var scale = 1f
    val scaleWidth = imageWidth / viewWidth.toFloat()
    val scaleHeight = imageHeight / viewHeigh.toFloat()
    //选择较大的那个比例
    if (scaleWidth >= scaleHeight && scaleWidth >= 1) {
        scale = scaleWidth
    } else if (scaleWidth < scaleHeight && scaleHeight >= 1) {
        scale = scaleHeight
    }


    return bitmap.scale((imageWidth/scale).toInt(),(imageHeight/scale).toInt())
}