基于Fresco的图片浏览器
1.实现功能
使用Fresco实现类似于微信图片查看器的功能:
- 手势拖动关闭
- 手势缩放
- 双击缩放
- 单击,双击等各种回调
现在网络上有许多类似微信的图片查看器,多数是使用ImageView来做的,如果项目中使用的是Fresco来加载图片,则不能适用,因此撸了这个。
效果:
效果图
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()
}
}