这个交互也太炸裂了趴

动画是为一个app创造出色用户体验的重要组成部分。 它的关键挑战是向用户解释应用程序的逻辑,但是常见的错误是鲁莽地使用动画,从而否定了改善用户体验的整个观点。 为了使应用出色而不仅仅是出色或平庸,动画必须正确集成并且不应多余。

在本文中,您将了解如何使用ScrollView和react-native的Animated API创建标题动画。在文章结尾之后,我们将获得以下输出

图1

How it works?

在ScrollView上渲染标头,并将ScrollView的顶部位置设置为标头的偏移量。然后,我们可以简单地使用ScrollView滚动位置插入标头。因此,让我们了解一下。

Let’s Code

我们使用Interpolate来更改位置,大小,颜色和Animated.event(),以将ScrollView位置与动画状态值进行映射。您可以阅读有关Interpolate(https://animationbook.codedaily.io/interpolation)和Animated.event(https://animationbook.codedaily.io/animated-event/)的更多详细信息。

始终在动画配置中设置useNativeDriver:true。通过跳过每个渲染帧的JS代码和本机代码之间的桥梁,可以使动画流畅。

  • 由于React-Native的限制,seNativeDriver当前不支持绝对位置。为此,我们使用了transform属性。对于顶部,我们使用translateY,对于左边,我们使用translateX。

使用来自构造函数的动画值初始化状态scrollY,如下所示:

constructor(props) {        
   super(props);       
   this.state = {            
       scrollY: new Animated.Value(0)        
    };    
}

现在我们不希望组件重新呈现,因此我们不使用setState()方法。我们使用ScrollView的onScroll属性来映射带有滚动位置的状态:

<Animated.ScrollView
    overScrollMode={'never'}                    
    style={{zIndex: 10}}
    scrollEventThrottle={16}                    
    onScroll={Animated.event(
        [
          {
             nativeEvent: {contentOffset:{y:this.state.scrollY}}
          }
        ]
    )}>
      //child here
</Animated.ScrollView>

Animated.event()方法使用ScrollView的Y偏移量映射状态scrollY表示垂直偏移量。

scrollEventThrottle 控制滚动时触发滚动事件的频率(以毫秒为单位的时间间隔)。 较小的数字可提高跟踪滚动位置的代码的准确性,但由于通过网桥发送的信息量大,因此可能导致滚动性能问题。 当JS运行循环同步到屏幕刷新率时,您不会注意到在1-16之间设置的值之间存在差异。

现在是时候对轮廓图像进行动画处理了,以借助Interpolation从屏幕中间到左侧标题进行动画处理。

//artist profile image position from left
_getImageLeftPosition = () => {
    const {scrollY} = this.state;

    return scrollY.interpolate({
        inputRange: [0, 80, 140],
        outputRange: [ThemeUtils.relativeWidth(30), ThemeUtils.relativeWidth(38), ThemeUtils.relativeWidth(10)],
        extrapolate: 'clamp',
        useNativeDriver: true
    });
};

可以先通过插值来运行每个属性。 插值通常使用线性插值将输入范围映射到输出范围,但还支持缓动功能。 默认情况下,它将外推曲线超出给定的范围,但是您也可以将其钳制输出值。

在这里,从输入范围0–80,我们的图像将相对于设备宽度的位置从30更改为38,从80–140则将相对位置更改为38至10。
当状态值从我们之前完成的ScrollView的onScroll映射更改时调用的插值方法。

//artist profile image position from top
_getImageTopPosition = () => {
    const {scrollY} = this.state;

    return scrollY.interpolate({
        inputRange: [0, 140],
        outputRange: [ThemeUtils.relativeHeight(20), Platform.OS === 'ios' ? 8 : 10],
        extrapolate: 'clamp',
        useNativeDriver: true
    });
};

 //artist profile image width
  _getImageWidth = () => {
      const {scrollY} = this.state;

      return scrollY.interpolate({
          inputRange: [0, 140],
          outputRange: [ThemeUtils.relativeWidth(40), ThemeUtils.APPBAR_HEIGHT - 20],
          extrapolate: 'clamp',
          useNativeDriver: true
      });
  };

  //artist profile image height
  _getImageHeight = () => {
      const {scrollY} = this.state;

      return scrollY.interpolate({
          inputRange: [0, 140],
          outputRange: [ThemeUtils.relativeWidth(40), ThemeUtils.APPBAR_HEIGHT - 20],
          extrapolate: 'clamp',
          useNativeDriver: true
      });
  };

与左位置相同,我们将位置设置为top,根据ScrollView的滚动位置设置图像的高度和宽度。现在,以Animated.Image的样式设置这些值,如下所示:

render() {
    const profileImageLeft = this._getImageLeftPosition();
    const profileImageTop = this._getImageTopPosition();
    const profileImageWidth = this._getImageWidth();
    const profileImageHeight = this._getImageHeight();

return(
      <Animated.Image
            style={
                [styles.profileImage, {
                borderRadius: (ThemeUtils.APPBAR_HEIGHT - 20) / 2,
                height: profileImageHeight,
                width: profileImageWidth,
                transform: [
                       {translateY: profileImageTop},
                       {translateX: profileImageLeft}
                    ]
                }]}
                source={profileImage}
      />
    );
}

现在,当用户滚动ScrollView时,将调用onScroll方法,我们的状态scrollY将更改,并且当stateY更改后,将调用内插法,并且完成了精美的动画。
在当前示例中,我使用了许多插值值,例如图像边框宽度,图像边框颜色,标题不透明度等。所有方法都在滚动位置映射的相同原理下工作。

最终代码:

import React, {Component, PureComponent} from 'react';
import {
    Animated,
    View,
    StatusBar,
    Text,
    Image,
    Platform,
    StyleSheet,
    Linking,
    TouchableOpacity,
} from 'react-native';

/*Data*/
import artistData from './assets/data/SongData.json';
import MaterialAnimatedView from './MaterialAnimatedView';
import {HTouchable} from '../../components/HTouchable';
import {Navigation, Options} from 'react-native-navigation';
import {MyColors} from '../../config/Colors';
import {ThemeUtils} from './utils/ThemeUtils';
const ARTIST_NAME = 'Villa_Mou';
const coverImage = require('./assets/images/bg.png');
const profileImage = require('./assets/images/icon.png');
const backImage = require('./assets/images/back.png');

interface Props {
    componentId: string;
}
interface State {
    scrollY: any;
}
/**
 * 图片高度 屏幕30%
 */
const HEADER_IMAGE_HEIGHT = ThemeUtils.relativeHeight(30);
export default class ArtistScreen extends PureComponent<Props, State> {
    static options(): Options {
        return {
            topBar: {
                visible: false,
            },
            statusBar: {drawBehind: true, style: 'light', backgroundColor: 'rgba(0,0,0,0.4)'},
        };
    }
    constructor(props) {
        super(props);
        this.state = {
            scrollY: new Animated.Value(0),
        };
    }

    render() {
        return (
            <View style={styles.container}>
                {this.renderHeaderImageBg()}
                {this.renderHeader()}
                {this.renderUserIcon()}
                {this.renderVipCard()}
                {this.renderList()}
            </View>
        );
    }

    private renderVipCard = () => {
        const top = this._getVipCardTop();
        const opacity = this._getVipCardOpacity();
        return (
            <Animated.View
                style={{
                    height: 50,
                    backgroundColor: MyColors.BLACK,
                    position: 'absolute',
                    top: top,
                    left: 25,
                    right: 25,
                    justifyContent: 'center',
                    paddingLeft: 15,
                    borderTopLeftRadius: 6,
                    borderTopRightRadius: 6,
                    opacity,
                }}>
                <Text style={{color: '#744307', fontWeight: 'bold', fontSize: 16}}>专属VIP卡</Text>
            </Animated.View>
        );
    };

    private renderList = () => {
        const listViewTop = this._getListViewTopPosition();
        const normalTitleOpacity = this._getNormalTitleOpacity();
        return (
            <Animated.ScrollView
                overScrollMode={'never'}
                style={{zIndex: 10}}
                scrollEventThrottle={16}
                onScroll={Animated.event([
                    {
                        nativeEvent: {contentOffset: {y: this.state.scrollY}},
                    },
                ])}>
                <Animated.Text
                    style={[
                        styles.profileTitle,
                        {
                            opacity: normalTitleOpacity,
                        },
                    ]}>
                    {ARTIST_NAME}
                </Animated.Text>

                <Animated.View
                    style={{
                        transform: [
                            {
                                translateY: listViewTop,
                            },
                        ],
                    }}>
                    {artistData.map((item, index) => this.renderItem(index, item))}
                </Animated.View>
            </Animated.ScrollView>
        );
    };

    private renderUserIcon = () => {
        const profileImageLeft = this._getImageLeftPosition();

        const profileImageTop = this._getImageTopPosition();

        const profileImageWidth = this._getImageWidth();

        const profileImageHeight = this._getImageHeight();

        const profileImageBorderWidth = this._getImageBorderWidth();

        const profileImageBorderColor = this._getImageBorderColor();
        return (
            <Animated.Image
                style={[
                    styles.profileImage,
                    {
                        borderWidth: profileImageBorderWidth,
                        borderColor: profileImageBorderColor,
                        borderRadius: (ThemeUtils.APPBAR_HEIGHT - 20) / 2,
                        height: profileImageHeight,
                        width: profileImageWidth,
                        transform: [{translateY: profileImageTop}, {translateX: profileImageLeft}],
                    },
                ]}
                source={profileImage}
            />
        );
    };

    private renderHeaderImageBg = () => {
        const headerImageOpacity = this._getHeaderImageOpacity();
        return (
            <Animated.Image
                style={[
                    styles.headerImageStyle,
                    {
                        opacity: headerImageOpacity,
                    },
                ]}
                source={coverImage}
            />
        );
    };

    private renderHeader = () => {
        const headerBackgroundColor = this._getHeaderBackgroundColor();
        const headerTitleOpacity = this._getHeaderTitleOpacity();
        return (
            <View>
                {Platform.OS === 'android' && (
                    <Animated.View
                        style={{
                            height: StatusBar.currentHeight,
                            backgroundColor: headerBackgroundColor,
                        }}
                    />
                )}
                <Animated.View
                    style={[
                        styles.headerStyle,
                        {
                            backgroundColor: headerBackgroundColor,
                        },
                    ]}>
                    <HTouchable
                        style={styles.headerLeftIcon}
                        onPress={() => {
                            Navigation.pop(this.props.componentId);
                        }}>
                        <Image source={backImage} />
                    </HTouchable>

                    <Animated.Text
                        style={[
                            styles.headerTitle,
                            {
                                opacity: headerTitleOpacity,
                            },
                        ]}>
                        {ARTIST_NAME}
                    </Animated.Text>
                </Animated.View>
            </View>
        );
    };

    renderItem = (index, item) => {
        return (
            <MaterialAnimatedView key={index.toString()} index={index}>
                <HTouchable style={styles.artistCardContainerStyle} onPress={() => {}}>
                    <Image source={{uri: item.artistImage}} style={styles.artistImage} />
                    <View style={styles.cardTextContainer}>
                        <Text numberOfLines={1} style={styles.songTitleStyle}>
                            {item.songName}
                        </Text>
                        <Text numberOfLines={1}>{item.albumName}</Text>
                    </View>
                </HTouchable>
            </MaterialAnimatedView>
        );
    };

    // scrollY从 0到 140 ,颜色从透明到黑色
    _getHeaderBackgroundColor = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: ['rgba(0,0,0,0.0)', MyColors.BLACK],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //scrollY从 0到 140 ,透明度从1到0
    _getHeaderImageOpacity = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: [1, 0],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist profile image position from left
    _getImageLeftPosition = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 80, 140],
            outputRange: [
                ThemeUtils.relativeWidth(35),
                ThemeUtils.relativeWidth(38),
                ThemeUtils.relativeWidth(12),
            ],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist profile image position from top
    _getImageTopPosition = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: [
                ThemeUtils.relativeHeight(20),
                Platform.OS === 'ios' ? 8 : 10 + StatusBar.currentHeight,
            ],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist profile image width
    _getImageWidth = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: [ThemeUtils.relativeWidth(30), ThemeUtils.APPBAR_HEIGHT - 20],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist profile image height
    _getImageHeight = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: [ThemeUtils.relativeWidth(30), ThemeUtils.APPBAR_HEIGHT - 20],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist profile image border width
    _getImageBorderWidth = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: [StyleSheet.hairlineWidth * 3, StyleSheet.hairlineWidth],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist profile image border color
    _getImageBorderColor = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 140],
            outputRange: [MyColors.CARD_BG_COLOR, 'rgba(255,255,255,1)'],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    _getVipCardTop = () => {
        const {scrollY} = this.state;
        return scrollY.interpolate({
            inputRange: [0, 50],
            outputRange: [ThemeUtils.relativeHeight(37), ThemeUtils.relativeHeight(30)],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    _getVipCardOpacity = () => {
        const {scrollY} = this.state;
        return scrollY.interpolate({
            inputRange: [0, 50, 60],
            outputRange: [1, 1, 0],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //Song list container position from top
    _getListViewTopPosition = () => {
        const {scrollY} = this.state;
        //TODO:   高度待处理
        return scrollY.interpolate({
            inputRange: [0, 250],
            outputRange: [ThemeUtils.relativeHeight(100) - ThemeUtils.relativeHeight(70), 0],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //header title opacity
    _getHeaderTitleOpacity = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 20, 50],
            outputRange: [0, 0.5, 1],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };

    //artist name opacity
    _getNormalTitleOpacity = () => {
        const {scrollY} = this.state;

        return scrollY.interpolate({
            inputRange: [0, 20, 30],
            outputRange: [1, 0.5, 0],
            extrapolate: 'clamp',
            useNativeDriver: true,
        });
    };
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: 'white',
    },
    /*header style*/
    headerLeftIcon: {
        position: 'absolute',
        left: ThemeUtils.relativeWidth(2),
    },
    headerRightIcon: {
        position: 'absolute',
        right: ThemeUtils.relativeWidth(2),
    },
    headerStyle: {
        height: ThemeUtils.APPBAR_HEIGHT,
        width: '100%',
        alignItems: 'center',
        justifyContent: 'center',
    },
    headerTitle: {
        textAlign: 'center',
        justifyContent: 'center',
        color: MyColors.HEADER_TEXT_COLOR,
        fontSize: ThemeUtils.fontNormal,
        fontWeight: 'bold',
    },
    /*Top Image Style*/
    headerImageStyle: {
        height: HEADER_IMAGE_HEIGHT,
        width: '100%',
        top: 0,
        alignSelf: 'center',
        position: 'absolute',
    },
    /*profile image style*/
    profileImage: {
        position: 'absolute',
        zIndex: 100,
    },
    /*profile title style*/
    profileTitle: {
        textAlign: 'center',
        color: MyColors.white,
        top: ThemeUtils.relativeHeight(29),
        left: 0,
        fontWeight: 'bold',
        right: 0,
        fontSize: 20,
    },
    /*song count text style*/
    songCountStyle: {
        position: 'absolute',
        textAlign: 'center',
        fontWeight: '400',
        top: ThemeUtils.relativeHeight(40),
        left: 0,
        right: 0,
        fontSize: ThemeUtils.fontNormal,
    },
    artistCardContainerStyle: {
        backgroundColor: MyColors.CARD_BG_COLOR,
        elevation: 5,
        shadowRadius: 3,
        shadowOffset: {
            width: 3,
            height: 3,
        },
        paddingVertical: 5,
        paddingHorizontal: 15,
        borderRadius: 6,
        marginBottom: 5,
        marginHorizontal: 15,
        flexDirection: 'row',
        alignItems: 'center',
    },
    artistImage: {
        height: ThemeUtils.relativeWidth(15),
        width: ThemeUtils.relativeWidth(15),
        borderRadius: ThemeUtils.relativeWidth(7.5),
        borderWidth: 1,
    },
    songTitleStyle: {
        fontSize: ThemeUtils.fontNormal,
        color: MyColors.BLACK,
    },
    cardTextContainer: {
        flex: 1,
        margin: ThemeUtils.relativeWidth(3),
    },
});

当然可能还存在部分适配问题,这个需要花时间去调一下,希望你喜欢这篇文章哦

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值