1.實(shí)現(xiàn)功能
使用Fresco實(shí)現(xiàn)類似于微信圖片查看器的功能:
- 手勢(shì)拖動(dòng)關(guān)閉
- 手勢(shì)縮放
- 雙擊縮放
- 單擊,雙擊等各種回調(diào)
現(xiàn)在網(wǎng)絡(luò)上有許多類似微信的圖片查看器,多數(shù)是使用ImageView來做的,如果項(xiàng)目中使用的是Fresco來加載圖片,則不能適用,因此擼了這個(gè)。
效果:

2.思路
(1)手勢(shì)拖動(dòng)
動(dòng)態(tài)的設(shè)置圖片的寬高以及scrollX和scrollY
(2)雙指縮放
動(dòng)態(tài)的設(shè)置圖片的寬高
3.難點(diǎn)
(1)圖片放大后,實(shí)際的邊界判斷,這涉及到圖片滑動(dòng)到邊緣后的事件處理
(2)放大狀態(tài)下,左右滑動(dòng)到圖片邊界,這個(gè)時(shí)候要把觸摸事件交給viewpager,如果不處理會(huì)有圖片跳動(dòng)的問題。
(3)慣性滑動(dòng)處理
4.可定制點(diǎn)
(1)現(xiàn)在gif是在顯示的時(shí)候才執(zhí)行動(dòng)畫,且切換gif的時(shí)候停止動(dòng)畫,這點(diǎn)可根據(jù)需求來做
(2)可以先加載縮略圖再加載原圖,目前是只加載原圖
(3)設(shè)置圖片url傳的模型可以根據(jù)需求來寫,現(xiàn)在傳的是List<String>
(4)入場和退場的縮放動(dòng)畫依賴rect,如果沒有rect默認(rèn)是透明度變化動(dòng)畫,這里可以根據(jù)實(shí)際需求來寫
5.具體實(shí)現(xiàn)
(1)手勢(shì)拖動(dòng)-非放大狀態(tài)
一開始當(dāng)手指下滑才會(huì)觸發(fā)滑動(dòng)手勢(shì),根據(jù)下滑的距離來計(jì)算圖片縮放倍率,這個(gè)倍率可調(diào)整,然后設(shè)置圖片寬高和scroll。
已刪除暫時(shí)不用看的代碼,主要看move這個(gè)事件處理。
/**
* 初始滑動(dòng),雙指縮放手勢(shì)
*/
private fun handleDragEvent(event: MotionEvent?): Boolean {
if (event == null) {
return false
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
getViewPager()?.requestDisallowInterceptTouchEvent(true)
getViewPager().onInterceptTouchEventFlag = false
// 初始化或者設(shè)置一些參數(shù) 用于手勢(shì)滑動(dòng)
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
//一根手指滑動(dòng) 包括下拉關(guān)閉 和 圖片放大狀態(tài)的拖動(dòng)
if (event.pointerCount == 1) {
// 上下滑動(dòng)的手勢(shì)
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
// 設(shè)置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
}
}
里面用到的方法:
/**
* 根據(jù)縮放倍率設(shè)置圖片寬高
*/
private fun setImageWidthHeightByScale(scale: Float) {
val lp = this.layoutParams
lp.width = (displayWidth * scale).toInt()
lp.height = (displayHeight * scale).toInt()
this.layoutParams = lp
}
拖動(dòng)之后,然后松手,有兩種情況(根據(jù)滑動(dòng)距離判斷):
- 回到遠(yuǎn)處
- 關(guān)閉
關(guān)閉有兩種動(dòng)畫,縮放和漸隱。如果知道回坑的位置,就執(zhí)行縮放動(dòng)畫回坑,否則就是漸隱動(dòng)畫。下面看看松開手之后的處理:
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (draging) {
if (disY > CLOSE_DISTANCE) {
imgAnimClose()
} else {
imgAnimToBack()
}
return true
}
draging = false
}
先看回到遠(yuǎn)處,用屬性動(dòng)畫實(shí)現(xiàn),該view的動(dòng)畫都是用屬性動(dòng)畫實(shí)現(xiàn)的
/**
* 如果拖拽距離小于設(shè)定值,則返回原處
* 就是一個(gè)屬性動(dòng)畫
*/
private fun imgAnimToBack() {
var currentValue = 0F
var animPercent = 0F //動(dòng)畫執(zhí)行百分比
val scrollYTemp = this.scrollY// 剛開始執(zhí)行動(dòng)畫時(shí)候的偏移量
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()
}
然后看看關(guān)閉動(dòng)畫:
/**
* 關(guān)閉動(dòng)畫
* 外部可直接調(diào)用該方法關(guān)閉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)雙指縮放
就是根據(jù)兩指間的距離,動(dòng)態(tài)設(shè)置圖片寬高。
雙指縮放在move事件里面實(shí)現(xiàn)
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());//兩點(diǎn)間的距離
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
}
手勢(shì)放大或縮小也是動(dòng)態(tài)設(shè)置圖片寬高。
/**
* 雙擊縮小或放大
*/
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)圖片放大狀態(tài)下的拖動(dòng)
在move事件里
/**
* 處理放大狀態(tài)下的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()
//水平方向移動(dòng)
if (movedX < 0) {
// 向右移動(dòng)
if (rect.left < 0) {
// 可以向右移動(dòng)
this.scrollBy(movedX.toInt(), 0)
} else {
handleViewpagerTouch(event, disX)
return
}
} else {
// 向左移動(dòng)
if (rect.right > screenWidth) {
// 可以向左滑動(dòng)
this.scrollBy(movedX.toInt(), 0)
} else {
handleViewpagerTouch(event, disX)
return
}
}
// 豎直方向移動(dòng)
if (movedY < 0) {
// 向下移動(dòng)
if (rect.top < 0) {
this.scrollBy(0, movedY.toInt())
}
} else {
// 向上移動(dòng)
if (rect.bottom > ImageBrowserUtil.getScreenHeight()) {
this.scrollBy(0, movedY.toInt())
}
}
scaleStateMovedX = this.scrollX
scaleStateMovedY = this.scrollY
lastDisX = disX
lastDisY = disY
}
關(guān)鍵點(diǎn):當(dāng)圖片在放大狀態(tài)下滑動(dòng)到圖片邊緣繼續(xù)滑動(dòng),此時(shí)需要viewpager來處理滑動(dòng),如果不做任何處理,此處會(huì)有一個(gè)閃動(dòng),因此需要通過反射設(shè)置viewpager的mLastMotionX屬性
/**
* 把滑動(dòng)事件交給viewpager處理 解決viewpager跳動(dòng)問題
*/
private fun handleViewpagerTouch(event: MotionEvent, disX: Float) {
flingEnable = false
val parentViewPager = getViewPager()
parentViewPager.onInterceptTouchEventFlag = true
parentViewPager?.requestDisallowInterceptTouchEvent(false)
/**一次完整的觸摸事件只需要設(shè)置一次*/
if (gifResetParentLastMotion) {
gifResetParentLastMotion = false
parentViewPager.setLastMotionX(event.rawX)
}
}
自定義viewpager里的方法:
/**
* 重要: 正常切換viewpager的item時(shí),mLastMotionX這個(gè)值和手指按下的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()
}
}