rn-relates icon indicating copy to clipboard operation
rn-relates copied to clipboard

记一次滑动组件开发

Open ljunb opened this issue 6 years ago • 0 comments

背景

在最近的一版需求中,App 的目的地详情页面,接入了中文站旅拍的入口,支持查看该目的地下或是 POI 下的相关旅拍。在旅拍详情页面中,参考中文站的功能交互,头图在滑动切换时需要做高度的渐变动画。

在自定义的滑动组件中,为了有更优效果,iOS、Android 两端分别采用了 ScrollView、ViewPagerAndroid 来实现。实际开发发现,把 Animated.View 作用于 ScrollView 外层,可达到目的。

而 ViewPagerAndroid 则无法使用该方式,由此引出 Animated.createAnimatedComponent 来封装 ViewPagerAndroid。

问题

组件渲染

iOS 端调试完毕后,开始着手 Android 端的实现。基于旅拍详情页的设计稿,该页面的最外层是需要使用 ScrollView 的,所以滑动组件属于 ScrollView 子组件。这里遇到第一个坑,无论是设置 ViewPagerAndroid 或其父组件的宽高,还是 flex 设置为1 ,组件都没有正常渲染出来。在官方 issue 列表中没有发现相关问题。重新检查代码,无意中看到最外层 ScrollView 组件设置了 removeClippedSubviews={true} ,看到这个当然是选择移除他了!果然,组件正常出来了。

实例引用

这个问题是在组件完成之后,想实现循环滑动效果时遇到的。主要是在 Android 端,在使用 Animated.createAnimatedComponent 封装支持动画的 AnimatedViewPager 后,在某些时机需要调用原始组件的实例方法 setPageWithoutAnimation ,但是报错:undefined is not a function ( evaluating 'this.viewPager.setPageWithoutAnimation(index)')

从错误信息来看,代码是可以正常获取到 AnimatedViewPager 的实例 this.viewPager,但是该实例并没有 setPageWithoutAnimation 方法,由此推测获取的实例并不是原始的 ViewPagerAndroid 实例。

以关键字 createAnimatedComponent 在 issue 列表搜了下,有相关的讨论 createAnimatedComponent should use forwardRef?。在底下的回复中,可以看到一个 getNode() 的调用。翻了下官方 createAnimatedComponent.js 的源码:该函数其实是一个高阶函数,原始组件的实例引用被保存在了一个内部变量中,可通过 getNode 调用。修改后代码:

// LiteBannerView.js
export default class LiteBannerView extends React.Component<LiteBannerViewProps, {}> {
    ...
    renderAndroidComponent = () => {
        return (
            <AnimatedViewPager
                ref={r => this.viewPager = r}
                style={{
                    height: this.props.animatedHeight,
                    width: Screen.width
                }}
                initialPage={this.props.loop ? 1 : 0}
                onPageScroll={this.handlePageScroll}
                onPageSelected={this.handlePageSelected}
            >
                {this.state.items.map(this.renderBannerItem)}
            </AnimatedViewPager>
        );
    };

    handlePageSelected = (evt) => {
        if (!this.props.loop) return;

        const { items = [] } = this.props;
        const { position } = evt.nativeEvent;
        // 使用 Animated.createAnimatedComponent 创建新组件后,
        // 使用 getNode 获取原始组件实例,否则无法调用绑定在原始组件上的方法
        const instance = this.viewPager.getNode();
        if (position === 0) {
            instance && instance.setPageWithoutAnimation(items.length);
        } else if (position === items.length + 1) {
            instance && instance.setPageWithoutAnimation(1);
        }
    };
}

遗留问题

以最终测试结果来看,组件的循环滑动功能是正常的,存在的问题是:第一张和最后一张两者切换时,无法做到高度的渐变。

该问题主要是因为组件的 animatedHeight 是由详情页面基于每张图片生成的动画插值,插值的计算是基于原始图片数组的,而滑动组件在设计过程中,原理是在首尾各添加一张图片,再根据滑动距离重新更新 index 。这样就导致首尾两张图片的动画插值无法匹配了。

鉴于组件设置了 loop 参数,考虑移除 animatedHeight,添加 dynamicHeights 的参数来接收所有图片的高度,然后在组件内部根据 loop 生成对应的高度动画插值。待有空验证。

ljunb avatar Mar 13 '19 07:03 ljunb