Fragment与ViewPager

Fragment与ViewPager

依附于Activity的组件,可以看作是拥有自己生命周期的UI组件

FragmentTransaction
  • show 方法:显示UI,不涉及生命周期

  • hide 方法:隐藏UI,不涉及生命周期

  • attach 方法:触发onStart和onResume

  • detach 方法:触发onPause和onStop

  • add 方法:添加Fragment并走完整的显示流程

  • remove 方法:移除Fragment并走完整的隐藏流程

FragmentPagerAdapter 与 FragmentStatePagerAdapter 区别

FragmentPagerAdapter:适用页面较少的情况,核心流程走的是attachdetach方法

FragmentStatePagerAdapter :适用页面较多的情况,核心流程走addremove方法,会触发onSaveInstanceState数据保存

有的人会将onCreateView里创建的变成全局变量长期持有,造成FragmentStatePagerAdapter 所能释放的数据并不会有多少,在内存有严苛要求的情况下可以在onDestoryView里释放View,每次在onCreateView里重新创建View即可

ViewPager中的生命周期问题

答:在缓存的范围内生命周期都会走到onResume方法,需要通过setUserVisibleHint方法来组合判断是否是当前显示页。同时需要注意的是,在ViewPager滑动时嵌套的子Fragment由于父Fragment没有任何生命周期变化,所以子FramgentsetUserVisibleHint并不会触发,需要自己手动通知子Fragment这是一个优化点,只让当前页面进行网络请求和播放动画

答:AndroidX中引入了Lifecycle生命周期判断,可以通过适配器中传入BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,这样就只有当前页会走到onResume,其它不可见页面只会走到onStart方法,同时由于父Fragment生命周期的变化,嵌套的子Fragment也变相的会收到页面不可见的通知

Fragment 界面显示异常解决思路

Fragment 界面异常,有可能 Activity 触发了onSaveInstanceState,状态被保存了,后续所有提交的事务即commit都不会生效,可以循着这个方向找答案

ViewPager中适配器生成Fragment问题

答:

有的人会提前将Fragment实例化添加到一个数组中,然后传入到PagerAdapter里,Activity回收时会产生重复创建的问题。由于Activity在回收时会将Fragment的状态保存下来存入到key为android:support:fragmentsBundle里,重建时在onCreate会尝试Fragment状态的恢复,不会触发适配器获取实例的方法,手动生成的Fragment数组就变成了错误的引用,外部对该数组进行操作就会产生异常(具体随业务而定)。

解决方案

方式一:在适当的函数里手动移除Bundle里存的值,可以在触发存储函数里,也可在onCreate函数里

方式二:自己适配数据重建逻辑,移除手动生成数组实例

ViewPager2特性

参考:https://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ==&mid=2650829623&idx=1&sn=79fa66ac994f09e501ae5c3563b5e5c2&chksm=80b7a7a9b7c02ebfcbada5c7dea37d3c9b57c79d6de201a7b76be20d80ea673eb2b3f526bfa7&scene=21#wechat_redirect

  1. 基于RecyclerView实现。这意味着RecyclerView的优点将会被ViewPager2所继承
  2. 支持竖直滑动。只需要一个参数就可以改变 滑动方向
  3. 支持关闭用户输入。通过setUserInputEnabled来设置是否禁止用户滑动页面
  4. 支持通过编程方式滚动。通过fakeDragBy(offsetPx)代码模拟用户滑动页面
  5. CompositePageTransformer 支持同时添加多个PageTransformer
  6. 支持DiffUtil ,可以添加数据集合改变的item动画
  7. 支持RTL (right-to-left)布局。我觉得这个功能对国内开发者来说可能用处不大..
ViewPager2的生命周期

答:默认只有当前页会走到onResume方法,其它缓存页面会走到onStart,同时由于父Fragment生命周期的变化,嵌套的子Fragment也变相的会收到页面不可见的通知

ViewPager2嵌套滚动问题

在同方向的嵌套滚动中,ViewPager2内部的视图是无法滚动的。为了支持同方向的嵌套滚动,需要在onInterceptTouchEvent中判断是否需要请求父视图不要拦截requestDisallowInterceptTouchEvent

参考:https://developer.android.com/training/animation/vp2-migration#nested-scrollables

方案:使用NestedScrollableHost包裹子滚动视图

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
87
88
89
90
91
92
93
94
95
96
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign

/**
* @Author: BenBen
* @CreateDate: 2020/12/27 23:07
* @Description:
*/
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}

private val child: View? get() = if (childCount > 0) getChildAt(0) else null

init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}

override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}

private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return

// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}

if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
Log.e("request", "true 1")
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
Log.e("request", "false 1")
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
Log.e("request", "true 2")
parent.requestDisallowInterceptTouchEvent(true)
} else {
Log.e("request", "false 2")
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}

private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
}