0%

漫画阅读器(三)翻页LayoutManager

看漫画我觉得还是卷纸模式舒服,小说的话翻书模式似乎好点。

还是从LinearLayoutManager开始,自带的onLayoutChildren自然就不能用了。

弄个currentPos存当前位置,重写onLayoutChildren

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
var currentPos = 0f

override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
if (orientation == VERTICAL) return super.onLayoutChildren(recycler, state)
detachAndScrapAttachedViews(recycler)

currentPos = Math.max(0f, Math.min(currentPos, itemCount - 1f))
if (state.itemCount <= 0 || state.isPreLayout) return
downPage = currentPos.toInt()

val currentIndex = currentPos.toInt()
val view = recycler.getViewForPosition(currentIndex)
addView(view)
measureChildWithMargins(view, 0, 0)
view.translationZ = 50f
view.translationX = -(currentPos - currentIndex) * width
layoutDecoratedWithMargins(view, 0, 0, view.measuredWidth, view.measuredHeight)
// 前一个
if (currentIndex - 1 >= 0) {
val nextView = recycler.getViewForPosition(currentIndex - 1)
addView(nextView)
nextView.translationX = -width * scale
nextView.translationZ = 100f
measureChildWithMargins(nextView, 0, 0)
layoutDecoratedWithMargins(nextView, 0, 0, view.measuredWidth, view.measuredHeight)
}
// 后一个
if (currentIndex + 1 < state.itemCount) {
val nextView = recycler.getViewForPosition(currentIndex + 1)
addView(nextView)
nextView.translationX = 0f
nextView.translationZ = 0f
measureChildWithMargins(nextView, 0, 0)
layoutDecoratedWithMargins(nextView, 0, 0, view.measuredWidth, view.measuredHeight)
}
}

首先根据itemCount约束currentPos的范围,onLayoutChildren要执行以下四步

  • detachAndScrapAttachedViews(recycler) 将所有子项回收

  • recycler.getViewForPosition(currentIndex) 获取子项

  • measureChildWithMargins 测量子项

  • layoutDecoratedWithMargins 布局子项

翻页只需要布局前后和当前三个子项,这里用translationX来位移子项,防止和缩放冲突,修改translationZ既能改变层级关系,还能给下层View带上阴影,一举两得。

currentPos独立于LinearlayoutManager,因此和位置相关的方法要一起重写,首先是computeHorizontalScrollOffsetcomputeHorizontalScrollRange,这两个函数返回的值用来判断是否滚动到边界:

1
2
3
4
5
6
7
8
9
override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
return if (orientation == VERTICAL) super.computeHorizontalScrollOffset(state)
else (currentPos * width).toInt() + if (scale > 1f) 1 else 0
}

override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
return if (orientation == VERTICAL) super.computeHorizontalScrollRange(state)
else itemCount * width
}

我一般只用scrollToPositionWithOffset修改位置,所以只重写这个:

1
2
3
4
override fun scrollToPositionWithOffset(position: Int, offset: Int) {
currentPos = position.toFloat()
super.scrollToPositionWithOffset(position, offset)
}

measureChildWithMargins在缩放的基础上,还要判断View是否小于RecyclerView的高度,小于要改成铺满

1
2
3
4
5
6
7
8
9
10
11
12
override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) {
...
if (orientation == VERTICAL || child.measuredHeight >= height) return
child.measure(
widthSpec, RecyclerView.LayoutManager.getChildMeasureSpec(
height, heightMode,
paddingTop + paddingBottom
+ lp.topMargin + lp.bottomMargin + heightUsed, RecyclerView.LayoutParams.MATCH_PARENT,
canScrollVertically()
)
)
}

同时,offset多了竖向偏移,要把offsetX复制一遍变成offsetYscrollVerticallyBy不能像横向一样由宽度乘以scale得到,要获取当前子项的实际高度:

1
2
3
4
5
6
7
8
9
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State?): Int {
if (orientation == VERTICAL) return super.scrollVerticallyBy(dy, recycler, state)

val view = findViewByPosition(currentPos.toInt())
val ddy = Math.max(Math.min(dy, (view?.height ?: height) - height - offsetY), -offsetY)
offsetY += ddy
offsetChildrenVertical(-ddy)
return if (scale == 1f) dy else ddy
}

在横向滚动scrollHorizontallyBy处理翻页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
val view = findViewByPosition(downPage)
val ddx = Math.max(
Math.min(
dx,
(if (orientation == VERTICAL) (width * scale).toInt() else view?.width ?: width) - width - offsetX
), -offsetX
)
offsetX += ddx
offsetChildrenHorizontal(-ddx)
view?.translationX = 0f
for (i in 0 until recyclerView.childCount) updateContent(recyclerView.getChildAt(i), this)

if (orientation == VERTICAL || scale > 1 || doingScale || view == null) return if (scale == 1f) dx else ddx

currentPos = Math.max(downPage - 1f, Math.min(currentPos + dx.toFloat() / width, downPage + 1f))
currentPos = Math.max(0f, Math.min(currentPos, itemCount - 1f))
view.translationX = -Math.max((currentPos - downPage) * width, 0f)
if (currentPos < downPage) findViewByPosition(downPage - 1)?.translationX = -(currentPos - downPage + 1) * width
return dx
}

最后,仿照SnapHelper让页面保持对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
view.onFlingListener = object : RecyclerView.OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
val minFlingVelocity = recyclerView.minFlingVelocity
if (orientation == VERTICAL || scale > 1f) return false

val targetPos = when {
Math.abs(velocityX) < minFlingVelocity -> Math.round(currentPos)
velocityX < 0 -> currentPos.toInt()
else -> Math.min(currentPos.toInt() + 1, itemCount - 1)
}
snapToTarget(targetPos)

return true
}
}
view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (orientation == VERTICAL || scale > 1f) return
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
snapToTarget(Math.round(currentPos))
}
}
})

snapToTarget是抄的PagerSnapHelper

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
fun snapToTarget(targetPos: Int) {
if (targetPos < 0 || targetPos > itemCount - 1) return
val smoothScroller: LinearSmoothScroller = createSnapScroller(targetPos)
smoothScroller.targetPosition = targetPos
startSmoothScroll(smoothScroller)
}

private fun createSnapScroller(targetPos: Int): LinearSmoothScroller {
return object : LinearSmoothScroller(recyclerView.context) {
override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
Log.v("snap", "$currentPos $targetPos")
val dx = -((currentPos - targetPos) * (width + 0.5f)).toInt()
val time = calculateTimeForDeceleration(Math.abs(dx))
if (time > 0) {
action.update(dx, 0, time, mDecelerateInterpolator)
}
}

override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}

override fun calculateTimeForScrolling(dx: Int): Int {
return Math.min(
MAX_SCROLL_ON_FLING_DURATION,
super.calculateTimeForScrolling(dx)
)
}
}
}

companion object {
const val MILLISECONDS_PER_INCH = 100f
const val MAX_SCROLL_ON_FLING_DURATION = 100 // ms
}

完整代码传送门