以Fresco图片做transitionName跳转新页面出现白/黑屏问题
Published in:2026-03-21 |

以Fresco图片做transitionName跳转新页面出现白/黑屏问题

最近接了一个需求,需要以图片为共享元素,给两个页面加上过渡动画,因为这两个页面都是Activity,所以,很快啊,技术方案就想出来了:将两个页面的图片作为共享元素,以同一个fileId为transitionName执行动画跳转。

背景

FrescoTransitionApp.kt

因为图片是从网络上下载下来,且为了提高展示速度,所以使用了Fresco框架(有三级缓存机制)

1
2
3
4
5
6
7
8
9
10
11
package com.phc.demo.fresco.transition

import android.app.Application
import com.facebook.drawee.backends.pipeline.Fresco

class FrescoTransitionApp : Application() {
override fun onCreate() {
super.onCreate()
Fresco.initialize(this)
}
}

ActivityA.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.phc.demo.fresco.transition

import android.content.Intent
import android.graphics.drawable.Animatable
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import com.demo.phc.databinding.ActivityAFrescoTransitionBinding
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.controller.BaseControllerListener
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
import com.facebook.imagepipeline.image.ImageInfo
import com.phc.log.PHCLog

class ActivityA : AppCompatActivity() {
private lateinit var binding: ActivityAFrescoTransitionBinding

companion object {
private const val IAMGE_URL =
"https://sns-na-i2.xhscdn.com/notes_pre_post/1040g3k031to4malimu0g5n2jtgf47g1bhbrofsg?imageView2/2/w/1440/format/heif/q/45&redImage/frame/0&ap=1&sc=DETAIL&sign=e1e6ab635bbbf58872eaeeb1bf522461&t=69bd2e52&origin=0"
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAFrescoTransitionBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}

private fun initView() {
val listener = object : BaseControllerListener<ImageInfo>() {
override fun onFinalImageSet(
id: String?,
imageInfo: ImageInfo?,
animatable: Animatable?
) {
imageInfo?.let { info ->
// 实际像素尺寸
val width = info.width
val height = info.height
val pixelBytes = 4
val bitmapSize = width * height * pixelBytes
PHCLog.d(
"ActivityA",
"Image load finished, bitmap size is ${
String.format(
"%.2f",
bitmapSize / 1024f / 1024f
)
}MB"
)
}
}
}
binding.draweeViewA.apply {
post {
PHCLog.d(
"ActivityA",
"Image loaded, image view size is ${this.width}x${this.height}"
)
}
setOnClickListener {
// 点击图片后跳转到 ActivityB
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this@ActivityA,
this,
"shared_image_transition"
)
val intent = Intent(this@ActivityA, ActivityB::class.java)
startActivity(intent, options.toBundle())
}
val controller = Fresco.newDraweeControllerBuilder()
// .setRetainImageOnDetach(true) // ✅ Controller 上有,保留
.setUri(IAMGE_URL)
.setControllerListener(listener)
.build()

val hierarchy = GenericDraweeHierarchyBuilder(resources)
// 删掉 .setRetainImageOnDetach(true) 这行
.build()

this.controller = controller
this.hierarchy = hierarchy
}
}
}

activity_a_fresco_transition.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/draweeViewA"
android:layout_width="match_parent"
android:layout_height="300dp"
android:transitionName="shared_image_transition"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

ActivityB.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.phc.demo.fresco.transition

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.demo.phc.databinding.ActivityBFrescoTransitionBinding

class ActivityB : AppCompatActivity() {
private lateinit var binding: ActivityBFrescoTransitionBinding

companion object {
private const val IAMGE_URL = "https://sns-na-i2.xhscdn.com/notes_pre_post/1040g3k031to4malimu0g5n2jtgf47g1bhbrofsg?imageView2/2/w/1440/format/heif/q/45&redImage/frame/0&ap=1&sc=DETAIL&sign=e1e6ab635bbbf58872eaeeb1bf522461&t=69bd2e52&origin=0"
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityBFrescoTransitionBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}

private fun initView() {
binding.draweeViewB.apply {
setImageURI(IAMGE_URL)
transitionName = "shared_image_transition"
setOnClickListener {
finishAfterTransition()
}
}
}
}

activity_b_fresco_transition.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/draweeViewB"
android:layout_width="match_parent"
android:layout_height="300dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

展示效果

可以看到,每次动效结束之后,都会变成白屏。这也是本次需求遇到的最大的障碍

原因

出现这个问题的表面原因其实很好“猜测”,肯定是因为这张图片太大了,导致ActivityA跳转到ActivityB的时候,**bitmap被回收掉了,所以导致做退场动画(ActivityB->ActivityA)的时候,Fresco框架根据url在缓存中找不到这个bitmap了。**我们可以打印下日志看看:

img

可以看到这张图还是蛮大的(>10MB),所以具体原因是:

Activity A 进入后台(被 B 覆盖)时会 onStop,Fresco 默认会在视图从窗口分离时释放已解码的位图以省内存;返回时共享元素过渡先显示视图,但图片需从缓存/网络重新加载,期间会显示占位(常为白色)。

A 被盖住后会 onStop,此时 SimpleDraweeView 会从窗口上 detach。Fresco 默认会在 detach 时 释放已解码的位图(省内存),所以内存里的那一帧图可能没了。

finishAfterTransition() 做的是「反向共享元素」过渡一开始就要在 A 上画出共享的那块区域;但这时 图往往还没重新 decode 完(即使磁盘缓存很快,也有一段空窗)。空窗期 Drawee 会画 占位/背景,很多主题下就是 白底,你就看到「闪白」。

为什么去 B 不明显?前进时 B 上的图正在 setImageURI 加载,过渡和加载节奏容易「对齐」;而回 A 时,问题出在 A 在后台被 Fresco 清过图,和「过渡要立即有画面」冲突更大。

解决方案

1. 页面不可见时不清除Bitmap

这个方式很简单,就是对Activity的SimpleDraweeView设置setLegacyVisibilityHandlingEnabled(true),即

img

我们可以看一下AI对setLegacyVisibilityHandlingEnabled方法的解释

img

配置值 核心行为 适用场景 优缺点
true(启用旧版) View 不可见时,图片加载不中断,加载完成后缓存 1. 需要「预加载」图片(比如 View 先 GONE 后快速显示);2. 兼容 Fresco 1.x 及更早版本的旧逻辑;3. 避免「View 刚显示就重新加载」导致的闪屏 ✅ 预加载提升体验;✅ 兼容旧代码❌ 浪费网络 / 内存(加载看不见的图片)
false(默认新版) View 不可见时,立即暂停 / 取消加载,重新可见时恢复加载 1. 列表 / 长页面(大量 View 滑出屏幕);2. 对内存 / 流量敏感的场景(如低配置设备、移动网络) ✅ 节省资源;✅ 避免无用加载❌ 重新显示 View 时可能需要重新加载,有短暂空白

2. 对ActivityA的bitmap进行压缩,减小内存,使其不容易被回收

还有一种方式能够不加剧内存压力的方式,那就是对在做转场动画的“快照”时,对其进行压缩处理,降低内存,减小被回收的概率,具体方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
FUNCTION makeSceneTransitionOptionsWithCompressedBitmap(imageBean: ImageBean) -> ActivityOptionsCompat OR NULL:
// 前置检查:获取上下文和目标View,校验有效性
activity = 从context获取当前Activity
IF activity IS NULL: RETURN NULL

photoView = 获取当前位置的大图展示View(PhotoView)
IF photoView IS NULL OR photoView宽度/高度 ≤ 0: RETURN NULL

pos = 获取当前图片位置
srcBitmap = NULL // 原始Bitmap
isOwnedBitmap = FALSE // 标记是否需要手动回收原始Bitmap

TRY:
// 步骤1:获取原始Bitmap(区分直播照片/普通照片)
IF 当前位置是直播照片:
liveFrameBitmap = 获取直播照片当前帧的Bitmap
IF liveFrameBitmap IS NOT NULL:
srcBitmap = liveFrameBitmap
isOwnedBitmap = TRUE // 直播帧Bitmap需手动回收
ELSE:
(srcBitmap, isOwnedBitmap) = 从photoView提取原始Bitmap
ELSE:
(srcBitmap, isOwnedBitmap) = 从photoView提取原始Bitmap

IF srcBitmap IS NULL: RETURN NULL

CATCH 内存溢出异常:
打印日志「获取共享元素Bitmap失败(OOM)」
RETURN NULL
CATCH 通用异常:
打印日志「获取共享元素Bitmap失败」
RETURN NULL

// 打印压缩前Bitmap信息(宽/高/内存大小)
打印日志「压缩前:宽=${srcBitmap.width} 高=${srcBitmap.height} 大小=${srcBitmap.byteCount}KB」

// 步骤2:等比例压缩Bitmap(不超过最大尺寸,不放大)
maxSize = 共享元素Bitmap最大限制尺寸
// 计算等比例缩放系数(取宽/高缩放比的较小值,且不超过1倍)
scale = MIN(maxSize/srcBitmap.width, maxSize/srcBitmap.height)
scale = MIN(scale, 1.0)

targetWidth = 取整(srcBitmap.width * scale),最小为1
targetHeight = 取整(srcBitmap.height * scale),最小为1

compressedBitmap = NULL
TRY:
// 创建缩放后的Bitmap
scaledBitmap = 创建缩放后的Bitmap(srcBitmap, targetWidth, targetHeight, 开启抗锯齿)

IF scaledBitmap 与 srcBitmap是同一对象:
// 尺寸未变,创建独立副本避免引用冲突
compressedBitmap = 创建Bitmap副本(srcBitmap)
IF isOwnedBitmap: 回收srcBitmap
ELSE:
compressedBitmap = scaledBitmap
IF isOwnedBitmap: 回收srcBitmap

CATCH 通用异常:
打印日志「Bitmap压缩失败」
// 异常时回收原始Bitmap(若需手动回收)
IF isOwnedBitmap:
TRY: 回收srcBitmap
CATCH: 打印日志「回收原始Bitmap失败」
RETURN NULL

// 打印压缩后Bitmap信息
打印日志「压缩后:宽=${compressedBitmap.width} 高=${compressedBitmap.height} 大小=${compressedBitmap.byteCount}KB」

// 步骤3:将压缩后的Bitmap设回View,配置共享元素
IF photoView是ImageView类型:
将compressedBitmap封装为BitmapDrawable,设置给photoView
为photoView设置transitionName = imageBean.fileid
// 创建并返回共享元素转场动画选项
RETURN 创建转场动画选项(activity, photoView, imageBean.fileid)
ELSE:
// View类型不匹配,回收压缩后的Bitmap
回收compressedBitmap
RETURN NULL

Next:
Fresco的缓存命中和使用策略