採用SubsamplingScaleImageView做爲圖片的承載控件,該控件經過文件緩存的方式在不一樣的縮放比例下加載不一樣分辨率的圖片,避免了圖片過大致使的內存問題。而且實現了平移、放大縮小等操做。git
在此基礎上,添加了一些過渡的動畫等,優化查看圖片時的交互體驗:github
SubsamplingScaleImageView
做爲圖片的承載控件。PhotoFragment
中。PhotoActivity
,好比指示器,長按等的操做。PhotoPageBuilder
做爲啓動器,主要計算所需的數據,以及提升擴展。看下慢放中的動畫:緩存
將這個動畫拆分爲三個部分:安全
將其轉換爲代碼(photo
是SubsamplingScaleImageView
控件,root
是父容器):markdown
photo.width
:mInImgSize
-> root.width
photo.translation
:mInLocation
->[0,0]
root.backgroundColor
:transparent
->black
private fun inAnimation() {
......
val scaleOa1 = ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener {
var vaule = it.animatedValue as Float
val mWidth = (root.width - mInImgSize[0]) * vaule + mInImgSize[0]
val mHeight = (root.height - mInImgSize[1]) * vaule + mInImgSize[1]
photo.updateLayoutParams<FrameLayout.LayoutParams> {
width = mWidth.toInt()
height = mHeight.toInt()
}
vaule = 1 - vaule
photo.translationX = vaule * (mInLocation[0] - mInImgSize[0] / 2f)
photo.translationY = vaule * (mInLocation[1] - mInImgSize[1] / 2f)
}
}
val colorOa =
ValueAnimator.ofObject(ArgbEvaluator(), Color.TRANSPARENT, Color.BLACK)
colorOa.addUpdateListener {
root.setBackgroundColor(it.animatedValue as Int)
}
colorOa.duration = 150
scaleOa1.duration = 300
setIn1 = AnimatorSet().apply {
playTogether(scaleOa1, colorOa)
start()
}
}
複製代碼
退出動畫的過程大體上是進入動畫的反向:網絡
photo.scale
:目前的縮放比例-> 退出目標尺寸的縮放比例photo.translation
:[0,0]
->mOutImgLocation
root.background.alpha
:目前透明度->0private fun outAnimation() {
......
val xOa = ObjectAnimator.ofFloat(
photo,
"translationX",
0f,
mOutLocation[0].toFloat() - photo.width / 2
)
val yOa = ObjectAnimator.ofFloat(
photo,
"translationY",
0f,
mOutLocation[1].toFloat() - photo.height / 2
)
val colorOa = ObjectAnimator.ofInt(root.background, "alpha", root.background.alpha, 0)
colorOa.duration = 150
xOa.duration = 300
yOa.duration = 300
if (photo.isReady) {
photo.minScale = min(photo.scale, 1.0f * mOutImgSize[0] / mBitmapSize[0])
photo.animateScale(1.0f * mOutImgSize[0] / mBitmapSize[0])
?.withDuration(300)
?.withInterruptible(false)
?.start()
}
setOut = AnimatorSet().apply {
playTogether(colorOa, xOa, yOa)
start()
}
}
複製代碼
private fun initEvent() {
val gestureDetector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll( e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float ): Boolean {
return onDrag(distanceX, distanceY)
}
}
)
photo.setOnTouchListener { _, event ->
if (currentState == STATE_DRAG) {
currentState = STATE_NOTHING
onDragEnd()
return@setOnTouchListener true
}
return@setOnTouchListener gestureDetector.onTouchEvent(event)
}
}
複製代碼
private fun onDrag(dx: Float, dy: Float): Boolean {
if (currentState == STATE_NOTHING && isLessThanScreenScale()) {
if (abs(dy) - abs(dx) < 0.5) {
val parent = photo.parent
parent?.requestDisallowInterceptTouchEvent(false)
return false
} else if (dy < 0 && abs(dy) - abs(dx) > 0.5) {
photo.minScale = 0.6f * mScreenScale
currentState = STATE_DRAG
}
}
if (currentState != STATE_DRAG || !isLessThanScreenScale()) {
return false
}
photo.scrollBy(dx.toInt(), dy.toInt()) // 移動圖像
alpha += dy * 0.0005f
intAlpha += (dy * 0.3).toInt()
if (alpha > 1f) {
alpha = 1f
} else if (alpha < 0f) {
alpha = 0f
}
if (intAlpha < 50) {
intAlpha = 50
} else if (intAlpha > 255) {
intAlpha = 255
}
root.background.alpha = intAlpha // 更改透明度
if (alpha >= 0.6 && photo.isReady) {
photo.setScaleAndCenter(alpha * mScreenScale, photo.center)
}
return true
}
複製代碼
private fun onDragEnd() {
if (photo.scale - mScreenScale > 10e-8f) {
return
}
if (root.background.alpha <= 150) {
outAnimation()
} else {
inAnimation2()
}
}
private fun outAnimation() {
......
val scrollX = photo.scrollX
val scrollY = photo.scrollY
val scrollOa = ValueAnimator.ofFloat(1f, 0f).apply {
addUpdateListener {
val vaule = it.animatedValue as Float
photo.scrollTo(
(vaule * scrollX).toInt(),
(vaule * scrollY).toInt()
)
}
}
......
}
/** * 下滑回滾動畫 */
private fun inAnimation2() {
if (root.background.alpha == 255 && photo.scrollX == 0 && photo.scrollY == 0 && photo.scale - mScreenScale > 10e-8f) {
return
}
val alphaOa = ObjectAnimator.ofInt(root.background, "alpha", root.background.alpha, 255)
val scrollXOa = ObjectAnimator.ofInt(photo, "scrollX", photo.scrollX, 0)
val scrollYOa = ObjectAnimator.ofInt(photo, "scrollY", photo.scrollY, 0)
alphaOa.duration = 100
scrollXOa.duration = 200
scrollYOa.duration = 200
if (photo.isReady) {
photo.animateScale(mScreenScale)
?.withDuration(200)
?.withInterruptible(false)
?.start()
}
setIn2 = AnimatorSet().apply {
playTogether(alphaOa, scrollXOa, scrollYOa)
start()
}
}
複製代碼
縮放的時候,能夠縮放到比最小縮放比例小的尺寸,並在釋放後回彈。回彈的過程與下滑的回彈相匹配,能夠直接複用。因此實現這個功能只須要在開始縮放的時候將minScale
從新賦值,在結束的時候調用下滑的回彈。app
將縮放開始的觸發綁定到兩根手指觸摸屏幕上,將縮放結束的觸發綁定到全部手指離開屏幕:ide
photo.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_UP) {
if (currentState == STATE_DRAG || currentState == STATE_SCALE) {
currentState = STATE_NOTHING
onDragEnd()
return@setOnTouchListener true
}
} else if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN){
if (currentState == STATE_NOTHING) {
currentState = STATE_SCALE
photo.isPanEnabled = true
photo.isZoomEnabled = true
photo.minScale = 0.8f * mScreenScale
}
}
return@setOnTouchListener gestureDetector.onTouchEvent(event)
}
複製代碼
這會有一個問題,將minScale
賦值以後,雙擊的縮放比例也會對應變化,因此將雙擊也攔截下來:oop
這裏有另外一種作法,就是在回彈以後將minScale
重置爲默認值,可是須要重置的狀況會特別多,因此再也不去限制minScale
而是去攔截處理雙擊post
......
val gestureDetector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
......
override fun onDoubleTap(e: MotionEvent?): Boolean {
if (photo.scale > 0.9f * mDoubleTapScale && photo.isReady) {
photo.animateScale(mScreenScale)
?.withDuration(200)
?.withInterruptible(false)
?.start()
return true
}
return super.onDoubleTap(e)
}
}
)
複製代碼
圖片加載須要等待網絡,時間上不肯定,其次,網絡加載完成後,加載到控件裏面也須要必定時間,這個時間受圖片大小以及手機性能影響。總體加載流程:
photo
。private fun loadPreviewImage() {
Glide.with(photo)
.asFile()
.load(imageUrl)
.into(object : CustomTarget<File?>() {
......
override fun onResourceReady( resource: File, transition: Transition<in File?>? ) {
......
showPreviewImage()
photo.setImage(
ImageSource.uri(Uri.fromFile(resource))
)
}
})
}
複製代碼
//圖片加載狀態監聽
photo.setOnImageEventListener(object : SubsamplingScaleImageView.OnImageEventListener {
override fun onImageLoaded() {
if (isReadyLoadingBig) {
loadBigImage()
} else {
isReadyLoadingBig = true
}
}
})
//進入動畫監聽
setIn1.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
if (isReadyLoadingBig) {
loadBigImage()
} else {
isReadyLoadingBig = true
}
}
override fun onAnimationCancel(animation: Animator?) {
if (isReadyLoadingBig) {
loadBigImage()
} else {
isReadyLoadingBig = true
}
}
})
複製代碼
ImageView
,對photo進行遮擋,而且photo開始載入圖片。ImageView
清除。private fun loadBigImage() {
Glide.with(photo)
.asFile()
.load(bigImageUrl)
.into(object : CustomTarget<File?>() {
......
override fun onResourceReady( resource: File, transition: Transition<in File?>? ) {
......
showPreviewImage()
photo.setImage(
ImageSource.uri(Uri.fromFile(resource))
)
}
})
}
private fun showPreviewImage() {
if (currentState != STATE_NOTHING){
previewBitmap = null
return
}
ivPreview.setImageBitmap(previewBitmap)
ivPreview.isVisible = true
handler.postDelayed({
ivPreview.setImageDrawable(null)
ivPreview.isVisible = false
previewBitmap = null
currentState = STATE_NOTHING
}, 200)
}
複製代碼
前面不少地方已經使用到了狀態,最後肯定下來的狀態會有這五個,在爲了交互體驗以及安全的前提下,只有空狀態下,才能進行狀態的變化,以及觸摸事件的處理。一個最直觀的狀況就是不能在動畫的播放過程當中不能進行點擊,下滑等各類操做。
companion object {
private const val STATE_NOTHING = -1 // 空狀態,沒有操做
private const val STATE_DRAG = -2 // 下滑狀態
private const val STATE_SCALE = -3 // 操做圖片狀態, 標識爲落下過雙指而且沒有所有離開屏幕
private const val STATE_ANIMATE = -4 // 動畫播放狀態
private const val STATE_PREVIEW_LOAD = -5 // ImageView顯示圖片的那段時間,200ms
}
複製代碼
首先是在狀態變化時,對不一樣狀況進行不一樣程度的鎖
private var currentState = STATE_NOTHING
set(value) {
when (value) {
STATE_DRAG -> {
/** * 下滑 */
if (field != STATE_DRAG) {
onAnimatorListener?.onStart()
}
photo.isZoomEnabled = false
}
STATE_ANIMATE -> {
/** * 動畫 */
if (field != STATE_ANIMATE) {
onAnimatorListener?.onStart()
}
photo.isPanEnabled = false
photo.isZoomEnabled = false
}
STATE_NOTHING -> {
/** * 空 */
if (field == STATE_ANIMATE) {
onAnimatorListener?.onEnd()
}
alpha = 1f
intAlpha = 255
photo.isPanEnabled = true
photo.isZoomEnabled = true
}
STATE_SCALE -> {
/** * 操做圖片 */
photo.isPanEnabled = true
photo.isZoomEnabled = true
photo.minScale = 0.8f * mScreenScale
}
STATE_PREVIEW_LOAD -> {
photo.isPanEnabled = false
photo.isZoomEnabled = false
}
}
field = value
}
複製代碼
在狀態變換時或者操做時進行判斷,是否處於空狀態
val gestureDetector = GestureDetector(
context,
object : GestureDetector.SimpleOnGestureListener() {
......
override fun onLongPress(e: MotionEvent?) {
if (currentState == STATE_NOTHING) {
longClickListener?.onLongClick(photo, imageUrl)
}
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
if (currentState == STATE_NOTHING) {
exit()
return true
}
return false
}
}
)
複製代碼
目前的結構,PhotoFragment
存放着交互的邏輯,而交互的數據(起始位置、結束位置、配合顯示的控件等等)都存放在Activity
中,後續根據不一樣的場景可設置更多的Activity
,在Activity
中控制不一樣的數據以實現不一樣的交互方式。目前,有不少套的查看圖片的組件在項目中運行,後續逐步考慮所有統一接入。
在查看圖片的時候,先翻動再退出,若是退出的時候能夠通知到外面已經翻頁了,體驗會比較好。最直接的方法就是經過activity
退出的時候回傳,但這須要在外面的進行接收,須要在外面更改邏輯,不夠優雅。
後續思考是否能夠經過builder傳入一個回調,經過liveData
將數據傳遞出去,這樣會有一個問題,不知道何時將這個liveData
釋放掉。
這種狀況簡單說就是多張圖片的動畫位置並不一致,處理起來並不困難,在builder中計算屏幕中顯示着的圖片的位置信息,將不在屏幕中的圖片設置一個默認的位置,經過Activity
給不一樣的fragment
傳入不一樣的位置信息便可。
目前是直接設置了一個固定值200ms,但受到圖片大小以及設備的影響,200ms圖片有可能並不能完成載入,仍是會出現閃黑屏的狀況,這個時長應該如何去調整。
把一杯水拿起來喝掉十分簡單,但把杯子在桌子上移動一釐米是困難的。把這個不算複雜的功能儘可能去作到最好,不斷的去考慮在這短短几百毫秒的時間裏,如何讓整個過程更加的舒服,更加的流暢,印象最深的就是如何在圖片放大的狀況下作退出的動畫,前先後後試了好幾天。但作出來來的東西大多數人甚至感知不到,整個過程是極吃力不討好的。好在結果總算不差,但願本身能將這份執拗堅持下去,但行好事莫問前程。