30

写了个视差滚动布局 ParallaxLayout

 4 years ago
source link: https://antiless.com/write-a-parallax-layout/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

新项目用到了大量的视差滚动效果,今天写了控件做支持,并非成熟但适用大多数简单场景,把大致思路在这里写下。

什么是视差滚动, 简单说就是

不同组件按不同速度滚动

像这种效果:

jieIVja.gif

我们今天介绍的主要是以纵向的滚动效果为主,当然如果你要实现横向思路也一样。

1. 思路

简单实现这个效果思路很简单,只需要监听滚动控件的滚动行为,再根据不同组件的速度要求调整偏移量就:accept:️了。

要做的通用点,我的思路是实现一个视差滚动的父控件,子 view 添加一个 layout_parallax_speed 的属性,这样无需多写 java 代码,就可以得到所需要的参数,在布局代码中就可以完成定义。

最后的调用类似这种效果:

<ScrollView>
    <ParallaxLayout>
        <TextView layout_parallax_speed="1"/>
        <ImageView layout_parallax_speed="1.5"/>
        <LinearLayout layout_parallax_speed="0.8"/>
        <AnyView layout_parallax_speed="0.7"/>
        <AnyView layout_parallax_speed="1"/>
    </ParallaxLayout>
</ScrollView>

其实这里要注意,我们的布局是介于 ScrollView 和视差组件之间的一个布局,因此它可能是 LinearLayout 可能是 RelativeLayout 也可能是 ConstraintLayout , 可能是任意一个 ViewGroup 的子类,因为我也不知道开发者需要一个什么样的内部结构。

再一点,这个布局本身不具备 Scroll 能力,仍需要嵌套在外部的可滚动组件。

可能你有疑问,为什么不直接写一个 ParallaxScrollView 之类的呢。

这样就不需要担心内部结构的问题,不需要写多种布局的子类,自身也能控制滚动行为。

也就是类似下面这种结构的:

<ParallaxScrollView>
    <LinearLayout>
        <TextView layout_parallax_speed="1"/>
        <ImageView layout_parallax_speed="1.5"/>
        <LinearLayout layout_parallax_speed="0.8"/>
        <AnyView layout_parallax_speed="0.7"/>
        <AnyView layout_parallax_speed="1"/>
    </LinearLayout>
</ParallaxScrollView>

原因也很简单,

AndroidViewGroup 没有跨越父子关系设置 LayoutParam 的能力(只能父子间,不能爷孙间),也就是说,这种结构上,最外侧的 ParallaxScrollView 不能以正常方式获取到最内层的 layout_parallax_speed 属性。

2. 实现

需要解决两个问题

  1. 定义并获取各子控件的速度属性 layout_parallax_speed
  2. 检测滚动,对子控件做相对移动

2.2 定义并获取子控件布局属性

这里先介绍一下自定义 ViewGroup ,定义和获取子 View 布局参数的流程。

我们知道 xml 中定义的这些属性都会转换成 AttributeSet , 然后在代码中保存在 LayoutParam 里。

然后会在 addView 方法时, 然后把它和 view 联系在一起。

ViewGroup 添加 View 流程图:

ve2yu2A.png!web

因此我们要做的 三件事 是:

  1. 定义一个 layout_parallax_speed 属性
  2. 定义一个 LayoutParam ,添加一个成员变量 parallaxSpeed 用来保存 1 中定义的值
  3. 默认为子View 生成我们定义的 LayoutParams

相对的, 我们的流程图 应该是:

  1. 定义一个 layout_parallax_speed 属性
<declare-styleable name="ParallaxLayout">
    <attr name="layout_parallax_speed" format="float"/>
</declare-styleable>
  1. 定义一个 LayoutParam ,解析 layout_parallax_speed 值并保存
class LayoutParams : RelativeLayout.LayoutParams {
    var parallaxSpeed: Float = 1f
    constructor(context: Context, attrs: AttributeSet?): super(context, attrs) {
        val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ParallaxLayout)
        parallaxSpeed = a.getFloat(R.styleable.ParallaxLayout_layout_parallax_speed, 1f)
        a.recycle()

    }
    constructor(width: Int, height: Int): super(width, height)
    constructor(layoutParams: MarginLayoutParams): super(layoutParams)
    constructor(layoutParams: LayoutParams): super(layoutParams)
}
  1. 重写 generateLayoutParams , 默认为子 View 生成我们定义的 LayoutParams
override fun generateLayoutParams(attrs: AttributeSet?): RelativeLayout.LayoutParams {
    return LayoutParams(context, attrs)
}

2.2 检测滚动

在添加到窗口时,对可滚动的父布局添加 addOnScrollChangedListener 方法

// TODO 添加关键代码

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    if (parent != null && parent is ScrollView) {
        (parent as ViewGroup).viewTreeObserver.addOnScrollChangedListener(this)
    }
}
override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    if (parent != null && parent is ScrollView) {
        (parent as ScrollView).viewTreeObserver.removeOnScrollChangedListener(this)
    }
}

如果你的最低API在24之上,那你就不需要用 viewTreeObserver 这么暴力的东西,可以直接 parent.addOnScrollChangedListener

onScrollChanged 事件中获取偏移量,计算各子 View 的相对偏移值

/**
 * parent 的 onScrollChanged 事件
 * 父亲滚动改变时,根据每个元素的滚动速度进行调整 view.translationY
 */
private var parentLastScrollY = 0
override fun onScrollChanged() {
    val currentScrollY = (parent as ViewGroup).scrollY
    val delta = currentScrollY - parentLastScrollY
    for (child in parallaxChildren) {
        val translationDelta = -delta * (child.speed - 1f)
        child.view.translationY += translationDelta.toInt()
    }
    parentLastScrollY = currentScrollY
}

3. 总结

最后再贴一遍整体流程图:

** 开头的部分是我们要做的工作


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK