自定义PhotoView实现手机相册图片展示效果(双击放大缩小,左右滑动切换,上下滑动关闭,双指缩放)

519 阅读3分钟

介绍

最近在学习事件分发的知识点,刚好做这个Demo也是为了巩固知识点 参照了手机系统相册的浏览交互交过,主要包含几个方面

  1. 双击放大缩小图片
  2. 图片放大状态时左右滑动图片
  3. 左右滑动切换图片
  4. 下滑缩小关闭图片,改变背景透明度

效果展示

话不多说,先来看实现效果

实现过程

整体实现思路

使用到了ViewPager2,在viewpagerAdapter中图片的装载,后面主要需要去处理viewpager和图片的左右滑动冲突

自定义View中使用了SDK提供的GestureDetector类接管onTouchEvnet,主要是自己写起来不太方便,双击事件还得判断两次ACTION_UP的时间间隔,抛掷事件还得判断滑动的速度,使用ScaleGestureDetector类判断双指缩放事件

图片是通过canvas.drawBitmap()方法进行绘制到屏幕上,通过canvas.scale()控制缩放

  • 重写onTouchEvent onTouchEvent里面进行事件的接管处理,在ACTION_UP的时候会判断当前Y的偏移距离
    /** 接管Touch事件*/
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        var consume: Boolean
        // 有新的事件来了,惯性滑动还没停止,强制停止
        if (!overScroller.isFinished) {
            overScroller.forceFinished(true)
        }

        if (event.action == MotionEvent.ACTION_UP) {
            logD("平移距离",translationY.toString())
            if (translationY / height > SCROLL_INTERVAL) {
            // 超过了滑动阈值,退出View
                onImageExit.invoke()
            }
            if (translationY != 0f) {
                // 重置
                currentScale = smallScale
                translationY = 0f
                translationX = 0f
            }
        }

        // 双指操作优先
        consume = scaleGestureDetector.onTouchEvent(event)
        if (!scaleGestureDetector.isInProgress) {
            consume = gestureDetector.onTouchEvent(event)
        }
        return consume
    }
  • 重写dispatchTouchEvent,用了内部拦截的方式处理事件冲突
    /** 解决与viewPager的事件冲突 */
    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        val x = event.rawX                  // 屏幕坐标系
        val y = event.rawY

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                // 定义滑动规则
                val deltaX = x - lastX
                val deltaY = y - lastY
                if (translationY != 0f) {
                    // picture正在平移中,不允许拦截事件
                    parent.requestDisallowInterceptTouchEvent(true)
                    return super.dispatchTouchEvent(event)
                }

                if (!isEnlarge) {
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    if (abs(deltaX) > abs(deltaY)) {
                        // 左右滑动
                        if (deltaX < 0) {
                            // 左
                            Log.d("事件冲突", "左滑了,x=${x},y=${y},lastX=${lastX},lastY=${lastY}")
                            if (offsetX == -(bitmap.width * bigScale - width) / 2) {
                                parent.requestDisallowInterceptTouchEvent(false)
                                Log.d("事件冲突", "左滑让事件给父亲")
                            }
                        } else {
                            // 右
                            Log.d("事件冲突", "右滑了,x=${x},y=${y},lastX=${lastX},lastY=${lastY}")
                            if (offsetX == (bitmap.width * bigScale - width) / 2) {
                                parent.requestDisallowInterceptTouchEvent(false)
                                Log.d("事件冲突", "右滑让事件给父亲")
                            }
                        }
                    }
                }
            }
        }
        lastX = x
        lastY = y
        return super.dispatchTouchEvent(event)
    }
  • 双击放大缩小图片 这个需求的关键点在于,如果获取到放大和缩小的倍数,只需要获取到了,每次在双击事件触发的时候进行切换就好了
        if (bitmap.width.toFloat() / bitmap.height > width.toFloat() / height) {
            // 比较宽高比,判断目标bitmap是横向还是纵向,如果比屏幕宽高比大,说明相比于屏幕来说,这张图片是横向的
            smallScale = width.toFloat() / bitmap.width
            bigScale = height.toFloat() / bitmap.height
        } else {
            smallScale = height.toFloat() / bitmap.height
            bigScale = width.toFloat() / bitmap.width
        }

		// 在双击的回调里进行切换
        override fun onDoubleTap(e: MotionEvent): Boolean {
            isEnlarge = !isEnlarge
            if (isEnlarge) {
                startScaleAnimator(smallScale, bigScale)
            } else {
                startScaleAnimator(currentScale, smallScale)
            }
            return true
        }

	// 使用到了属性动画,让缩放平滑些
    private fun startScaleAnimator(fromScale: Float, toScale: Float) =
        ObjectAnimator.ofFloat(this, "currentScale", fromScale, toScale).start()
  • 图片放大状态时左右滑动图片,在没放大的情况下下滑缩小关闭图片,改变背景透明度
        /** 滑动 */
        override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
            if (isEnlarge) {
                offsetX -= distanceX
                offsetY -= distanceY
                fixOffsets()
                invalidate()
            } else {
                // 在没放大的情况下
                val scrollDeltaY = e2.rawY - lastScrollY
                val scrollDeltaX = e2.rawX - lastScrollX
                val translationY = this@PhotoView.translationY + scrollDeltaY
                val translationX = this@PhotoView.translationX + scrollDeltaX
                if (lastScrollY != 0f) {
                    currentScale *= (1 - (scrollDeltaY / height))
                    Log.d("滑动事件", "height:${height},translation:${translationY},height/translation:${translationY / height}")
                    Log.d("滑动事件", "$currentScale")
                    onAlphaChange(1 - (this@PhotoView.translationY / height))		// 回调修改透明度
                    this@PhotoView.translationY = translationY
                    this@PhotoView.translationX = translationX
                }
                lastScrollY = e2.rawY
                lastScrollX = e2.rawX
            }

            return super.onScroll(e1, e2, distanceX, distanceY)
        }

    /** 处理边界 */
    private fun fixOffsets() {
        offsetX = min(offsetX, (bitmap.width * bigScale - width) / 2)               // 相当于取绝对值,获得偏移的区间值
        offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
        offsetY = min(offsetY, (bitmap.height * bigScale - height) / 2)
        offsetY = max(offsetY, -(bitmap.height * bigScale - height) / 2)
    }
  • 双指缩放,实现ScaleGestureDetector.OnScaleGestureListener接口
        private var initialScale: Float = 0f

        override fun onScale(detector: ScaleGestureDetector?): Boolean {
            if (currentScale > smallScale && !isEnlarge || currentScale == smallScale && isEnlarge) {
                isEnlarge = !isEnlarge
            }
            // 缩放因子
            currentScale = initialScale * detector!!.scaleFactor
            if (currentScale > bigScale) currentScale = bigScale
            if (currentScale < smallScale) currentScale = smallScale
            invalidate()
            return false
        }

        override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
            initialScale = currentScale
            return true
        }
  • 其他关键代码
        /** 这里必须要返回true,表示接收此事件序列,不然无法收到后面的ACTION_UP和ACTION_DOWN事件 */
        override fun onDown(e: MotionEvent?): Boolean {
            lastScrollX = 0f
            lastScrollY = 0f
            return true
        }
        
        override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
            performClick()
            return super.onSingleTapConfirmed(e)
        }

源码地址

github地址,在项目中的PhotoView类