基于Fresco的图片浏览器

作者: 选一个昵称这么难 | 来源:发表于2019-05-10 13:43 被阅读5次

    1.实现功能

    使用Fresco实现类似于微信图片查看器的功能:

    • 手势拖动关闭
    • 手势缩放
    • 双击缩放
    • 单击,双击等各种回调

    现在网络上有许多类似微信的图片查看器,多数是使用ImageView来做的,如果项目中使用的是Fresco来加载图片,则不能适用,因此撸了这个。

    效果:


    效果图

    github地址

    2.思路

    (1)手势拖动

    动态的设置图片的宽高以及scrollX和scrollY

    (2)双指缩放

    动态的设置图片的宽高

    3.难点

    (1)图片放大后,实际的边界判断,这涉及到图片滑动到边缘后的事件处理
    (2)放大状态下,左右滑动到图片边界,这个时候要把触摸事件交给viewpager,如果不处理会有图片跳动的问题。
    (3)惯性滑动处理

    4.可定制点

    (1)现在gif是在显示的时候才执行动画,且切换gif的时候停止动画,这点可根据需求来做
    (2)可以先加载缩略图再加载原图,目前是只加载原图
    (3)设置图片url传的模型可以根据需求来写,现在传的是List<String>
    (4)入场和退场的缩放动画依赖rect,如果没有rect默认是透明度变化动画,这里可以根据实际需求来写

    5.具体实现

    (1)手势拖动-非放大状态

    一开始当手指下滑才会触发滑动手势,根据下滑的距离来计算图片缩放倍率,这个倍率可调整,然后设置图片宽高和scroll。
    已删除暂时不用看的代码,主要看move这个事件处理。

     /**
         * 初始滑动,双指缩放手势
         */
        private fun handleDragEvent(event: MotionEvent?): Boolean {
            if (event == null) {
                return false
            }
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    getViewPager()?.requestDisallowInterceptTouchEvent(true)
                    getViewPager().onInterceptTouchEventFlag = false
    
                    // 初始化或者设置一些参数 用于手势滑动
                    startX = event.rawX
                    startY = event.rawY
                    imgScaleDown = imgCurrentScale
                    imgCurrentScaleTemp = imgCurrentScale
                    gifResetParentLastMotion = true
                    draging = false
                    lastDisX = 0F
                    lastDisY = 0F
                    actionDownScrollX = this.scrollX
                    actionDownScrollY = this.scrollY
                    scaleStateMovedX = this.scrollX
                    scaleStateMovedY = this.scrollY
                    return true
                }
                
                MotionEvent.ACTION_MOVE -> {
                    velocityTracker?.addMovement(event)
    
    
                    if (startX <= 0.1F) {
                        startX = event.rawX
                        startY = event.rawY
                    }
                    disX = event.rawX - startX
                    disY = event.rawY - startY
                    //一根手指滑动 包括下拉关闭 和 图片放大状态的拖动
                    if (event.pointerCount == 1) {
                        // 上下滑动的手势
                        if ((disY > 0 && Math.abs(disY) > Math.abs(disX)) || draging || imgCurrentScale > 1) {
                     
                            draging = true
                            // 缩放 start
                            var scale = 1F
                            scale = (1 - Math.abs(disY) / (screenHeight * 0.6F)) * imgCurrentScaleTemp / imgCurrentScale
                            if (((1 - Math.abs(disY) / (screenHeight * 0.6F)) * imgCurrentScaleTemp) < 0.3) {
                                scale = 0.3F / imgCurrentScale
                            }
                            imgCurrentScale = imgCurrentScale * scale
                            lastDisY = disY
    
                            val imgChangeScale = imgCurrentScale * 1F
    //
                            setImageWidthHeightByScale(imgChangeScale)
                            // 缩放 end
    
                            // 平移start
                            extraX = (1F - imgCurrentScale / imgCurrentScaleTemp) * (startX - screenWidth / 2)
                            extraY = (1F - imgCurrentScale / imgCurrentScaleTemp) * (startY - screenHeight / 2)
                            this.scrollTo(
                                (actionDownScrollX * (imgCurrentScale / imgCurrentScaleTemp) - disX - extraX).toInt(),
                                (actionDownScrollY * (imgCurrentScale / imgCurrentScaleTemp) - disY - extraY).toInt()
                            )
                            // 平移end
                            // 设置viewpager背景透明度
                            currentAlpha = 1F - disY * 1.5F / screenHeight
                            if (currentAlpha > 1) {
                                currentAlpha = 1F
                            }
                            setBgAlpha(currentAlpha)
                            return true
                        } else {
                            // 交给viewpager处理
                            viewpagerHandleDrag()
                            return false
                        }
                    } else {
                        viewpagerHandleDrag()
                        return false
                    }
                }
    

    里面用到的方法:

    /**
         * 根据缩放倍率设置图片宽高
         */
        private fun setImageWidthHeightByScale(scale: Float) {
            val lp = this.layoutParams
            lp.width = (displayWidth * scale).toInt()
            lp.height = (displayHeight * scale).toInt()
            this.layoutParams = lp
        }
    

    拖动之后,然后松手,有两种情况(根据滑动距离判断):

    • 回到远处
    • 关闭

    关闭有两种动画,缩放和渐隐。如果知道回坑的位置,就执行缩放动画回坑,否则就是渐隐动画。下面看看松开手之后的处理:

    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                    if (draging) {
                        if (disY > CLOSE_DISTANCE) {
                            imgAnimClose()
                        } else {
                            imgAnimToBack()
                        }
                        return true
                    }
                    draging = false
                }
    

    先看回到远处,用属性动画实现,该view的动画都是用属性动画实现的

         /**
         * 如果拖拽距离小于设定值,则返回原处
         * 就是一个属性动画
         */
        private fun imgAnimToBack() {
            var currentValue = 0F
            var animPercent = 0F //动画执行百分比
            val scrollYTemp = this.scrollY// 刚开始执行动画时候的偏移量
            val scrollXTemp = this.scrollX
            val animator = ValueAnimator.ofFloat(imgCurrentScale, imgScaleDown)
            animator.duration = 300
            animator.interpolator = AccelerateDecelerateInterpolator()
            animator.addUpdateListener {
                currentValue = it.animatedValue as Float
                setImageWidthHeightByScale(currentValue)
                animPercent = (currentValue - imgCurrentScale) / (imgScaleDown - imgCurrentScale)
                val scrollToX = scrollXTemp + (scaleStateMovedX - scrollXTemp) * animPercent
                val scrollToY = scrollYTemp + (scaleStateMovedY - scrollYTemp) * animPercent
                this.scrollTo(scrollToX.toInt(), scrollToY.toInt())
                setBgAlpha(currentAlpha + (1F - currentAlpha) * animPercent)
            }
            animator.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
    
                }
    
                override fun onAnimationEnd(animation: Animator?) {
                    imgCurrentScale = imgScaleDown
                }
    
                override fun onAnimationCancel(animation: Animator?) {
                    imgCurrentScale = imgScaleDown
                }
    
                override fun onAnimationStart(animation: Animator?) {
                }
    
            })
            animator.start()
        }
    

    然后看看关闭动画:

     /**
         * 关闭动画
         * 外部可直接调用该方法关闭view
         */
        fun imgAnimClose() {
            if (currentRect != null) {
                imgZoomCloseAnim(currentRect!!)
            } else {
                imgFadeCloseAnim()
            }
        }
    
        /**
         * 缩放退场
         */
        private fun imgZoomCloseAnim(tarRect: Rect) {
            var currentValue = 0F
            var animPercent = 0F
            val gifCurrentScaleTemp = imgCurrentScale
            val targetScale = tarRect.width().toFloat() / screenWidth
            val scrollXTemp = this.scrollX
            val scrollYTemp = this.scrollY
    
            val targetTransY = (imageCenterY - displayHeight * targetScale / 2) - tarRect.top
            val targetScrollX = screenWidth * (1F - targetScale) / 2 - tarRect.left
    
            val animator = ValueAnimator.ofFloat(imgCurrentScale, targetScale)
            animator.addUpdateListener {
                currentValue = it.animatedValue as Float
                animPercent = (gifCurrentScaleTemp - currentValue) / (gifCurrentScaleTemp - targetScale)
                setImageWidthHeightByScale(currentValue)
    
                val x = scrollXTemp + (targetScrollX - scrollXTemp) * animPercent
                val y = scrollYTemp + (targetTransY - scrollYTemp) * animPercent
                this.scrollTo(x.toInt(), y.toInt())
    
                setBgAlpha(currentAlpha * (1F - animPercent))
            }
            animator.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
    
                }
    
                override fun onAnimationEnd(animation: Animator?) {
                    listener?.onDragEnd()
                }
    
                override fun onAnimationCancel(animation: Animator?) {
                    listener?.onDragEnd()
                }
    
                override fun onAnimationStart(animation: Animator?) {
                }
    
            })
            animator.duration = 260
            animator.interpolator = AccelerateDecelerateInterpolator()
            animator.start()
        }
    
        /**
         * 透明度渐变退场
         */
        private fun imgFadeCloseAnim() {
            val animator = ValueAnimator.ofFloat(0F, 1F)
            animator.addUpdateListener {
                val currentValue = it.animatedValue as Float
                setBgAlpha(currentAlpha * (1F - currentValue))
                getViewPager().alpha = 1F - currentValue
            }
            animator.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
    
                }
    
                override fun onAnimationEnd(animation: Animator?) {
                    listener?.onDragEnd()
                }
    
                override fun onAnimationCancel(animation: Animator?) {
                    listener?.onDragEnd()
                }
    
                override fun onAnimationStart(animation: Animator?) {
                }
    
            })
            animator.duration = 260
            animator.interpolator = LinearInterpolator()
            animator.start()
        }
    

    (2)双指缩放

    就是根据两指间的距离,动态设置图片宽高。
    双指缩放在move事件里面实现

    MotionEvent.ACTION_MOVE -> {
                   if (doubleFingerTouch) {
                        // 处理双指缩放
                        var scale = 1F + (getDistance(event) - disBetweenFingersPre) / 600
                        if (disBetweenFingersPre <= 0) {
                            scale = 1F
                        }
                        if (imgCurrentScale * scale < GIF_MIN_SCALE) {
                            scale = 1F
                        } else if (imgCurrentScale * scale > GIF_MAX_SCALE) {
                            scale = 1F
                        }
                        imgCurrentScale = imgCurrentScale * scale
                        imgCurrentScaleTemp = imgCurrentScale
                        setImageWidthHeightByScale(imgCurrentScale)
                        disBetweenFingersPre = getDistance(event)
                        return true
                    } 
                }
    

    获取两指距离的方法:

    /*获取两指之间的距离*/
        private fun getDistance(event: MotionEvent): Float {
            val x = event.getX(1) - event.getX(0);
            val y = event.getY(1) - event.getY(0);
            val distance = Math.sqrt((x * x + y * y).toDouble());//两点间的距离
            return distance.toFloat();
        }
    

    (3)双击缩放

    双击缩放处理放在Gesture

    override fun onDoubleTap(e: MotionEvent?): Boolean {
            if (imgCurrentScale > 1F) {
                doubleClapScale(1F)
            } else if (imgCurrentScale == 1F) {
                doubleClapScale(GIF_MAX_SCALE)
            }
            return true
        }
    

    手势放大或缩小也是动态设置图片宽高。

    /**
         * 双击缩小或放大
         */
        private fun doubleClapScale(targetScale: Float) {
            var currentValue = 0F
            var animPercent = 0F
            val scrollYTemp = this.scrollY
            val scrollXTemp = this.scrollX
            val animator = ValueAnimator.ofFloat(imgCurrentScale, targetScale)
            animator.duration = 200
            animator.interpolator = AccelerateDecelerateInterpolator()
            animator.addUpdateListener {
                currentValue = it.animatedValue as Float
                setImageWidthHeightByScale(currentValue)
                animPercent = (currentValue - imgCurrentScale) / (targetScale - imgCurrentScale)
                val x = scrollXTemp * (1 - animPercent)
                val y = scrollYTemp * (1 - animPercent)
                this.scrollTo(x.toInt(), y.toInt())
            }
            animator.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
    
                }
    
                override fun onAnimationEnd(animation: Animator?) {
                    if (targetScale == 1F) {
                        resetState()
                    }
                    imgCurrentScale = targetScale
                }
    
                override fun onAnimationCancel(animation: Animator?) {
                    if (targetScale == 1F) {
                        resetState()
                    }
                    imgCurrentScale = targetScale
                }
    
                override fun onAnimationStart(animation: Animator?) {
                }
    
            })
            animator.start()
        }
    

    (4)图片放大状态下的拖动

    在move事件里

    /**
         * 处理放大状态下的move
         */
        private fun handleScaleStateMove(event: MotionEvent) {
            scaleStateDraging = true
            if (lastDisX == 0F) {
                lastDisX = disX
                lastDisY = disY
                getViewPager()?.onInterceptTouchEventFlag = false
                getViewPager()?.requestDisallowInterceptTouchEvent(true)
                return
            }
            val movedX = lastDisX - disX
            val movedY = lastDisY - disY
            val rect = getImgRect()
            //水平方向移动
            if (movedX < 0) {
                // 向右移动
                if (rect.left < 0) {
                    // 可以向右移动
                    this.scrollBy(movedX.toInt(), 0)
                } else {
                    handleViewpagerTouch(event, disX)
                    return
                }
            } else {
                // 向左移动
                if (rect.right > screenWidth) {
                    // 可以向左滑动
                    this.scrollBy(movedX.toInt(), 0)
                } else {
                    handleViewpagerTouch(event, disX)
                    return
                }
            }
            // 竖直方向移动
            if (movedY < 0) {
                // 向下移动
                if (rect.top < 0) {
                    this.scrollBy(0, movedY.toInt())
                }
    
            } else {
                // 向上移动
                if (rect.bottom > ImageBrowserUtil.getScreenHeight()) {
                    this.scrollBy(0, movedY.toInt())
                }
            }
            scaleStateMovedX = this.scrollX
            scaleStateMovedY = this.scrollY
            lastDisX = disX
            lastDisY = disY
        }
    

    关键点:当图片在放大状态下滑动到图片边缘继续滑动,此时需要viewpager来处理滑动,如果不做任何处理,此处会有一个闪动,因此需要通过反射设置viewpager的mLastMotionX属性

    /**
         * 把滑动事件交给viewpager处理 解决viewpager跳动问题
         */
        private fun handleViewpagerTouch(event: MotionEvent, disX: Float) {
            flingEnable = false
            val parentViewPager = getViewPager()
            parentViewPager.onInterceptTouchEventFlag = true
            parentViewPager?.requestDisallowInterceptTouchEvent(false)
    
            /**一次完整的触摸事件只需要设置一次*/
            if (gifResetParentLastMotion) {
                gifResetParentLastMotion = false
                parentViewPager.setLastMotionX(event.rawX)
            }
        }
    

    自定义viewpager里的方法:

    /**
         * 重要: 正常切换viewpager的item时,mLastMotionX这个值和手指按下的rawx值相等
         */
        fun setLastMotionX(x: Float) {
            try {
                val lastMotionXField = this.javaClass.superclass.getDeclaredField("mLastMotionX")
                val initialMotionXField = this.javaClass.superclass.getDeclaredField("mInitialMotionX")
    
                lastMotionXField.isAccessible = true
                lastMotionXField.set(this, x)
    
                initialMotionXField.isAccessible = true
                initialMotionXField.set(this, x)
            } catch (error: Exception) {
                error.printStackTrace()
            }
        }
    

    相关文章

      网友评论

        本文标题:基于Fresco的图片浏览器

        本文链接:https://www.haomeiwen.com/subject/jmcwoqtx.html