虽然React Native已经不在是当前热门的跨平台技术了,但是还是可以看到很多的公司和个人在用,Flutter有Flutter好,RN呢也有适合自己的地方,至于究竟哪个更好,我们不做过多的比较。
最近,我在升级之前的《React Native移动开发实战》一书,书中的项目有城市切换的功能,效果如下图所示。
在这里插入图片描述
可以看到,这个城市选择页面是很常规的,包含了当前定位城市和城市列表,右侧可以通过SlideBar进行快捷定位,除此之外,此组件还支持搜索功能。

首先,我们看一下城市列表,对于这一功能,我们可以使用SectionList组件,因为我们可以使用SectionList的ListHeaderComponent属性来实现当前定位布局,而右边的字母索引效果需要借助SectionList的scrollToLocation()函数,如下所示。

const _scrollTo = (index, letter) => {
    listViewRef?.current?.scrollToLocation({itemIndex: 0, sectionIndex: index});
  };

而搜索功能就更加简单了,使用FlatList组件展示即可,此处也可以使用list.map循环来开发列表功能。
CitySelectScreen.js

import React, {useState, useEffect} from 'react';
import {
  View,
  Text,
  TextInput,
  StyleSheet,
  TouchableOpacity,
  Keyboard,
} from 'react-native';
import PropTypes from 'prop-types';

import {CityList} from './components';
import apiRequest from '../../api';
import Header from '../../common/Header/Header';

const CitySelectScreen = ({location = '上海市', navigation}) => {
  let inputRef = null;
  const [cities, setCities] = useState([]);
  const [currentCityList, setCurrentCityList] = useState({});
  const [isFocused, setIsFocused] = useState(false);
  const [result, setResult] = useState([]);
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    getCities();
  }, []);

  const onChangeText = e => {
    setKeyword(e);
  };

  const onSelectCity = city => {
    setTimeout(() => {
      navigation.navigate('SelectCinemaScreen', {
        title: city.CITY_NAME,
        CITY_CD: city.CITY_CD,
      });
    }, 200);
    setResult([]);
  };

  const searchSubmit = () => {
    if (isFocused) {
      inputRef.blur();
      setIsFocused(false);
      setResult([]);
      Keyboard.dismiss();
    } else {
      setIsFocused(true);
      inputRef.focus();
    }
  };

  const getCities = async () => {
    let url = 'https://prd-api.cgv.com.cn/product/areas/that/group';
    const res = await apiRequest.get(url);
    setCities(res);
  };

  const searchCities = async () => {
    let url = 'https://prd-api.cgv.com.cn/product/areas/that/group';
    const params = {condition: keyword};
    const res = await apiRequest.get(url, params);
    console.log(res[0].data);
    setResult(res[0].data);
  };

  const onCurrentPress = (name = '上海市') => {
    cities.map(item =>
      item.data.map(val => {
        if (val.CITY_NAME === name) {
          onSelectCity(val);
          return null;
        }
      }),
    );
  };

  const renderSearchView = () => {
    return (
      <View style={styles.searchView}>
        <TextInput
          style={{flex: 1}}
          assignRef={c => {
            inputRef = c;
          }}
          onChangeText={onChangeText}
          returnKeyType="search"
          onSubmitEditing={() => {
            if (keyword) {
              searchCities();
            }
          }}
          onFocus={() => setIsFocused(true)}
          placeholder="输入城市名或拼音"
        />
        <TouchableOpacity onPress={() => searchSubmit(isFocused)}>
          <Text style={styles.searchTxt}>{!isFocused ? '搜索' : '取消'}</Text>
        </TouchableOpacity>
      </View>
    );
  };

  return (
    <View style={styles.container}>
      <Header title={'选择城市'} />
      {renderSearchView()}
      <View style={{flex: 1}}>
        {(!isFocused && !keyword && keyword.length < 1) || !isFocused ? (
          <CityList
            onCurrentCityPress={onCurrentPress}
            onSelectCity={onSelectCity}
            currentCity={location}
            allCityList={cities}
            currentCityList={currentCityList}
          />
        ) : (
          <SearchResult list={result} onSelectCity={onSelectCity} />
        )}
      </View>
    </View>
  );
};

const SearchResult = ({list, onSelectCity}) => {
  return (
    <View style={{marginTop: 10}}>
      {list.map((item, index) => (
        <TouchableOpacity
          activeOpacity={0.7}
          key={index.toString()}
          style={styles.rowView}
          onPress={() => {
            onSelectCity(item);
          }}>
          <View style={styles.rowdata}>
            <Text type="subheading">{item.CITY_NAME}</Text>
          </View>
        </TouchableOpacity>
      ))}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
  },
  searchView: {
    height: 48,
    flexDirection: 'row',
    backgroundColor: '#fff',
    alignItems: 'center',
    paddingLeft: 10,
    paddingRight: 10,
  },
  searchTxt: {
    color: '#FC5869',
    marginRight: 5,
    fontSize: 16,
  },
  rowView: {
    backgroundColor: '#fff',
    height: 44,
    paddingLeft: 13,
    justifyContent: 'center',
  },
  leftIcon: {
    width: 28,
    height: 28,
    paddingLeft: 13,
  },
});

CitySelectScreen.propTypes = {
  cities: PropTypes.array,
  getCities: PropTypes.func,
};

export default CitySelectScreen;

CityList.js代码如下:

import React, {useEffect, useRef} from 'react';
import {
  View,
  SectionList,
  TouchableOpacity,
  StyleSheet,
  Text,
  Image,
  Dimensions,
} from 'react-native';
import PropTypes from 'prop-types';
import ItemSeparatorComponent from '../../../../common/ItemSeparator';
import location from '../../../../assets/images/home/location.png';
import refresh from '../../../../assets/images/home/refresh.png';

const {width} = Dimensions.get('window');

const propTypes = {
  keyword: PropTypes.string,
  onChangeTextKeyword: PropTypes.func,
};

const defaultProps = {};

const CityList = ({
  onSelectCity,
  allCityList = [],
  currentCity,
  onCurrentCityPress,
  position: _position,
}) => {
  const listViewRef = useRef(null);

  useEffect(() => {
    console.log(allCityList);
  }, []);

  const city =
    currentCity && currentCity.city
      ? currentCity.city
      : '定位失败,请手动选择城市';

  const _cityNameClick = cityJson => {
    onSelectCity(cityJson);
  };

  const getLocation = async () => {
    // await PermissionsAndroid.request(
    //   PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
    // );
    // const res = await Geocode.reverse({
    //   latitude: '29.604451007313266',
    //   longitude: '106.52727499999997',
    // });
    // _position(res);
  };

  const CityHeader = props => {
    const {currentCity = '上海', onCurrentCityPress} = props;
    return (
      <View>
        <View style={styles.sectionTitle}>
          <Text style={{fontSize: 15}}>当前城市</Text>
        </View>
        <TouchableOpacity
          activeOpacity={0.7}
          onPress={() => onCurrentCityPress(currentCity)}
          style={styles.headerContainer}>
          <View style={styles.headerLeft}>
            <Image source={location} style={{width: 20, height: 20}} />
            <Text type="subheading" style={{marginLeft: 2}}>
              {currentCity}
            </Text>
          </View>
          <TouchableOpacity activeOpacity={0.7} onPress={() => getLocation()}>
            <Image source={refresh} style={{width: 20, height: 20}} />
          </TouchableOpacity>
        </TouchableOpacity>
      </View>
    );
  };

  const _renderListRow = (cityJson, rowId) => {
    return (
      <TouchableOpacity
        activeOpacity={0.7}
        key={`list_item_${cityJson.item.CITI_CD}`}
        style={styles.rowView}
        onPress={() => _cityNameClick(cityJson.item)}>
        <View style={styles.rowData}>
          <Text type="subheading">{cityJson.item.CITY_NAME}</Text>
        </View>
      </TouchableOpacity>
    );
  };

  const _scrollTo = (index, letter) => {
    listViewRef?.current?.scrollToLocation({itemIndex: 0, sectionIndex: index});
  };

  const _renderRightLetters = (letter, index) => {
    return (
      <TouchableOpacity
        key={`letter_idx_${index}`}
        activeOpacity={0.6}
        onPress={() => {
          _scrollTo(index, letter);
        }}>
        <View style={styles.letter}>
          <Text>{letter}</Text>
        </View>
      </TouchableOpacity>
    );
  };

  return (
    <View style={styles.container}>
      <SectionList
        getItemLayout={(param, index) => ({
          length: 44,
          offset: 44 * index,
          index,
        })}
        ListHeaderComponent={
          <CityHeader
            currentCity={city}
            onCurrentCityPress={onCurrentCityPress}
          />
        }
        ref={listViewRef}
        sections={allCityList}
        keyExtractor={(item, index) => index.toString()}
        renderItem={_renderListRow}
        ItemSeparatorComponent={() => <ItemSeparatorComponent />}
        renderSectionHeader={({section: {name}}) => (
          <View style={styles.sectionTitle}>
            <Text style={{fontSize: 15}}>{name}</Text>
          </View>
        )}
        stickySectionHeadersEnabled={true}
      />
      <View style={styles.letterSpace}>
        {allCityList.map((item, index) =>
          _renderRightLetters(item.name, index),
        )}
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F4F4F4',
  },
  sectionTitle: {
    paddingVertical: 5,
    paddingLeft: 12,
    backgroundColor: '#F3F4F5',
  },
  iconContainer: {
    height: 40,
    justifyContent: 'center',
    alignItems: 'center',
  },
  leftIconContainer: {
    marginEnd: 12,
  },
  rightIconContainer: {
    marginStart: 8,
  },
  headerView: {
    width: width,
    display: 'flex',
    flexDirection: 'row',
    position: 'relative',
    alignItems: 'center',
    backgroundColor: '#fff',
  },
  leftIcon: {
    width: 15,
    height: 13,
    marginLeft: 15,
    marginRight: 5,
  },
  rowView: {
    paddingLeft: 12,
    backgroundColor: '#fff',
  },
  rowData: {
    width: width,
    height: 44,
    justifyContent: 'center',
  },
  headerContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: '#fff',
    paddingHorizontal: 12,
    height: 44,
  },
  headerLeft: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  letter: {
    marginBottom: 3,
  },
  letterSpace: {
    position: 'absolute',
    right: 4,
    bottom: 0,
    top: 0,
    justifyContent: 'center',
  },
});

CityList.propTypes = propTypes;
CityList.defaultProps = defaultProps;

export default CityList;

另外,我们的网络请求使用的是Axios,相关内容可以查看我之前文章的介绍:React Native使用axios进行网络请求

源码:https://github.com/xiangzhihong/rn_city


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》