采用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图片有可能并不能完成载入,仍是会出现闪黑屏的状况,这个时长应该如何去调整。
把一杯水拿起来喝掉十分简单,但把杯子在桌子上移动一厘米是困难的。把这个不算复杂的功能尽可能去作到最好,不断的去考虑在这短短几百毫秒的时间里,如何让整个过程更加的舒服,更加的流畅,印象最深的就是如何在图片放大的状况下作退出的动画,前先后后试了好几天。但作出来来的东西大多数人甚至感知不到,整个过程是极吃力不讨好的。好在结果总算不差,但愿本身能将这份执拗坚持下去,但行好事莫问前程。